From 7938d138aad54a7539a65787942cb79285db46b9 Mon Sep 17 00:00:00 2001 From: Peter Foldiak Date: Sun, 8 Nov 2026 01:35:20 +0530 Subject: [PATCH 1/5] JavaScript port (js/): the same store, wire-compatible with Python A versioned key -> record store for Node and browsers, ported from the Python core: staging, commits, sorted iteration, diff, three-way merge and reconciling a moved pointer, over memory, a Bee node (/bytes) or Freedom Browser's window.swarm. Plain modules with JSDoc types and no npm dependencies; keccak from js-sha3 0.9.3, vendored as swarmlite vendors it. js/tools/make_fixtures.py runs the Python code in src/ and records canonical encodings, Swarm addresses, the root after every commit of scripted and random stores (362 commits, both addressings) and three-way merges; the JS tests must reach the same bytes and roots. Live: OntoDAG's space pack (5,134 records, written by Python) listed from Swarm through Freedom's Ant node, every blob checked against its reference. Not ported yet: feed pointers, proofs, history, local-first. The package stays private until it has a name on npm. Co-Authored-By: Claude Opus 5.5 --- README.md | 3 + js/README.md | 158 + js/package.json | 41 + js/src/address.js | 129 + js/src/bytes.js | 98 + js/src/canonical.js | 160 + js/src/index.js | 16 + js/src/recordstore.js | 469 + js/src/stores.js | 213 + js/src/trie.js | 527 ++ js/test/address.test.js | 33 + js/test/api.test.js | 135 + js/test/canonical.test.js | 53 + js/test/fixtures/canonical.json | 296 + js/test/fixtures/merges.json | 1190 +++ js/test/fixtures/stores.json | 10979 ++++++++++++++++++++++++ js/test/fixtures/swarm-addresses.json | 66 + js/test/helpers.js | 34 + js/test/live.test.js | 29 + js/test/merge.test.js | 42 + js/test/store.test.js | 48 + js/tools/make_fixtures.py | 383 + js/vendor/js-sha3/LICENSE | 20 + js/vendor/js-sha3/PROVENANCE.md | 23 + js/vendor/js-sha3/index.js | 12 + js/vendor/js-sha3/sha3.js | 662 ++ 26 files changed, 15819 insertions(+) create mode 100644 js/README.md create mode 100644 js/package.json create mode 100644 js/src/address.js create mode 100644 js/src/bytes.js create mode 100644 js/src/canonical.js create mode 100644 js/src/index.js create mode 100644 js/src/recordstore.js create mode 100644 js/src/stores.js create mode 100644 js/src/trie.js create mode 100644 js/test/address.test.js create mode 100644 js/test/api.test.js create mode 100644 js/test/canonical.test.js create mode 100644 js/test/fixtures/canonical.json create mode 100644 js/test/fixtures/merges.json create mode 100644 js/test/fixtures/stores.json create mode 100644 js/test/fixtures/swarm-addresses.json create mode 100644 js/test/helpers.js create mode 100644 js/test/live.test.js create mode 100644 js/test/merge.test.js create mode 100644 js/test/store.test.js create mode 100644 js/tools/make_fixtures.py create mode 100644 js/vendor/js-sha3/LICENSE create mode 100644 js/vendor/js-sha3/PROVENANCE.md create mode 100644 js/vendor/js-sha3/index.js create mode 100644 js/vendor/js-sha3/sha3.js diff --git a/README.md b/README.md index 5c6a604..8d3e71c 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,9 @@ store = swarm_store("my-notes", owner=addr) # follow someone else's feed - **[Reference](docs/REFERENCE.md)** — compact and definition-first: every export, signature, error, and extra in tables, pinned against the code by `tests/test_reference.py`. The right document to hand to an AI agent. +- **[JavaScript port](js/README.md)** — early: the same store for Node and + browsers (including Freedom Browser's `window.swarm`), wire-compatible + with this package and tested against fixtures it generates. ## Testing diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..4357c23 --- /dev/null +++ b/js/README.md @@ -0,0 +1,158 @@ +# recordstore for JavaScript + +A versioned key → record store over content-addressed storage such as +[Ethereum Swarm](https://www.ethswarm.org/): the JavaScript port of the +[Python recordstore](../README.md), and wire-compatible with it. A store +written by either reads in the other, and equal content gives the same root +in both: byte for byte, under SHA-256 or Swarm addressing. + +**Status: early (0.1.0, not on npm yet).** The core is complete and tested +against the Python implementation: records, staging, commits, sorted +iteration, diff, three-way merge, and reconciling a moved pointer. Stores for +memory, a Bee node, and Freedom Browser's `window.swarm`. Not yet ported: +feed pointers (signing), proofs, history and undo, local-first storage. + +Plain JavaScript modules with JSDoc types and no npm dependencies, so a page +can import it with relative URLs only, including a page published on Swarm. +The one vendored file is js-sha3 0.9.3 for keccak, the same copy swarmlite +uses (see [vendor/js-sha3/PROVENANCE.md](vendor/js-sha3/PROVENANCE.md)). +Node 22 or later, or any current browser (SHA-256 addressing uses Web Crypto, +which browsers offer only in secure contexts). + +```js +import { RecordStore, MemoryBytesStore } from './recordstore/js/src/index.js'; + +const store = new RecordStore(new MemoryBytesStore()); +store.put('users/alice', { name: 'Alice', role: 'admin' }); +store.put('users/bob', { name: 'Bob' }); +const root = await store.commit(); // one reference names this version + +await store.get('users/alice'); // { name: 'Alice', role: 'admin' } +for await (const key of store.keys('users/')) console.log(key); + +const snapshot = RecordStore.at(root, store.blobs); // read-only, frozen +``` + +## Reading and writing on Swarm + +```js +import { RecordStore, BeeBytesStore } from './recordstore/js/src/index.js'; + +// Read any published store: no postage batch needed. +const pack = RecordStore.at( + '67f6be2688e5fb7ca85a463c3d2929dad2b177bb3edb6bb910e8fa110598cff3', + new BeeBytesStore('http://localhost:1633'), +); +await pack.get('abbey'); + +// Write: name a postage batch of the node's. +const blobs = new BeeBytesStore('http://localhost:1633', { batch: '' }); +``` + +Every blob read is checked against its reference by recomputing its Swarm +address, and every write against the reference the node returns. Writes go up +with redundancy level 1 for a blob that fits one chunk and 0 above, as the +Python store does, so that the node's reference is the plain content +address. A store written with erasure coding by some other tool reads with +`verify: false`. + +Measured on 8 November 2026: OntoDAG's `space` pack (5,134 records, written +by Python) listed in full from Swarm through the Ant node inside Freedom +Browser, cold, in 391 s, every blob checked (`test/live.test.js`). Listing +everything walks every trie node; a single `get` touches only the nodes on +its key's path. + +### In Freedom Browser + +A page in Freedom cannot reach a node's HTTP API; it has `window.swarm`. + +```js +import { RecordStore, SwarmProviderBytesStore } from './recordstore/js/src/index.js'; +const blobs = new SwarmProviderBytesStore(window.swarm); +``` + +Reads use `swarm_readData` and count against Freedom's per-site read budget. +Writes split each blob into chunks as Bee does and publish them with +`swarm_publishChunk`, so the references are Bee's and Python's; the first +write asks for access. **Not yet tried in Freedom.** Expect writes to be slow +there: one call per chunk, several seconds each through Ant in swarmtyp's +measurements. + +## The API + +Shaped like the Python one, but asynchronous and JavaScript-idiomatic: + +| | | +|---|---| +| `new RecordStore(blobs, {root, pointer, readonly, nodeCacheSize})` | a store at `root` (null: empty) | +| `await RecordStore.open(blobs, {pointer})` | a store at its pointer's current root | +| `RecordStore.at(root, blobs)` | a read-only snapshot | +| `store.put(key, value)` | stage a write (validated and copied now) | +| `await store.delete(key)` | stage a removal; `false` if there is no such record | +| `await store.get(key)` | the record (a fresh copy), or `undefined` if absent; records can be `null` | +| `await store.has(key)` | Python's `contains` | +| `await store.getMany(keys)` | a `Map` of the records present, read together | +| `for await (const k of store.keys(prefix))` | sorted keys, staged changes included | +| `for await (const [k, v] of store.items(prefix))` | sorted records | +| `for await (const [k, mine, theirs] of store.diff(otherRoot))` | what differs; a missing side is `ABSENT` | +| `await store.commit({reconcile, resolver, retries})` | write, return the new root, move the pointer | +| `await RecordStore.merge(blobs, base, ours, theirs, resolver)` | three-way merge; `MergeConflict` without a resolver | +| `store.refsUnder(prefix)` | every blob the keys under `prefix` need (for keeping them alive) | + +Errors: `RecordUnavailable` when a record exists but a blob on its path cannot +be read right now (never reported as absence), `MergeConflict`, +`BlobNotFoundError`, `VerificationError`. + +Bytes stores: `MemoryBytesStore({addressing: 'sha256' | 'swarm'})`, +`BeeBytesStore(url, {batch, verify, deferred, concurrency})`, +`SwarmProviderBytesStore(provider, {verify, concurrency})`. Anything with +async `put(bytes) → ref` and `get(ref) → bytes` works, optionally with +`putMany` and `getMany` (a `Map`). `MemoryPointer` is a pointer with +compare-and-set for writers in one process. + +## Compatibility with Python + +The canonical encoding is Python's `json.dumps(sort_keys=True, +separators=(",", ":"), ensure_ascii=False, allow_nan=False)`, reproduced byte +for byte: keys sorted by code point (not JavaScript's UTF-16 order), the same +string escapes, Python's float formatting. Where the languages differ: + +- **Numbers.** JavaScript has one number type. A safe integer is written as an + integer, so a Python float such as `1.0`, read and written back from + JavaScript, becomes `1`: a different blob, as the Python guide warns. Other + numbers are written as Python writes floats (`1e-05`, `1e+16`). +- **Large integers.** A `BigInt` is written as an integer of any size, and + reading gives a `BigInt` back for an integer too large for a number, where + the engine exposes the literal's source text (Node 21+, Chrome 114+). + Elsewhere such an integer reads as the nearest number. +- **Lone surrogates** in keys or strings are refused, as Python refuses to + encode them; `TextEncoder` would otherwise turn them into U+FFFD silently. +- **Not JSON:** `undefined`, functions, symbols, `Date`, `Map` and class + instances are refused rather than converted. + +## Tests + +```sh +npm test # Node's own runner, no install +RECORDSTORE_BEE_URL=http://localhost:1633 npm test # also read a live store +``` + +`test/fixtures/` is written by `tools/make_fixtures.py`, which runs the Python +implementation in `../src` (not an installed copy) and records canonical +encodings, Swarm addresses, the root after every commit of scripted and random +stores under both addressings, and three-way merges. The JavaScript tests +replay the same operations and must reach the same bytes and roots. After a +change to the Python format, `npm run fixtures`; `npm run fixtures:check` +fails while the fixtures are stale. + +## Not yet ported + +- `SwarmFeedPointer`: the store's latest root in a Swarm feed (needs signing; + swarmlite vendors noble-secp256k1 for it). +- Proofs (`verify_proof`, `verify_prefix`, `verify_extension`). +- History, undo and redo (`FilePointer`'s timeline). +- Local-first storage. + +## License + +BSD-3-Clause, as the Python package. The vendored js-sha3 is MIT. diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..8deac92 --- /dev/null +++ b/js/package.json @@ -0,0 +1,41 @@ +{ + "name": "recordstore", + "version": "0.1.0", + "private": true, + "description": "A versioned key -> record store over content-addressed storage such as Ethereum Swarm; wire-compatible JavaScript port of the Python recordstore", + "type": "module", + "main": "src/index.js", + "exports": { + ".": "./src/index.js" + }, + "files": [ + "src", + "vendor" + ], + "engines": { + "node": ">=22" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/petfold/recordstore.git", + "directory": "js" + }, + "homepage": "https://github.com/petfold/recordstore/tree/main/js#readme", + "keywords": [ + "swarm", + "ethswarm", + "key-value", + "content-addressed", + "merkle", + "trie", + "versioned", + "decentralized" + ], + "license": "BSD-3-Clause", + "author": "Peter Foldiak", + "scripts": { + "test": "node --test test/*.test.js", + "fixtures": "python3 tools/make_fixtures.py", + "fixtures:check": "python3 tools/make_fixtures.py --check" + } +} diff --git a/js/src/address.js b/js/src/address.js new file mode 100644 index 0000000..591d5c2 --- /dev/null +++ b/js/src/address.js @@ -0,0 +1,129 @@ +// Content addresses: SHA-256 (the default of MemoryBytesStore) and Swarm's +// own reference (BMT chunk hashes over Bee's chunk tree), ported from +// swarmfs's `bmt` and `splitter` modules. +// +// Swarm tree format (Bee's pkg/file/splitter): +// - a leaf is span (8 bytes, little-endian) + payload, payload <= 4096 bytes; +// - an intermediate is span + up to 128 child references (32 bytes each), +// span being the content length beneath it; +// - a level with a single entry is promoted, never wrapped; +// - empty content is one chunk with span 0 and an empty payload. +// +// This is the reference of a *plain* upload. A blob over one chunk uploaded +// with erasure coding gets another reference; `redundancyFor` names the level +// to upload with so that the node's reference is this one. + +import { keccak256Bytes } from '../vendor/js-sha3/index.js'; +import { concatBytes, fromHex, toHex } from './bytes.js'; + +export const CHUNK_SIZE = 4096; +const SEGMENT_SIZE = 32; +const REF_SIZE = 32; +const BRANCHES = CHUNK_SIZE / REF_SIZE; // 128 + +/** keccak256 of bytes, as bytes. */ +export { keccak256Bytes }; + +/** Root of the binary Merkle tree over a payload's 32-byte segments (zero-padded to 4096). */ +export function bmtRoot(payload) { + if (payload.length > CHUNK_SIZE) throw new RangeError(`payload of ${payload.length} bytes exceeds a chunk`); + let level = []; + for (let i = 0; i < CHUNK_SIZE; i += SEGMENT_SIZE) { + const seg = new Uint8Array(SEGMENT_SIZE); + seg.set(payload.subarray(i, Math.min(i + SEGMENT_SIZE, payload.length))); + level.push(seg); + } + while (level.length > 1) { + const next = []; + for (let i = 0; i < level.length; i += 2) next.push(keccak256Bytes(concatBytes(level[i], level[i + 1]))); + level = next; + } + return level[0]; +} + +/** Little-endian 8-byte span. */ +export function encodeSpan(span) { + const out = new Uint8Array(8); + let n = BigInt(span); + for (let i = 0; i < 8; i++) { + out[i] = Number(n & 0xffn); + n >>= 8n; + } + return out; +} + +/** BMT address of a content-addressed chunk (span + payload). */ +export function chunkAddress(chunk) { + return keccak256Bytes(concatBytes(chunk.subarray(0, 8), bmtRoot(chunk.subarray(8)))); +} + +/** + * Split data into Swarm chunks as Bee does. + * @param {Uint8Array} data + * @returns {{root: string, chunks: {address: string, payload: Uint8Array, span: number}[]}} + * the root reference (hex) and every chunk of the tree, leaves first, each + * with the payload and span to upload it with. + */ +export function splitData(data) { + const chunks = []; + const store = (payload, span) => { + const address = toHex(chunkAddress(concatBytes(encodeSpan(span), payload))); + chunks.push({ address, payload, span }); + return address; + }; + if (data.length === 0) return { root: store(new Uint8Array(0), 0), chunks }; + + let level = []; // [reference hex, content length beneath it] + for (let i = 0; i < data.length; i += CHUNK_SIZE) { + const payload = data.subarray(i, i + CHUNK_SIZE); + level.push([store(payload, payload.length), payload.length]); + } + while (level.length > 1) { + const parents = []; + for (let i = 0; i < level.length; i += BRANCHES) { + const group = level.slice(i, i + BRANCHES); + if (group.length === 1) { + parents.push(group[0]); // promote, never wrap + continue; + } + const span = group.reduce((sum, [, size]) => sum + size, 0); + const refs = new Uint8Array(group.length * REF_SIZE); + group.forEach(([ref], j) => refs.set(fromHex(ref), j * REF_SIZE)); + parents.push([store(refs, span), span]); + } + level = parents; + } + return { root: level[0][0], chunks }; +} + +/** Swarm's reference for `data` (plain upload), hex. */ +export function swarmRef(data) { + return splitData(data).root; +} + +/** SHA-256 of `data`, hex (MemoryBytesStore's references, as in Python). */ +export async function sha256Ref(data) { + const digest = await globalThis.crypto.subtle.digest('SHA-256', data); + return toHex(new Uint8Array(digest)); +} + +/** + * The redundancy level to upload a blob of `size` bytes with so that the + * node's reference is `swarmRef`'s: 1 while it fits one chunk (copies of the + * chunk, same reference), 0 above (parity would change the reference). + */ +export function redundancyFor(size) { + return size <= CHUNK_SIZE ? 1 : 0; +} + +/** + * An addressing scheme: 'sha256', 'swarm', or a function bytes -> reference + * (or a promise of one). + * @param {'sha256' | 'swarm' | ((data: Uint8Array) => string | Promise)} addressing + */ +export function resolveAddressing(addressing) { + if (typeof addressing === 'function') return addressing; + if (addressing === 'sha256') return sha256Ref; + if (addressing === 'swarm') return swarmRef; + throw new TypeError(`unknown addressing ${JSON.stringify(addressing)}; use 'sha256', 'swarm' or a function`); +} diff --git a/js/src/bytes.js b/js/src/bytes.js new file mode 100644 index 0000000..3fd7ddd --- /dev/null +++ b/js/src/bytes.js @@ -0,0 +1,98 @@ +// Byte, hex and text helpers shared by the other modules. +// +// Trie keys are handled as lowercase hex of their UTF-8 bytes. Hex keeps +// byte order under plain string comparison (so sorted hex is sorted UTF-8 is +// sorted code points, the order Python uses), slices on byte boundaries at +// even offsets, and is the form trie nodes store prefixes in anyway. + +const encoder = new TextEncoder(); +const decoder = new TextDecoder('utf-8', { fatal: true }); + +const HEX = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, '0')); + +/** Lowercase hex of a byte array. */ +export function toHex(bytes) { + let out = ''; + for (let i = 0; i < bytes.length; i++) out += HEX[bytes[i]]; + return out; +} + +/** Bytes of an even-length hex string (either case). */ +export function fromHex(hex) { + if (typeof hex !== 'string' || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) { + throw new TypeError(`not a hex string: ${String(hex).slice(0, 80)}`); + } + const out = new Uint8Array(hex.length / 2); + for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(2 * i, 2 * i + 2), 16); + return out; +} + +/** Concatenate byte arrays. */ +export function concatBytes(...parts) { + let size = 0; + for (const p of parts) size += p.length; + const out = new Uint8Array(size); + let at = 0; + for (const p of parts) { + out.set(p, at); + at += p.length; + } + return out; +} + +/** + * Does `s` contain a lone surrogate? Python cannot encode one to UTF-8 and + * refuses; TextEncoder would silently write U+FFFD instead, which would + * give a different key or value than the caller passed. So we refuse too. + */ +export function isWellFormed(s) { + if (typeof s.isWellFormed === 'function') return s.isWellFormed(); + return !/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? equal bytes, identical to Python's +// +// json.dumps(obj, sort_keys=True, separators=(",", ":"), +// ensure_ascii=False, allow_nan=False).encode("utf-8") +// +// Content addressing makes byte identity a correctness requirement: a value +// encoded differently here than in Python would get a different reference, +// and the store a different root, for the same content. +// +// Numbers are the one place the two languages differ. Python has int and +// float; JavaScript has one number type (plus BigInt). The rules: +// +// - A safe integer (|n| <= 2**53 - 1) is written as an integer, like a +// Python int. So a Python float such as 1.0 ("1.0"), read here, comes back +// as 1 and would be written "1": different bytes. Store what JSON +// represents faithfully, as the Python guide says. +// - Any other finite number is written as Python writes a float (its +// shortest round-trip repr, exponent from 1e16 and below 1e-4): 0.1, +// 1e-05, 1e+16, 9007199254740992.0. +// - A BigInt is written as an integer, like a Python int of any size, and +// `parseJson` gives back a BigInt for an integer literal that a number +// cannot hold exactly (where the engine exposes the literal's source text). +// - NaN and the infinities have no JSON form and are refused, as in Python. + +import { compareCodePoints, fromUtf8, isWellFormed, utf8 } from './bytes.js'; + +/** Canonical JSON text of a JSON-compatible value. */ +export function canonicalJson(value) { + return encode(value, 0); +} + +/** Canonical bytes (UTF-8 of `canonicalJson`). */ +export function canonicalBytes(value) { + return utf8(canonicalJson(value)); +} + +const MAX_DEPTH = 1000; // Python's default recursion limit is of this order + +function encode(v, depth) { + if (depth > MAX_DEPTH) throw new RangeError('value nested too deeply'); + if (v === null) return 'null'; + switch (typeof v) { + case 'boolean': + return v ? 'true' : 'false'; + case 'number': + return encodeNumber(v); + case 'bigint': + return v.toString(); + case 'string': + return encodeString(v); + case 'object': + break; + default: + throw new TypeError(`not JSON-serializable: ${typeof v}`); + } + if (Array.isArray(v)) { + const parts = []; + for (let i = 0; i < v.length; i++) { + if (!(i in v) || v[i] === undefined) throw new TypeError(`not JSON-serializable: array hole or undefined at index ${i}`); + parts.push(encode(v[i], depth + 1)); + } + return `[${parts.join(',')}]`; + } + const proto = Object.getPrototypeOf(v); + if (proto !== Object.prototype && proto !== null) { + throw new TypeError(`not JSON-serializable: ${Object.prototype.toString.call(v)} (only plain objects, arrays and primitives)`); + } + if (Object.getOwnPropertySymbols(v).length) throw new TypeError('not JSON-serializable: symbol keys'); + const keys = Object.keys(v).sort(compareCodePoints); + const parts = []; + for (const k of keys) { + if (v[k] === undefined) throw new TypeError(`not JSON-serializable: undefined at key ${JSON.stringify(k)}`); + parts.push(`${encodeString(k)}:${encode(v[k], depth + 1)}`); + } + return `{${parts.join(',')}}`; +} + +function encodeString(s) { + // JSON.stringify escapes exactly what Python's encoder escapes with + // ensure_ascii=False: " \ and the C0 controls, \b \f \n \r \t short and the + // rest as lowercase \u00XX; U+007F, U+2028 and U+2029 stay raw in both. + // The one difference is a lone surrogate, which Python refuses to encode. + if (!isWellFormed(s)) throw new TypeError('string contains a lone surrogate (not encodable as UTF-8)'); + return JSON.stringify(s); +} + +function encodeNumber(x) { + if (!Number.isFinite(x)) throw new RangeError(`${x} has no canonical JSON form`); + if (Number.isSafeInteger(x)) return String(x); // String(-0) === "0" + return pythonFloatRepr(x); +} + +/** + * Python's `repr(float)`: the shortest digits that round-trip (which + * `toExponential()` also gives), written positionally when the decimal + * exponent is in [-4, 16) and in exponent form otherwise, with a sign and at + * least two exponent digits; a positional integral value gets ".0". + */ +export function pythonFloatRepr(x) { + let s = x.toExponential(); + let sign = ''; + if (s[0] === '-') { + sign = '-'; + s = s.slice(1); + } + const [mantissa, expText] = s.split('e'); + const digits = mantissa.replace('.', ''); + const exp = parseInt(expText, 10); + const decpt = exp + 1; // digits are 0.d1d2... x 10**decpt + if (decpt <= -4 || decpt > 16) { + const m = digits.length > 1 ? `${digits[0]}.${digits.slice(1)}` : digits; + const e = String(Math.abs(exp)).padStart(2, '0'); + return `${sign}${m}e${exp < 0 ? '-' : '+'}${e}`; + } + if (decpt <= 0) return `${sign}0.${'0'.repeat(-decpt)}${digits}`; + if (decpt >= digits.length) return `${sign}${digits}${'0'.repeat(decpt - digits.length)}.0`; + return `${sign}${digits.slice(0, decpt)}.${digits.slice(decpt)}`; +} + +const INTEGER = /^-?(?:0|[1-9][0-9]*)$/; + +function reviver(_key, value, context) { + if (typeof value === 'number' && !Number.isSafeInteger(value) && + context && typeof context.source === 'string' && INTEGER.test(context.source)) { + return BigInt(context.source); + } + return value; +} + +/** + * JSON.parse that keeps large integers exact: an integer literal beyond + * 2**53 - 1 becomes a BigInt, where the engine passes the literal's source + * text to revivers (Node 21+, Chrome 114+). Elsewhere it becomes the nearest + * number, as plain JSON.parse gives. + */ +export function parseJson(text) { + return JSON.parse(text, reviver); +} + +// The record envelope, as Python's `_encode_value` / `_decode_value`. +const SCHEMA_VERSION = 1; + +/** Bytes of a record value as stored in its blob. */ +export function encodeValue(value) { + return canonicalBytes({ rsv: SCHEMA_VERSION, val: value }); +} + +/** The record value a blob holds. */ +export function decodeValue(bytes) { + const obj = parseJson(fromUtf8(bytes)); + if (obj === null || typeof obj !== 'object' || obj.rsv !== SCHEMA_VERSION) { + throw new Error(`unsupported record schema version: ${JSON.stringify(obj && obj.rsv)}`); + } + return obj.val; +} + +/** A deep copy that is also a validity check: what a stored value reads back as. */ +export function canonicalCopy(value) { + return parseJson(canonicalJson(value)); +} diff --git a/js/src/index.js b/js/src/index.js new file mode 100644 index 0000000..15780ec --- /dev/null +++ b/js/src/index.js @@ -0,0 +1,16 @@ +// recordstore for JavaScript: a versioned key -> record store over a +// content-addressed bytes store, wire-compatible with the Python package. + +export { + RecordStore, MemoryPointer, MergeConflict, RecordUnavailable, ABSENT, DELETE, +} from './recordstore.js'; +export { + MemoryBytesStore, BeeBytesStore, SwarmProviderBytesStore, BlobNotFoundError, VerificationError, +} from './stores.js'; +export { + canonicalBytes, canonicalJson, parseJson, encodeValue, decodeValue, +} from './canonical.js'; +export { + swarmRef, sha256Ref, splitData, chunkAddress, bmtRoot, redundancyFor, CHUNK_SIZE, +} from './address.js'; +export { DEFAULT_NODE_CACHE_SIZE } from './trie.js'; diff --git a/js/src/recordstore.js b/js/src/recordstore.js new file mode 100644 index 0000000..e36751f --- /dev/null +++ b/js/src/recordstore.js @@ -0,0 +1,469 @@ +// RecordStore: a staged, versioned key -> record store over a bytes store, +// ported from Python's `RecordStore`. Same data model and wire format; an +// asynchronous, JavaScript-shaped API: +// +// - reads and commits return promises, iteration is `for await`; +// - `get` resolves to `undefined` for an absent key (records can be null); +// - `delete` resolves to `false` for an absent key instead of raising; +// - `has` is Python's `contains`. + +import { canonicalCopy, decodeValue, encodeValue } from './canonical.js'; +import { hexUtf8, isWellFormed, utf8Hex } from './bytes.js'; +import { DEFAULT_NODE_CACHE_SIZE, KeyMissing, Trie } from './trie.js'; + +/** A side of a diff or merge where the key is absent. */ +export const ABSENT = Symbol.for('recordstore.ABSENT'); +/** What a merge resolver returns to drop a key. */ +export const DELETE = Symbol.for('recordstore.DELETE'); + +const TOMBSTONE = Symbol('tombstone'); + +/** Both sides of a merge changed the same keys to different values. */ +export class MergeConflict extends Error { + constructor(conflicts) { + const keys = [...conflicts]; + const shown = keys.slice().sort().slice(0, 5).join(', ') + (keys.length > 5 ? ', ...' : ''); + super(`unresolved merge conflict on ${keys.length} key(s): ${shown}`); + this.name = 'MergeConflict'; + this.conflicts = keys; + } +} + +/** + * The key exists under the committed root, but a blob on its path or its + * value cannot be read right now (missing from the store, network failure). + * Never reported as absence. + */ +export class RecordUnavailable extends Error { + constructor(message, cause) { + super(message, { cause }); + this.name = 'RecordUnavailable'; + } +} + +/** UTF-8 hex of a key, after checking it is a non-empty well-formed string. */ +function keyHex(key) { + if (typeof key !== 'string' || key === '') throw new TypeError('key must be a non-empty string'); + if (!isWellFormed(key)) throw new TypeError('key contains a lone surrogate (not encodable as UTF-8)'); + return utf8Hex(key); +} + +function prefixHex(prefix) { + if (typeof prefix !== 'string') throw new TypeError("prefix is a string ('' for every key)"); + if (!isWellFormed(prefix)) throw new TypeError('prefix contains a lone surrogate'); + return utf8Hex(prefix); +} + +async function fetchBlobs(blobs, refs) { + if (!refs.length) return new Map(); + if (typeof blobs.getMany === 'function') return blobs.getMany(refs); + const out = new Map(); + for (const r of refs) out.set(r, await blobs.get(r)); + return out; +} + +async function putBlobs(blobs, datas) { + if (!datas.length) return []; + if (typeof blobs.putMany === 'function') return blobs.putMany(datas); + const refs = []; + for (const d of datas) refs.push(await blobs.put(d)); + return refs; +} + +/** Values are fetched in windows of this many while iterating. */ +const WINDOW = 256; + +export class RecordStore { + /** + * @param {object} blobs a bytes store + * @param {{root?: string | null, pointer?: object | null, readonly?: boolean, + * nodeCacheSize?: number}} [options] + * `root`: the committed state to start from (null: empty). With a + * `pointer` and no root, use `RecordStore.open`, which reads the + * pointer first. + */ + constructor(blobs, { root = null, pointer = null, readonly = false, nodeCacheSize = DEFAULT_NODE_CACHE_SIZE } = {}) { + this._blobs = blobs; + this._trie = new Trie(blobs, nodeCacheSize); + this._root = root; + this._pointer = pointer; + this._readonly = readonly; + /** @type {Map} key -> staged value or TOMBSTONE */ + this._staged = new Map(); + } + + /** A store at its pointer's current root. */ + static async open(blobs, { pointer = null, ...options } = {}) { + const root = pointer ? await pointer.get() : null; + return new RecordStore(blobs, { ...options, root, pointer }); + } + + /** A read-only snapshot of any root. */ + static at(root, blobs) { + return new RecordStore(blobs, { root, readonly: true }); + } + + /** Root of the last committed state (staged changes not included). */ + get root() { + return this._root; + } + + /** The underlying bytes store. */ + get blobs() { + return this._blobs; + } + + /** Number of staged, uncommitted changes. */ + get stagedCount() { + return this._staged.size; + } + + // -- reading -------------------------------------------------------------- + + /** The record under `key` (a fresh copy), or `undefined` if absent. */ + async get(key) { + const kh = keyHex(key); + if (this._staged.has(key)) { + const staged = this._staged.get(key); + return staged === TOMBSTONE ? undefined : canonicalCopy(staged); + } + let vref; + try { + vref = await this._trie.get(this._root, kh); + } catch (e) { + throw new RecordUnavailable(`record ${JSON.stringify(key)}: a trie node on its path is unreachable: ${e.message}`, e); + } + if (vref === null) return undefined; + let blob; + try { + blob = await this._blobs.get(vref); + } catch (e) { + throw new RecordUnavailable(`record ${JSON.stringify(key)} exists (value ${vref.slice(0, 16)}...) but its bytes are unreachable: ${e.message}`, e); + } + return decodeValue(blob); + } + + /** Is there a record under `key`? */ + async has(key) { + return (await this.get(key)) !== undefined; + } + + /** + * The records under several keys, read together (about one round trip per + * trie level, and the values in one batch). Absent keys are left out. + * @returns {Promise>} + */ + async getMany(keys) { + const out = new Map(); + const walk = new Map(); // key hex -> key + for (const key of keys) { + const kh = keyHex(key); + if (this._staged.has(key)) { + const staged = this._staged.get(key); + if (staged !== TOMBSTONE) out.set(key, canonicalCopy(staged)); + } else { + walk.set(kh, key); + } + } + let wanted; + let blobs; + try { + const vrefs = await this._trie.getMany(this._root, [...walk.keys()]); + wanted = new Map(); + for (const [kh, vref] of vrefs) if (vref !== null) wanted.set(walk.get(kh), vref); + blobs = await fetchBlobs(this._blobs, [...new Set(wanted.values())].sort()); + } catch (e) { + throw new RecordUnavailable(`${walk.size} records: part of their trie or values is unreachable: ${e.message}`, e); + } + for (const [key, vref] of wanted) out.set(key, decodeValue(blobs.get(vref))); + return out; + } + + /** + * Committed and staged entries under `prefix`, in sorted key order, as + * [key, value ref | null, staged value | undefined]; tombstones dropped. + */ + async *_merged(prefix) { + const ph = prefixHex(prefix); + const staged = [...this._staged.keys()] + .map((k) => [utf8Hex(k), k]) + .filter(([kh]) => kh.startsWith(ph)) + .sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0)); + let si = 0; + for await (const [kh, vref] of this._trie.items(this._root, ph)) { + while (si < staged.length && staged[si][0] < kh) { + const sk = staged[si++][1]; + const v = this._staged.get(sk); + if (v !== TOMBSTONE) yield [sk, null, v]; + } + if (si < staged.length && staged[si][0] === kh) { + const sk = staged[si++][1]; + const v = this._staged.get(sk); + if (v !== TOMBSTONE) yield [sk, null, v]; + } else { + yield [hexUtf8(kh), vref, undefined]; + } + } + while (si < staged.length) { + const sk = staged[si++][1]; + const v = this._staged.get(sk); + if (v !== TOMBSTONE) yield [sk, null, v]; + } + } + + /** Sorted keys under `prefix` (code point order, as Python), staged changes included. */ + async *keys(prefix = '') { + for await (const [key] of this._merged(prefix)) yield key; + } + + /** Sorted [key, value] under `prefix`, staged changes included; values fetched a window at a time. */ + async *items(prefix = '') { + let buf = []; + let refs = []; + const flush = async function* (store) { + const blobs = await fetchBlobs(store._blobs, refs); + for (const [key, vref, staged] of buf) { + yield [key, vref === null ? canonicalCopy(staged) : decodeValue(blobs.get(vref))]; + } + }; + for await (const entry of this._merged(prefix)) { + buf.push(entry); + if (entry[1] !== null) refs.push(entry[1]); + if (refs.length >= WINDOW) { + yield* flush(this); + buf = []; + refs = []; + } + } + if (buf.length) yield* flush(this); + } + + /** + * [key, mine, theirs] for every key whose value differs between this + * store's committed root and `otherRoot`; a side without the key is + * `ABSENT`. Cost follows the difference, not the dataset. Keys arrive in + * no particular order. + */ + async *diff(otherRoot) { + let buf = []; + const flush = async function* (store) { + const refs = [...new Set(buf.flatMap(([, a, b]) => [a, b]).filter((r) => r !== null))]; + const blobs = await fetchBlobs(store._blobs, refs); + for (const [kh, mine, theirs] of buf) { + yield [hexUtf8(kh), + mine === null ? ABSENT : decodeValue(blobs.get(mine)), + theirs === null ? ABSENT : decodeValue(blobs.get(theirs))]; + } + }; + for await (const entry of this._trie.diff(this._root, otherRoot)) { + buf.push(entry); + if (buf.length >= WINDOW) { + yield* flush(this); + buf = []; + } + } + if (buf.length) yield* flush(this); + } + + /** ['node', ref] / ['value', ref] for every blob the keys under `prefix` need in the committed root. */ + refsUnder(prefix = '') { + return this._trie.refsUnder(this._root, prefixHex(prefix)); + } + + // -- writing -------------------------------------------------------------- + + /** Stage `value` under `key` (validated and copied now, written on commit). */ + put(key, value) { + if (this._readonly) throw new TypeError('read-only snapshot'); + keyHex(key); + this._staged.set(key, canonicalCopy(value)); + } + + /** Stage the removal of `key`; resolves to false (staging nothing) if there is no such record. */ + async delete(key) { + if (this._readonly) throw new TypeError('read-only snapshot'); + const kh = keyHex(key); + if (this._staged.has(key)) { + if (this._staged.get(key) === TOMBSTONE) return false; // already deleted in this batch + } else if ((await this._trie.get(this._root, kh)) === null) { + return false; + } + this._staged.set(key, TOMBSTONE); + return true; + } + + /** Apply the staged changes on top of `base`; return the new root. */ + async _buildRoot(base) { + const order = [...this._staged.keys()].map((k) => [utf8Hex(k), k]) + .sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0)); + const writes = order.filter(([, k]) => this._staged.get(k) !== TOMBSTONE); + const refs = await putBlobs(this._blobs, writes.map(([, k]) => encodeValue(this._staged.get(k)))); + const vref = new Map(writes.map(([kh], i) => [kh, refs[i]])); + try { + let root = base; + for (const [kh, k] of order) { + if (this._staged.get(k) === TOMBSTONE) { + try { + root = await this._trie.delete(root, kh); + } catch (e) { + if (!(e instanceof KeyMissing)) throw e; // deleted a key that never existed + } + } else { + root = await this._trie.insert(root, kh, vref.get(kh)); + } + } + return await this._trie.flush(root); + } finally { + this._trie.reset(); + } + } + + /** + * Write the staged changes and return the new root; then move the pointer, + * if there is one. With `reconcile`, a pointer that moved since this store + * read it is three-way merged into instead of overwritten (`resolver` + * settles conflicts), retrying up to `retries` times. + */ + async commit({ reconcile = false, resolver = null, retries = 5 } = {}) { + if (this._readonly) throw new TypeError('read-only snapshot'); + const base = this._root; + let root = await this._buildRoot(base); + if (this._pointer) { + if (reconcile) root = await this._reconcile(base, root, resolver, retries); + else await this._pointer.set(root); + } + this._staged.clear(); + this._root = root; + return root; + } + + async _reconcile(base, root, resolver, retries) { + const pointer = this._pointer; + const cas = typeof pointer.compareAndSet === 'function' ? pointer.compareAndSet.bind(pointer) : null; + let expected = base; + for (let i = 0; i < Math.max(1, retries); i++) { + const current = await pointer.get(); + if (current === expected) { + if (!cas) { + await pointer.set(root); // best effort: no compare-and-set + return root; + } + if (await cas(expected, root)) return root; + continue; // lost the race: read again + } + root = await RecordStore.merge(this._blobs, expected, root, current, resolver); + expected = current; + } + throw new Error(`commit could not reconcile the pointer after ${retries} tries`); + } + + // -- merge ---------------------------------------------------------------- + + /** + * Three-way merge of two roots that diverged from `base`; resolves to the + * merged root. A change on one side is taken, the same change on both + * taken once, different changes to one key are a conflict, settled by + * `resolver(key, base, ours, theirs)` (values or ABSENT; return a value or + * DELETE; may be async) or else raised as MergeConflict. + */ + static async merge(blobs, base, ours, theirs, resolver = null) { + if (ours === theirs) return ours; + if (ours === base) return theirs; + if (theirs === base) return ours; + + const trie = new Trie(blobs); + const ourDiff = new Map(); + for await (const [k, bv, sv] of trie.diff(base, ours)) ourDiff.set(k, [bv, sv]); + const theirDiff = new Map(); + for await (const [k, bv, sv] of trie.diff(base, theirs)) theirDiff.set(k, [bv, sv]); + + const changes = new Map(); // key hex -> value ref | TOMBSTONE + const conflicts = []; + for (const k of new Set([...ourDiff.keys(), ...theirDiff.keys()])) { + let bv; + let merged; + if (ourDiff.has(k) && theirDiff.has(k)) { + [bv, merged] = ourDiff.get(k); + if (merged !== theirDiff.get(k)[1]) { + conflicts.push(k); + continue; + } + } else if (ourDiff.has(k)) { + [bv, merged] = ourDiff.get(k); + } else { + [bv, merged] = theirDiff.get(k); + } + if (merged !== bv) changes.set(k, merged !== null ? merged : TOMBSTONE); + } + + if (conflicts.length && !resolver) throw new MergeConflict(conflicts.map(hexUtf8)); + if (conflicts.length) { + const resolved = await RecordStore._resolve(blobs, conflicts.sort(), ourDiff, theirDiff, resolver); + for (const [k, c] of resolved) changes.set(k, c); + } + + try { + let root = base; + for (const k of [...changes.keys()].sort()) { + const c = changes.get(k); + if (c === TOMBSTONE) { + try { + root = await trie.delete(root, k); + } catch (e) { + if (!(e instanceof KeyMissing)) throw e; + } + } else { + root = await trie.insert(root, k, c); + } + } + return await trie.flush(root); + } finally { + trie.reset(); + } + } + + static async _resolve(blobs, conflicts, ourDiff, theirDiff, resolver) { + const trio = new Map(conflicts.map((k) => [k, [ourDiff.get(k)[0], ourDiff.get(k)[1], theirDiff.get(k)[1]]])); + const refs = [...new Set([...trio.values()].flat().filter((r) => r !== null))]; + const values = await fetchBlobs(blobs, refs); + const decode = (r) => (r === null ? ABSENT : decodeValue(values.get(r))); + const kept = new Map(); + for (const k of conflicts) { + const [bv, ov, tv] = trio.get(k); + kept.set(k, await resolver(hexUtf8(k), decode(bv), decode(ov), decode(tv))); + } + const writes = conflicts.filter((k) => kept.get(k) !== DELETE); + const newRefs = await putBlobs(blobs, writes.map((k) => encodeValue(kept.get(k)))); + const refOf = new Map(writes.map((k, i) => [k, newRefs[i]])); + const changes = new Map(); + for (const k of conflicts) { + const merged = refOf.get(k) ?? null; // null: the resolver deleted it + if (merged !== trio.get(k)[0]) changes.set(k, merged !== null ? merged : TOMBSTONE); + } + return changes; + } +} + +/** + * A "latest root" held in memory, with compare-and-set: what several + * writers in one process can share, and the test double for a feed. + */ +export class MemoryPointer { + constructor(root = null) { + this.root = root; + } + + async get() { + return this.root; + } + + async set(root) { + this.root = root; + } + + async compareAndSet(expected, root) { + if (this.root !== expected) return false; + this.root = root; + return true; + } +} diff --git a/js/src/stores.js b/js/src/stores.js new file mode 100644 index 0000000..ea0df79 --- /dev/null +++ b/js/src/stores.js @@ -0,0 +1,213 @@ +// Bytes stores: where blobs live. The interface, as in Python: +// +// put(data: Uint8Array) -> Promise +// get(reference) -> Promise +// +// and optionally `putMany(datas)` / `getMany(refs)` (a Map) for batched or +// parallel I/O, which recordstore uses when present. + +import { redundancyFor, resolveAddressing, splitData, swarmRef } from './address.js'; +import { fromBase64 } from './bytes.js'; + +/** A reference the store does not hold. */ +export class BlobNotFoundError extends Error { + constructor(ref) { + super(`reference not found: ${ref}`); + this.name = 'BlobNotFoundError'; + this.ref = ref; + } +} + +/** Bytes that do not hash to the reference they were fetched or stored under. */ +export class VerificationError extends Error { + constructor(message) { + super(message); + this.name = 'VerificationError'; + } +} + +/** Run `fn` over `items` with at most `limit` in flight; results in order. */ +async function pool(items, limit, fn) { + const out = new Array(items.length); + let next = 0; + const worker = async () => { + while (next < items.length) { + const i = next++; + out[i] = await fn(items[i], i); + } + }; + await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker)); + return out; +} + +/** + * In-memory content-addressed store, the test double for Swarm. Its + * references are SHA-256 by default, as Python's MemoryBytesStore; with + * `addressing: 'swarm'` they are the references Swarm would give. + */ +export class MemoryBytesStore { + /** @param {{addressing?: 'sha256' | 'swarm' | ((data: Uint8Array) => string | Promise)}} [options] */ + constructor({ addressing = 'sha256' } = {}) { + this.addressing = addressing; + this._address = resolveAddressing(addressing); + /** @type {Map} */ + this.blobs = new Map(); + } + + async put(data) { + const ref = await this._address(data); + this.blobs.set(ref, Uint8Array.from(data)); + return ref; + } + + async get(ref) { + const data = this.blobs.get(ref); + if (data === undefined) throw new BlobNotFoundError(ref); + return Uint8Array.from(data); + } + + async getMany(refs) { + const out = new Map(); + for (const ref of refs) out.set(ref, await this.get(ref)); + return out; + } + + async putMany(datas) { + const refs = []; + for (const data of datas) refs.push(await this.put(data)); + return refs; + } + + get size() { + return this.blobs.size; + } +} + +/** + * Blobs on a Bee node (or any server with Bee's `/bytes` API, such as Ant), + * the counterpart of Python's BeeBytesStore. Reads need no postage batch; + * writes do. Every blob read is checked against its reference by recomputing + * the Swarm address, and every write against the address the node returns. + * That holds for blobs uploaded plainly, as recordstore uploads them (Python + * and JS both); pass `verify: false` for a store written with erasure coding. + */ +export class BeeBytesStore { + /** + * @param {string} url the node's API, e.g. http://localhost:1633 + * @param {{batch?: string, verify?: boolean, deferred?: boolean, + * concurrency?: number, fetch?: typeof fetch}} [options] + */ + constructor(url, { batch = null, verify = true, deferred = true, concurrency = 8, fetch: fetchFn } = {}) { + this.url = url.replace(/\/+$/, ''); + this.batch = batch; + this.verify = verify; + this.deferred = deferred; + this.concurrency = concurrency; + this._fetch = fetchFn || globalThis.fetch.bind(globalThis); + this.addressing = 'swarm'; + } + + async get(ref) { + const res = await this._fetch(`${this.url}/bytes/${ref}`); + if (res.status === 404) throw new BlobNotFoundError(ref); + if (!res.ok) throw new Error(`GET /bytes/${ref}: HTTP ${res.status}`); + const data = new Uint8Array(await res.arrayBuffer()); + if (this.verify && swarmRef(data) !== ref) { + throw new VerificationError(`the node returned bytes that do not hash to ${ref}`); + } + return data; + } + + async put(data) { + if (!this.batch) throw new Error('writing to Bee needs a postage batch (the `batch` option)'); + const res = await this._fetch(`${this.url}/bytes`, { + method: 'POST', + headers: { + 'content-type': 'application/octet-stream', + 'swarm-postage-batch-id': this.batch, + 'swarm-redundancy-level': String(redundancyFor(data.length)), + 'swarm-deferred-upload': String(this.deferred), + }, + body: data, + }); + if (!res.ok) throw new Error(`POST /bytes: HTTP ${res.status} ${await res.text().catch(() => '')}`); + const { reference } = await res.json(); + if (this.verify && reference !== swarmRef(data)) { + throw new VerificationError(`the node stored the blob under ${reference}, not its plain Swarm address`); + } + return reference; + } + + async getMany(refs) { + const datas = await pool(refs, this.concurrency, (ref) => this.get(ref)); + return new Map(refs.map((ref, i) => [ref, datas[i]])); + } + + async putMany(datas) { + return pool(datas, this.concurrency, (data) => this.put(data)); + } +} + +/** + * Blobs through a browser's `window.swarm` provider (Freedom Browser), where + * a page cannot reach a node's HTTP API. Reads use `swarm_readData` (the node + * joins the blob) and are checked against the reference here as well. Writes + * split the blob into chunks as Bee does and publish each with + * `swarm_publishChunk`, so references match Bee's and Python's; they need + * the page to be connected (`swarm_requestAccess`), which the first write + * asks for. Untested in Freedom so far. + */ +export class SwarmProviderBytesStore { + /** @param {{request: (args: {method: string, params?: object}) => Promise}} [provider] */ + constructor(provider = globalThis.swarm, { verify = true, concurrency = 4 } = {}) { + if (!provider || typeof provider.request !== 'function') { + throw new TypeError('no window.swarm provider (open the page in Freedom Browser)'); + } + this.provider = provider; + this.verify = verify; + this.concurrency = concurrency; + this.addressing = 'swarm'; + this._access = null; + } + + async get(ref) { + let result; + try { + result = await this.provider.request({ method: 'swarm_readData', params: { reference: ref } }); + } catch (e) { + if (e && e.data && e.data.reason === 'chunk_not_found') throw new BlobNotFoundError(ref); + throw e; + } + const data = fromBase64(result.data); + if (this.verify && swarmRef(data) !== ref) { + throw new VerificationError(`the provider returned bytes that do not hash to ${ref}`); + } + return data; + } + + async put(data) { + if (data.length === 0) throw new RangeError('the provider publishes chunks of 1 to 4096 bytes; an empty blob cannot be written'); + this._access ??= this.provider.request({ method: 'swarm_requestAccess' }); + await this._access; + const { root, chunks } = splitData(data); + await pool(chunks, this.concurrency, async ({ address, payload, span }) => { + const { reference } = await this.provider.request({ + method: 'swarm_publishChunk', + params: span === payload.length ? { data: payload } : { data: payload, span }, + }); + if (reference !== address) { + throw new VerificationError(`the provider published a chunk as ${reference}, expected ${address}`); + } + }); + return root; + } + + async getMany(refs) { + const datas = await pool(refs, this.concurrency, (ref) => this.get(ref)); + return new Map(refs.map((ref, i) => [ref, datas[i]])); + } + + async putMany(datas) { + return pool(datas, this.concurrency, (data) => this.put(data)); + } +} diff --git a/js/src/trie.js b/js/src/trie.js new file mode 100644 index 0000000..9d9d3dd --- /dev/null +++ b/js/src/trie.js @@ -0,0 +1,527 @@ +// The canonical persistent radix trie, ported from Python's `_Trie`. +// +// Each node is one blob, the canonical JSON of +// {"c": {: , ...}, "p": , +// "tn": 1, "v": } +// A child's prefix excludes the byte its parent branched on. Two invariants +// make the trie's shape a pure function of its contents (hence equal content +// => equal root): a node with no value and no children does not exist, and a +// node with no value and one child is merged into that child. +// +// Keys are lowercase hex of their UTF-8 bytes throughout (see bytes.js). +// Operations take a root and return a new root; nodes built during a commit +// are kept as `pending:N` placeholders and written by `flush`, bottom-up, +// one batch per level, only those reachable from the final root. + +import { canonicalBytes } from './canonical.js'; +import { fromUtf8 } from './bytes.js'; + +/** Default bound on the decoded-node cache (nodes are immutable and re-fetchable). */ +export const DEFAULT_NODE_CACHE_SIZE = 65536; + +/** Most nodes a walk loads ahead of where it is. */ +const WALK_LOOKAHEAD = 4096; + +export class TrieNode { + /** + * @param {string} prefix hex + * @param {string | null} valueRef + * @param {Map} children first byte (2 hex) -> child ref + */ + constructor(prefix, valueRef, children) { + this.prefix = prefix; + this.valueRef = valueRef; + this.children = children; + } +} + +/** Raised by `Trie.delete` for a key the trie does not hold. */ +export class KeyMissing extends Error { + constructor(key) { + super(`key not in trie: ${key}`); + this.name = 'KeyMissing'; + } +} + +/** Bounded LRU over decoded nodes (Map iteration order is insertion order). */ +class NodeCache { + constructor(maxsize) { + this.maxsize = Math.max(1, maxsize); + this.map = new Map(); + } + + get(ref) { + const node = this.map.get(ref); + if (node !== undefined) { + this.map.delete(ref); + this.map.set(ref, node); + } + return node; + } + + has(ref) { + return this.map.has(ref); + } + + set(ref, node) { + this.map.delete(ref); + this.map.set(ref, node); + while (this.map.size > this.maxsize) this.map.delete(this.map.keys().next().value); + } +} + +/** Longest common prefix of two hex strings, on a byte boundary. */ +function commonPrefix(a, b) { + const n = Math.min(a.length, b.length); + let i = 0; + while (i < n && a[i] === b[i]) i++; + return a.slice(0, i - (i % 2)); +} + +/** A hex string's prefixes agree up to the shorter of the two. */ +function canMeet(full, prefix) { + const probe = Math.min(full.length, prefix.length); + return full.slice(0, probe) === prefix.slice(0, probe); +} + +const sortedBytes = (children) => [...children.keys()].sort(); + +export class Trie { + /** + * @param {{get: Function, put: Function, getMany?: Function, putMany?: Function}} blobs + * @param {number} [cacheSize] + */ + constructor(blobs, cacheSize = DEFAULT_NODE_CACHE_SIZE) { + this.blobs = blobs; + this.cache = new NodeCache(cacheSize); + /** @type {Map} */ + this.pending = new Map(); + this.pn = 0; + } + + // -- node io -------------------------------------------------------------- + + /** Decode a node blob. */ + static decode(bytes) { + const obj = JSON.parse(fromUtf8(bytes)); + if (obj === null || typeof obj !== 'object' || obj.tn !== 1) { + throw new Error('not a trie node or unsupported version'); + } + const children = new Map(); + for (const [k, ref] of Object.entries(obj.c)) { + const b = parseInt(k, 16); + if (!(b >= 0 && b <= 255)) throw new Error(`bad child key in trie node: ${k}`); + children.set(b.toString(16).padStart(2, '0'), ref); + } + return new TrieNode(obj.p.toLowerCase(), obj.v, children); + } + + /** The canonical bytes of a node. */ + static serialize(node) { + return canonicalBytes({ + tn: 1, + p: node.prefix, + v: node.valueRef, + c: Object.fromEntries(node.children), + }); + } + + /** At hand without a fetch: a placeholder of the commit being built, or cached. */ + has(ref) { + return this.pending.has(ref) || this.cache.has(ref); + } + + /** A node if at hand, `undefined` if not; a `null` side stays `null`. */ + peek(side) { + if (typeof side !== 'string') return side; + return this.pending.get(side) || this.cache.get(side); + } + + async load(ref) { + const pending = this.pending.get(ref); + if (pending) return pending; + let node = this.cache.get(ref); + if (!node) { + node = Trie.decode(await this.blobs.get(ref)); + this.cache.set(ref, node); + } + return node; + } + + /** Load several nodes, fetching the uncached ones in one batch. */ + async loadMany(refs) { + const out = new Map(); + const unique = [...new Set(refs)]; + for (const r of unique) { + const node = this.pending.get(r) || this.cache.get(r); + if (node) out.set(r, node); + } + const missing = unique.filter((r) => !out.has(r)); + if (missing.length) { + let blobs; + if (typeof this.blobs.getMany === 'function') { + blobs = await this.blobs.getMany(missing); + } else { + blobs = new Map(); + for (const r of missing) blobs.set(r, await this.blobs.get(r)); + } + for (const r of missing) { + const node = Trie.decode(blobs.get(r)); + this.cache.set(r, node); + out.set(r, node); // held here even if the LRU drops it + } + } + return out; + } + + /** Defer a node: a placeholder, resolved to its real reference by `flush`. */ + store(node) { + const pid = `pending:${this.pn++}`; + this.pending.set(pid, node); + return pid; + } + + /** + * Write the placeholders reachable from `root`, bottom-up, one batch per + * level, and return the real root reference. Orphaned intermediates are + * never written. + */ + async flush(root) { + if (root === null || !this.pending.has(root)) return root; + const reachable = new Set(); + const stack = [root]; + while (stack.length) { + const pid = stack.pop(); + if (reachable.has(pid)) continue; + reachable.add(pid); + for (const c of this.pending.get(pid).children.values()) if (this.pending.has(c)) stack.push(c); + } + const resolved = new Map(); + const remaining = new Set(reachable); + while (!resolved.has(root)) { + const ready = [...remaining].filter((pid) => + [...this.pending.get(pid).children.values()].every((c) => !this.pending.has(c) || resolved.has(c))); + if (!ready.length) throw new Error('trie flush stalled: no writable nodes'); + const batch = ready.map((pid) => { + const node = this.pending.get(pid); + const children = new Map([...node.children].map(([b, c]) => [b, resolved.get(c) ?? c])); + const real = new TrieNode(node.prefix, node.valueRef, children); + return [pid, real, Trie.serialize(real)]; + }); + const datas = batch.map((entry) => entry[2]); + let refs; + if (typeof this.blobs.putMany === 'function') { + refs = await this.blobs.putMany(datas); + } else { + refs = []; + for (const d of datas) refs.push(await this.blobs.put(d)); + } + batch.forEach(([pid, real], i) => { + resolved.set(pid, refs[i]); + this.cache.set(refs[i], real); + remaining.delete(pid); + }); + } + return resolved.get(root); + } + + /** Drop the placeholders of a finished (or failed) build. */ + reset() { + this.pending.clear(); + this.pn = 0; + } + + // -- reads ---------------------------------------------------------------- + + /** The value reference under `key` (hex), or null. */ + async get(root, key) { + while (root !== null) { + const node = await this.load(root); + if (!key.startsWith(node.prefix)) return null; + key = key.slice(node.prefix.length); + if (key === '') return node.valueRef; + root = node.children.get(key.slice(0, 2)) ?? null; + key = key.slice(2); + } + return null; + } + + /** + * `get` for many keys at once, level by level, so a network store pays + * about one round trip per trie level rather than one per node. + * @returns {Promise>} key hex -> value ref or null + */ + async getMany(root, keys) { + const found = new Map(); + const unique = [...new Set(keys)]; + if (root === null) { + for (const k of unique) found.set(k, null); + return found; + } + let walking = unique.map((k) => [k, k, root]); + while (walking.length) { + const nodes = await this.loadMany(walking.map((w) => w[2])); + const descending = []; + for (const [key, rest0, ref] of walking) { + const node = nodes.get(ref); + if (!rest0.startsWith(node.prefix)) { + found.set(key, null); + continue; + } + const rest = rest0.slice(node.prefix.length); + if (rest === '') { + found.set(key, node.valueRef); + continue; + } + const child = node.children.get(rest.slice(0, 2)); + if (child === undefined) found.set(key, null); + else descending.push([key, rest.slice(2), child]); + } + walking = descending; + } + return found; + } + + // -- writes (functional: a root in, a new root out) ------------------------- + + async insert(root, key, valueRef) { + if (root === null) return this.store(new TrieNode(key, valueRef, new Map())); + const node = await this.load(root); + const common = commonPrefix(node.prefix, key); + + if (common.length < node.prefix.length) { + // split: demote the existing node under the diverging byte + const demoted = new TrieNode(node.prefix.slice(common.length + 2), node.valueRef, new Map(node.children)); + const children = new Map([[node.prefix.slice(common.length, common.length + 2), this.store(demoted)]]); + const rest = key.slice(common.length); + if (rest === '') return this.store(new TrieNode(common, valueRef, children)); + children.set(rest.slice(0, 2), this.store(new TrieNode(rest.slice(2), valueRef, new Map()))); + return this.store(new TrieNode(common, null, children)); + } + + const rest = key.slice(node.prefix.length); + if (rest === '') return this.store(new TrieNode(node.prefix, valueRef, new Map(node.children))); + const children = new Map(node.children); + const childRef = children.get(rest.slice(0, 2)); + if (childRef === undefined) { + children.set(rest.slice(0, 2), this.store(new TrieNode(rest.slice(2), valueRef, new Map()))); + } else { + children.set(rest.slice(0, 2), await this.insert(childRef, rest.slice(2), valueRef)); + } + return this.store(new TrieNode(node.prefix, node.valueRef, children)); + } + + async delete(root, key) { + if (root === null) throw new KeyMissing(key); + const node = await this.load(root); + if (!key.startsWith(node.prefix)) throw new KeyMissing(key); + const rest = key.slice(node.prefix.length); + + if (rest === '') { + if (node.valueRef === null) throw new KeyMissing(key); + return this.canonicalize(node.prefix, null, new Map(node.children)); + } + + const childRef = node.children.get(rest.slice(0, 2)); + if (childRef === undefined) throw new KeyMissing(key); + const newChild = await this.delete(childRef, rest.slice(2)); + const children = new Map(node.children); + if (newChild === null) children.delete(rest.slice(0, 2)); + else children.set(rest.slice(0, 2), newChild); + return this.canonicalize(node.prefix, node.valueRef, children); + } + + /** Restore the canonical-form invariants after a removal. */ + async canonicalize(prefix, valueRef, children) { + if (valueRef === null && children.size === 0) return null; + if (valueRef === null && children.size === 1) { + const [[byte, childRef]] = children; + const child = await this.load(childRef); + return this.store(new TrieNode(prefix + byte + child.prefix, child.valueRef, new Map(child.children))); + } + return this.store(new TrieNode(prefix, valueRef, children)); + } + + // -- walks ------------------------------------------------------------------ + + /** + * Load what a depth-first walk will visit next, level by level: the + * unloaded refs on its stack (nearest first) and the children of those + * already loaded, skipping subtrees that cannot hold `prefix`. + */ + async prefetch(stack, prefix) { + const budget = Math.max(1, Math.min(WALK_LOOKAHEAD, Math.floor(this.cache.maxsize / 2))); + let frontier = [...stack].reverse(); + let fetched = 0; + let visited = 0; + while (frontier.length && fetched < budget && visited < 2 * budget) { + const missing = []; + for (const [ref] of frontier) if (!this.has(ref) && missing.length < budget - fetched) missing.push(ref); + if (missing.length) { + await this.loadMany(missing); + fetched += missing.length; + } + const next = []; + for (const [ref, acc] of frontier) { + const node = this.peek(ref); + if (!node) continue; // beyond this round's budget + visited++; + const full = acc + node.prefix; + if (!canMeet(full, prefix)) continue; + for (const byte of sortedBytes(node.children)) next.push([node.children.get(byte), full + byte]); + } + frontier = next; + } + } + + /** All [key hex, value ref] with key under `prefix` (hex), in sorted key order. */ + async *items(root, prefix = '') { + if (root === null) return; + const stack = [[root, '']]; + while (stack.length) { + if (!this.has(stack[stack.length - 1][0])) await this.prefetch(stack, prefix); + const [ref, acc] = stack.pop(); + const node = await this.load(ref); + const full = acc + node.prefix; + if (!canMeet(full, prefix)) continue; + if (node.valueRef !== null && full.startsWith(prefix)) yield [full, node.valueRef]; + for (const byte of sortedBytes(node.children).reverse()) stack.push([node.children.get(byte), full + byte]); + } + } + + /** + * ['node', ref] and ['value', ref] for every blob needed to read the keys + * under `prefix`: the nodes on the path and in the subtree, and the values. + */ + async *refsUnder(root, prefix = '') { + if (root === null) return; + const stack = [[root, '']]; + while (stack.length) { + if (!this.has(stack[stack.length - 1][0])) await this.prefetch(stack, prefix); + const [ref, acc] = stack.pop(); + const node = await this.load(ref); + const full = acc + node.prefix; + if (!canMeet(full, prefix)) continue; + yield ['node', ref]; + if (node.valueRef !== null && full.startsWith(prefix)) yield ['value', node.valueRef]; + for (const byte of sortedBytes(node.children)) stack.push([node.children.get(byte), full + byte]); + } + } + + // -- diff (structural, prunes shared subtrees) ------------------------------ + + /** + * [key hex, a value ref | null, b value ref | null] for every key where the + * two roots differ. Subtrees with equal refs are pruned, so the cost follows + * the difference, not the dataset. + */ + async *diff(aRoot, bRoot) { + if (aRoot === bRoot) return; + const stack = [[aRoot, bRoot, '']]; + while (stack.length) { + if (this.unloaded(stack[stack.length - 1])) await this.prefetchPairs(stack); + const [a, b, acc] = stack.pop(); + const { values, pairs } = Trie.diffStep(await this.asNode(a), await this.asNode(b), acc); + yield* values; + stack.push(...pairs); + } + } + + /** A pair's side is a ref, a split node (not itself stored), or null. */ + async asNode(side) { + return typeof side === 'string' ? this.load(side) : side; + } + + unloaded(pair) { + return [pair[0], pair[1]].some((x) => typeof x === 'string' && !this.has(x)); + } + + /** + * One pair of subtrees: the differing values at their tops, and the pairs + * still to compare. + */ + static diffStep(a, b, acc) { + const values = []; + const pairs = []; + if (a === null || b === null) { + const node = b === null ? a : b; + if (node === null) return { values, pairs }; + const full = acc + node.prefix; + if (node.valueRef !== null) values.push(b === null ? [full, node.valueRef, null] : [full, null, node.valueRef]); + for (const [byte, cref] of node.children) { + pairs.push(b === null ? [cref, null, full + byte] : [null, cref, full + byte]); + } + return { values, pairs }; + } + + const pa = a.prefix; + const pb = b.prefix; + if (pa === pb) { + const ka = acc + pa; + if (a.valueRef !== b.valueRef) values.push([ka, a.valueRef, b.valueRef]); + for (const byte of new Set([...a.children.keys(), ...b.children.keys()])) { + const ca = a.children.get(byte) ?? null; + const cb = b.children.get(byte) ?? null; + if (ca !== cb) pairs.push([ca, cb, ka + byte]); // equal refs: a shared subtree + } + return { values, pairs }; + } + + const common = commonPrefix(pa, pb); + if (common.length < pa.length && common.length < pb.length) { + // prefixes diverge: the two subtrees cover disjoint keys + pairs.push([a, null, acc]); + pairs.push([null, b, acc]); + } else if (common.length === pa.length) { + // a's key is a proper prefix of b's: b lives under one of a's branches + const ka = acc + pa; + const bb = pb.slice(pa.length, pa.length + 2); + const bSplit = new TrieNode(pb.slice(pa.length + 2), b.valueRef, b.children); + if (a.valueRef !== null) values.push([ka, a.valueRef, null]); + for (const [byte, cref] of a.children) pairs.push([cref, byte === bb ? bSplit : null, ka + byte]); + if (!a.children.has(bb)) pairs.push([null, bSplit, ka + bb]); + } else { + // b's key is a proper prefix of a's + const kb = acc + pb; + const ab = pa.slice(pb.length, pb.length + 2); + const aSplit = new TrieNode(pa.slice(pb.length + 2), a.valueRef, a.children); + if (b.valueRef !== null) values.push([kb, null, b.valueRef]); + for (const [byte, cref] of b.children) pairs.push([byte === ab ? aSplit : null, cref, kb + byte]); + if (!b.children.has(ab)) pairs.push([aSplit, null, kb + ab]); + } + return { values, pairs }; + } + + /** `prefetch` for a diff: load what the diff will visit, level by level. */ + async prefetchPairs(stack) { + const budget = Math.max(1, Math.min(WALK_LOOKAHEAD, Math.floor(this.cache.maxsize / 2))); + let frontier = [...stack].reverse(); + let fetched = 0; + let visited = 0; + while (frontier.length && fetched < budget && visited < 2 * budget) { + const missing = []; + const seen = new Set(); + for (const [a, b] of frontier) { + for (const side of [a, b]) { + if (typeof side === 'string' && !this.has(side) && !seen.has(side) && missing.length < budget - fetched) { + seen.add(side); + missing.push(side); + } + } + } + if (missing.length) { + await this.loadMany(missing); + fetched += missing.length; + } + const next = []; + for (const [a, b, acc] of frontier) { + const na = this.peek(a); + const nb = this.peek(b); + if (na === undefined || nb === undefined) continue; // beyond this round's budget + visited++; + next.push(...Trie.diffStep(na, nb, acc).pairs); + } + frontier = next; + } + } +} diff --git a/js/test/address.test.js b/js/test/address.test.js new file mode 100644 index 0000000..38217f6 --- /dev/null +++ b/js/test/address.test.js @@ -0,0 +1,33 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { keccak256Bytes, redundancyFor, sha256Ref, splitData, swarmRef } from '../src/address.js'; +import { toHex, utf8 } from '../src/bytes.js'; +import { fixture, pattern } from './helpers.js'; + +test('keccak256 is Ethereum keccak (the vendored js-sha3 sanity vector)', () => { + assert.equal(toHex(keccak256Bytes(utf8('hello'))), '1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8'); +}); + +test('Swarm references match swarmfs for every tree shape in the fixtures', () => { + for (const { size, ref } of fixture('swarm-addresses.json').cases) { + assert.equal(swarmRef(pattern(size)), ref, `${size} bytes`); + } +}); + +test('splitData lists every chunk, leaves first, intermediates with their spans', () => { + const data = pattern(4096 * 3 + 10); + const { root, chunks } = splitData(data); + assert.equal(chunks.length, 5); // four leaves and one intermediate + assert.equal(chunks.at(-1).address, root); + assert.equal(chunks.at(-1).span, data.length); + assert.equal(chunks.at(-1).payload.length, 4 * 32); +}); + +test('sha256 references are hex SHA-256, as Python MemoryBytesStore', async () => { + assert.equal(await sha256Ref(utf8('abc')), 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad'); +}); + +test('redundancy level: 1 while a blob fits a chunk, 0 above', () => { + assert.equal(redundancyFor(4096), 1); + assert.equal(redundancyFor(4097), 0); +}); diff --git a/js/test/api.test.js b/js/test/api.test.js new file mode 100644 index 0000000..1c75562 --- /dev/null +++ b/js/test/api.test.js @@ -0,0 +1,135 @@ +// Behaviour of the JavaScript API itself. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { + ABSENT, BlobNotFoundError, MemoryBytesStore, MemoryPointer, RecordStore, RecordUnavailable, +} from '../src/index.js'; +import { collect } from './helpers.js'; + +test('reads see staged writes; commit makes them the root', async () => { + const store = new RecordStore(new MemoryBytesStore()); + store.put('users/alice', { name: 'Alice' }); + assert.deepEqual(await store.get('users/alice'), { name: 'Alice' }); + assert.equal(store.root, null); + const root = await store.commit(); + assert.match(root, /^[0-9a-f]{64}$/); + assert.equal(store.stagedCount, 0); + assert.deepEqual(await RecordStore.at(root, store.blobs).get('users/alice'), { name: 'Alice' }); +}); + +test('absent keys: get is undefined, has is false, delete is false; null is a value', async () => { + const store = new RecordStore(new MemoryBytesStore()); + store.put('n', null); + await store.commit(); + assert.equal(await store.get('n'), null); + assert.equal(await store.has('n'), true); + assert.equal(await store.get('missing'), undefined); + assert.equal(await store.has('missing'), false); + assert.equal(await store.delete('missing'), false); + assert.equal(store.stagedCount, 0); +}); + +test('records are copies: changing what get returned or what was put changes nothing stored', async () => { + const store = new RecordStore(new MemoryBytesStore()); + const value = { list: [1, 2] }; + store.put('k', value); + value.list.push(3); + const read = await store.get('k'); + read.list.push(4); + assert.deepEqual(await store.get('k'), { list: [1, 2] }); +}); + +test('equal content gives an equal root, whatever the history', async () => { + const a = new RecordStore(new MemoryBytesStore()); + for (const k of ['c', 'a', 'b']) a.put(k, k); + const b = new RecordStore(new MemoryBytesStore()); + b.put('a', 'a'); + b.put('x', 'temporary'); + await b.commit(); + b.put('b', 'b'); + b.put('c', 'c'); + await b.delete('x'); + assert.equal(await a.commit(), await b.commit()); +}); + +test('keys and items are sorted by code point, filtered by prefix, staged changes included', async () => { + const store = new RecordStore(new MemoryBytesStore()); + for (const k of ['b', '😀', '￿', 'a/2', 'a/1']) store.put(k, k); + await store.commit(); + store.put('a/0', 'staged'); + await store.delete('a/2'); + assert.deepEqual(await collect(store.keys()), ['a/0', 'a/1', 'b', '￿', '😀']); + assert.deepEqual(await collect(store.items('a/')), [['a/0', 'staged'], ['a/1', 'a/1']]); +}); + +test('getMany reads several records, leaving out absent keys', async () => { + const store = new RecordStore(new MemoryBytesStore()); + for (let i = 0; i < 50; i++) store.put(`k${i}`, i); + await store.commit(); + store.put('k1', 'staged'); + const got = await store.getMany(['k0', 'k1', 'k49', 'nope']); + assert.equal(got.size, 3); + assert.equal(got.get('k0'), 0); + assert.equal(got.get('k1'), 'staged'); + assert.equal(got.get('k49'), 49); +}); + +test('diff names what changed between two roots', async () => { + const store = new RecordStore(new MemoryBytesStore()); + for (let i = 0; i < 20; i++) store.put(`k${i}`, i); + const before = await store.commit(); + store.put('k3', 'changed'); + await store.delete('k4'); + store.put('new', true); + await store.commit(); + const changes = (await collect(RecordStore.at(before, store.blobs).diff(store.root))) + .sort((x, y) => (x[0] < y[0] ? -1 : 1)); + assert.deepEqual(changes, [['k3', 3, 'changed'], ['k4', 4, ABSENT], ['new', ABSENT, true]]); +}); + +test('a snapshot is read-only', async () => { + const snapshot = RecordStore.at(null, new MemoryBytesStore()); + assert.throws(() => snapshot.put('k', 1), TypeError); + await assert.rejects(snapshot.delete('k'), TypeError); +}); + +test('keys must be non-empty well-formed strings', () => { + const store = new RecordStore(new MemoryBytesStore()); + for (const bad of ['', '\ud800', 1, null]) assert.throws(() => store.put(bad, 1), TypeError); +}); + +test('a missing blob is reported as unavailable, never as absence', async () => { + const blobs = new MemoryBytesStore(); + const store = new RecordStore(blobs); + store.put('k', 'v'); + const root = await store.commit(); + const [[, valueRef]] = (await collect(store.refsUnder())).filter(([kind]) => kind === 'value'); + blobs.blobs.delete(valueRef); + await assert.rejects(RecordStore.at(root, blobs).get('k'), (e) => e instanceof RecordUnavailable && e.cause instanceof BlobNotFoundError); + blobs.blobs.clear(); + await assert.rejects(RecordStore.at(root, blobs).get('k'), RecordUnavailable); +}); + +test('two writers sharing a pointer converge with reconcile', async () => { + const blobs = new MemoryBytesStore(); + const pointer = new MemoryPointer(); + const a = await RecordStore.open(blobs, { pointer }); + const b = await RecordStore.open(blobs, { pointer }); + a.put('from/a', 1); + b.put('from/b', 2); + await a.commit({ reconcile: true }); + const root = await b.commit({ reconcile: true }); // the pointer moved under b: merge + assert.equal(await pointer.get(), root); + const merged = RecordStore.at(root, blobs); + assert.deepEqual(await collect(merged.items()), [['from/a', 1], ['from/b', 2]]); +}); + +test('commit with nothing staged returns the same root', async () => { + const store = new RecordStore(new MemoryBytesStore()); + store.put('k', 1); + const root = await store.commit(); + assert.equal(await store.commit(), root); + store.put('k', 1); // the same value again + assert.equal(await store.commit(), root); +}); diff --git a/js/test/canonical.test.js b/js/test/canonical.test.js new file mode 100644 index 0000000..76e60c2 --- /dev/null +++ b/js/test/canonical.test.js @@ -0,0 +1,53 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { canonicalBytes, canonicalJson, decodeValue, encodeValue, parseJson, pythonFloatRepr } from '../src/canonical.js'; +import { toHex } from '../src/bytes.js'; +import { fixture } from './helpers.js'; + +const canonical = fixture('canonical.json'); + +test('every value encodes to the bytes Python writes', () => { + for (const { value, hex } of canonical.values) { + assert.equal(toHex(canonicalBytes(value)), hex, `value ${canonicalJson(value)}`); + } +}); + +test('every record envelope encodes as Python writes it, and decodes back', () => { + for (const { value, hex } of canonical.records) { + assert.equal(toHex(encodeValue(value)), hex); + assert.deepEqual(decodeValue(encodeValue(value)), value); + } +}); + +test('large integers round-trip exactly through parseJson as BigInt', () => { + const big = parseJson('{"n": 9223372036854775808, "m": -1180591620717411303424, "s": 9007199254740991}'); + assert.equal(big.n, 9223372036854775808n); + assert.equal(big.m, -1180591620717411303424n); + assert.equal(big.s, 9007199254740991); + assert.equal(canonicalJson(big), '{"m":-1180591620717411303424,"n":9223372036854775808,"s":9007199254740991}'); +}); + +test('integral numbers below 2**53 are written as integers (the documented divergence from Python floats)', () => { + assert.equal(canonicalJson(1.0), '1'); + assert.equal(canonicalJson(-0), '0'); + assert.equal(canonicalJson(parseJson('1.0')), '1'); // Python wrote "1.0"; JavaScript cannot tell +}); + +test('other numbers follow Python float repr', () => { + const cases = [[0.1, '0.1'], [1e-5, '1e-05'], [1e-4, '0.0001'], [1e16, '1e+16'], [1e15 + 0.5, '1000000000000000.5'], + [2 ** 53, '9007199254740992.0'], [5e-324, '5e-324'], [-1e100, '-1e+100'], [1.5e-300, '1.5e-300'], [123.456, '123.456']]; + for (const [x, text] of cases) assert.equal(pythonFloatRepr(x), text); +}); + +test('object keys sort by code point, as Python sorts str', () => { + // UTF-16 order would put the emoji (a surrogate pair, 0xD83D...) before U+FFFF. + assert.equal(canonicalJson({ '😀': 1, '￿': 2, z: 3, é: 4 }), '{"z":3,"é":4,"￿":2,"😀":1}'); +}); + +test('what JSON cannot represent is refused, as in Python', () => { + for (const bad of [NaN, Infinity, -Infinity, undefined, () => 1, Symbol('s'), new Date(0), new Map(), + { a: undefined }, [undefined], '\ud800', { '\udc00': 1 }]) { + assert.throws(() => canonicalBytes(bad), `${String(bad)} should be refused`); + } + assert.throws(() => canonicalBytes([1, , 3])); // eslint-disable-line no-sparse-arrays +}); diff --git a/js/test/fixtures/canonical.json b/js/test/fixtures/canonical.json new file mode 100644 index 0000000..f83f736 --- /dev/null +++ b/js/test/fixtures/canonical.json @@ -0,0 +1,296 @@ +{ + "note": "Generated by js/tools/make_fixtures.py from the Python reference implementation; do not edit by hand.", + "values": [ + { + "value": null, + "hex": "6e756c6c" + }, + { + "value": true, + "hex": "74727565" + }, + { + "value": false, + "hex": "66616c7365" + }, + { + "value": "", + "hex": "2222" + }, + { + "value": "plain", + "hex": "22706c61696e22" + }, + { + "value": "quote \" and backslash \\", + "hex": "2271756f7465205c2220616e64206261636b736c617368205c5c22" + }, + { + "value": "tab\tnewline\ncr\r", + "hex": "227461625c746e65776c696e655c6e63725c7222" + }, + { + "value": "controls \u0000\u0001\u001f", + "hex": "22636f6e74726f6c73205c75303030305c75303030315c75303031667f22" + }, + { + "value": "\b\f", + "hex": "225c625c6622" + }, + { + "value": "é 中文 😀", + "hex": "22c3a920e4b8ade6968720f09f988022" + }, + { + "value": "

", + "hex": "22e280a8e280a922" + }, + { + "value": "￿", + "hex": "22efbfbf22" + }, + { + "value": "/", + "hex": "222f22" + }, + { + "value": "", + "hex": "223c2f7363726970743e22" + }, + { + "value": 0, + "hex": "30" + }, + { + "value": 1, + "hex": "31" + }, + { + "value": -1, + "hex": "2d31" + }, + { + "value": 42, + "hex": "3432" + }, + { + "value": 2147483648, + "hex": "32313437343833363438" + }, + { + "value": -2147483649, + "hex": "2d32313437343833363439" + }, + { + "value": 9007199254740991, + "hex": "39303037313939323534373430393931" + }, + { + "value": -9007199254740991, + "hex": "2d39303037313939323534373430393931" + }, + { + "value": 9007199254740992, + "hex": "39303037313939323534373430393932" + }, + { + "value": 9223372036854775808, + "hex": "39323233333732303336383534373735383038" + }, + { + "value": -1180591620717411303424, + "hex": "2d31313830353931363230373137343131333033343234" + }, + { + "value": 1000000000000000000000000000000, + "hex": "31303030303030303030303030303030303030303030303030303030303030" + }, + { + "value": 0.1, + "hex": "302e31" + }, + { + "value": -2.5, + "hex": "2d322e35" + }, + { + "value": 0.30000000000000004, + "hex": "302e3330303030303030303030303030303034" + }, + { + "value": 1.0000000000000002, + "hex": "312e30303030303030303030303030303032" + }, + { + "value": 123.456, + "hex": "3132332e343536" + }, + { + "value": 0.0001, + "hex": "302e30303031" + }, + { + "value": 0.0001234, + "hex": "302e30303031323334" + }, + { + "value": 1e-05, + "hex": "31652d3035" + }, + { + "value": 1e-07, + "hex": "31652d3037" + }, + { + "value": 2.5e-05, + "hex": "322e35652d3035" + }, + { + "value": 1.5e-300, + "hex": "312e35652d333030" + }, + { + "value": 5e-324, + "hex": "35652d333234" + }, + { + "value": 1000000000000000.5, + "hex": "313030303030303030303030303030302e35" + }, + { + "value": 1e+16, + "hex": "31652b3136" + }, + { + "value": 1e+21, + "hex": "31652b3231" + }, + { + "value": 1e+22, + "hex": "31652b3232" + }, + { + "value": 1.7976931348623157e+308, + "hex": "312e37393736393331333438363233313537652b333038" + }, + { + "value": 9007199254740992.0, + "hex": "393030373139393235343734303939322e30" + }, + { + "value": -1e+100, + "hex": "2d31652b313030" + }, + { + "value": 3.14159265358979, + "hex": "332e3134313539323635333538393739" + }, + { + "value": 100.5, + "hex": "3130302e35" + }, + { + "value": {}, + "hex": "7b7d" + }, + { + "value": [], + "hex": "5b5d" + }, + { + "value": { + "b": 1, + "a": 2 + }, + "hex": "7b2261223a322c2262223a317d" + }, + { + "value": { + "z": 1, + "é": 2, + "😀": 3, + "￿": 4, + "A": 5, + "a": 6, + "": 7, + "aa": 8 + }, + "hex": "7b22223a372c2241223a352c2261223a362c226161223a382c227a223a312c22c3a9223a322c22efbfbf223a342c22f09f9880223a337d" + }, + { + "value": { + "nested": { + "list": [ + 1, + "two", + null, + true, + false, + { + "x": [] + } + ], + "empty": {} + } + }, + "hex": "7b226e6573746564223a7b22656d707479223a7b7d2c226c697374223a5b312c2274776f222c6e756c6c2c747275652c66616c73652c7b2278223a5b5d7d5d7d7d" + }, + { + "value": [ + null, + true, + false, + 0, + "", + [], + {} + ], + "hex": "5b6e756c6c2c747275652c66616c73652c302c22222c5b5d2c7b7d5d" + }, + { + "value": { + "users": [ + { + "name": "Alice", + "role": "admin" + }, + { + "name": "Bob", + "tags": [ + "x", + "y" + ] + } + ] + }, + "hex": "7b227573657273223a5b7b226e616d65223a22416c696365222c22726f6c65223a2261646d696e227d2c7b226e616d65223a22426f62222c2274616773223a5b2278222c2279225d7d5d7d" + } + ], + "records": [ + { + "value": null, + "hex": "7b22727376223a312c2276616c223a6e756c6c7d" + }, + { + "value": "x", + "hex": "7b22727376223a312c2276616c223a2278227d" + }, + { + "value": 1, + "hex": "7b22727376223a312c2276616c223a317d" + }, + { + "value": 0.5, + "hex": "7b22727376223a312c2276616c223a302e357d" + }, + { + "value": { + "a": [ + 1, + 2 + ] + }, + "hex": "7b22727376223a312c2276616c223a7b2261223a5b312c325d7d7d" + } + ] +} diff --git a/js/test/fixtures/merges.json b/js/test/fixtures/merges.json new file mode 100644 index 0000000..73a9b1b --- /dev/null +++ b/js/test/fixtures/merges.json @@ -0,0 +1,1190 @@ +{ + "note": "Generated by js/tools/make_fixtures.py from the Python reference implementation; do not edit by hand.", + "resolver": "ours wins: DELETE if ours is ABSENT, else ours", + "cases": [ + { + "name": "disjoint/sha256", + "addressing": "sha256", + "base": [ + { + "op": "put", + "key": "k0", + "value": { + "n": 0 + } + }, + { + "op": "put", + "key": "k1", + "value": { + "n": 1 + } + }, + { + "op": "put", + "key": "k2", + "value": { + "n": 2 + } + }, + { + "op": "put", + "key": "k3", + "value": { + "n": 3 + } + }, + { + "op": "put", + "key": "k4", + "value": { + "n": 4 + } + }, + { + "op": "put", + "key": "k5", + "value": { + "n": 5 + } + }, + { + "op": "put", + "key": "k6", + "value": { + "n": 6 + } + }, + { + "op": "put", + "key": "k7", + "value": { + "n": 7 + } + }, + { + "op": "put", + "key": "k8", + "value": { + "n": 8 + } + }, + { + "op": "put", + "key": "k9", + "value": { + "n": 9 + } + }, + { + "op": "put", + "key": "k10", + "value": { + "n": 10 + } + }, + { + "op": "put", + "key": "k11", + "value": { + "n": 11 + } + }, + { + "op": "put", + "key": "k12", + "value": { + "n": 12 + } + }, + { + "op": "put", + "key": "k13", + "value": { + "n": 13 + } + }, + { + "op": "put", + "key": "k14", + "value": { + "n": 14 + } + }, + { + "op": "put", + "key": "k15", + "value": { + "n": 15 + } + }, + { + "op": "put", + "key": "k16", + "value": { + "n": 16 + } + }, + { + "op": "put", + "key": "k17", + "value": { + "n": 17 + } + }, + { + "op": "put", + "key": "k18", + "value": { + "n": 18 + } + }, + { + "op": "put", + "key": "k19", + "value": { + "n": 19 + } + }, + { + "op": "put", + "key": "shared/a", + "value": 1 + }, + { + "op": "put", + "key": "shared/b", + "value": 2 + }, + { + "op": "commit" + } + ], + "ours": [ + { + "op": "put", + "key": "k1", + "value": "ours" + }, + { + "op": "delete", + "key": "k2" + }, + { + "op": "put", + "key": "new/ours", + "value": 1 + } + ], + "theirs": [ + { + "op": "put", + "key": "k3", + "value": "theirs" + }, + { + "op": "delete", + "key": "k4" + }, + { + "op": "put", + "key": "new/theirs", + "value": 2 + } + ], + "roots": { + "base": "5d8608fd97e8d12a397a5f8066a952f6cb347e6fb5a62f5e0590c3f9ed98a8d9", + "ours": "7f8b141faeaa443edcf3834970e2ec50d8f0e49264f6f4c9937b55291790ce51", + "theirs": "f803321a6a18e1836cec9d9d79eba5c4cf84cadb7bc8868367c21668b5b3ec87" + }, + "merged": "69095080f2ee342d582a83b671644828fc11d64cb9fe27d8599e879d0aeef6f0" + }, + { + "name": "same-change/sha256", + "addressing": "sha256", + "base": [ + { + "op": "put", + "key": "k0", + "value": { + "n": 0 + } + }, + { + "op": "put", + "key": "k1", + "value": { + "n": 1 + } + }, + { + "op": "put", + "key": "k2", + "value": { + "n": 2 + } + }, + { + "op": "put", + "key": "k3", + "value": { + "n": 3 + } + }, + { + "op": "put", + "key": "k4", + "value": { + "n": 4 + } + }, + { + "op": "put", + "key": "k5", + "value": { + "n": 5 + } + }, + { + "op": "put", + "key": "k6", + "value": { + "n": 6 + } + }, + { + "op": "put", + "key": "k7", + "value": { + "n": 7 + } + }, + { + "op": "put", + "key": "k8", + "value": { + "n": 8 + } + }, + { + "op": "put", + "key": "k9", + "value": { + "n": 9 + } + }, + { + "op": "put", + "key": "k10", + "value": { + "n": 10 + } + }, + { + "op": "put", + "key": "k11", + "value": { + "n": 11 + } + }, + { + "op": "put", + "key": "k12", + "value": { + "n": 12 + } + }, + { + "op": "put", + "key": "k13", + "value": { + "n": 13 + } + }, + { + "op": "put", + "key": "k14", + "value": { + "n": 14 + } + }, + { + "op": "put", + "key": "k15", + "value": { + "n": 15 + } + }, + { + "op": "put", + "key": "k16", + "value": { + "n": 16 + } + }, + { + "op": "put", + "key": "k17", + "value": { + "n": 17 + } + }, + { + "op": "put", + "key": "k18", + "value": { + "n": 18 + } + }, + { + "op": "put", + "key": "k19", + "value": { + "n": 19 + } + }, + { + "op": "put", + "key": "shared/a", + "value": 1 + }, + { + "op": "put", + "key": "shared/b", + "value": 2 + }, + { + "op": "commit" + } + ], + "ours": [ + { + "op": "put", + "key": "k5", + "value": "both" + }, + { + "op": "delete", + "key": "k6" + } + ], + "theirs": [ + { + "op": "put", + "key": "k5", + "value": "both" + }, + { + "op": "delete", + "key": "k6" + }, + { + "op": "put", + "key": "k7", + "value": "theirs" + } + ], + "roots": { + "base": "5d8608fd97e8d12a397a5f8066a952f6cb347e6fb5a62f5e0590c3f9ed98a8d9", + "ours": "cd7f9abb508a208fcc27df1e3a1f721863390f62971bfb923110845552bcec81", + "theirs": "d7e07af3d119833eb80d453c38f301a9b894c7b9fbb1ef6a3c5df809cf7f0a2e" + }, + "merged": "d7e07af3d119833eb80d453c38f301a9b894c7b9fbb1ef6a3c5df809cf7f0a2e" + }, + { + "name": "conflict/sha256", + "addressing": "sha256", + "base": [ + { + "op": "put", + "key": "k0", + "value": { + "n": 0 + } + }, + { + "op": "put", + "key": "k1", + "value": { + "n": 1 + } + }, + { + "op": "put", + "key": "k2", + "value": { + "n": 2 + } + }, + { + "op": "put", + "key": "k3", + "value": { + "n": 3 + } + }, + { + "op": "put", + "key": "k4", + "value": { + "n": 4 + } + }, + { + "op": "put", + "key": "k5", + "value": { + "n": 5 + } + }, + { + "op": "put", + "key": "k6", + "value": { + "n": 6 + } + }, + { + "op": "put", + "key": "k7", + "value": { + "n": 7 + } + }, + { + "op": "put", + "key": "k8", + "value": { + "n": 8 + } + }, + { + "op": "put", + "key": "k9", + "value": { + "n": 9 + } + }, + { + "op": "put", + "key": "k10", + "value": { + "n": 10 + } + }, + { + "op": "put", + "key": "k11", + "value": { + "n": 11 + } + }, + { + "op": "put", + "key": "k12", + "value": { + "n": 12 + } + }, + { + "op": "put", + "key": "k13", + "value": { + "n": 13 + } + }, + { + "op": "put", + "key": "k14", + "value": { + "n": 14 + } + }, + { + "op": "put", + "key": "k15", + "value": { + "n": 15 + } + }, + { + "op": "put", + "key": "k16", + "value": { + "n": 16 + } + }, + { + "op": "put", + "key": "k17", + "value": { + "n": 17 + } + }, + { + "op": "put", + "key": "k18", + "value": { + "n": 18 + } + }, + { + "op": "put", + "key": "k19", + "value": { + "n": 19 + } + }, + { + "op": "put", + "key": "shared/a", + "value": 1 + }, + { + "op": "put", + "key": "shared/b", + "value": 2 + }, + { + "op": "commit" + } + ], + "ours": [ + { + "op": "put", + "key": "k8", + "value": "ours" + }, + { + "op": "delete", + "key": "shared/a" + }, + { + "op": "put", + "key": "x", + "value": 1 + } + ], + "theirs": [ + { + "op": "put", + "key": "k8", + "value": "theirs" + }, + { + "op": "put", + "key": "shared/a", + "value": 99 + }, + { + "op": "put", + "key": "x", + "value": 2 + } + ], + "roots": { + "base": "5d8608fd97e8d12a397a5f8066a952f6cb347e6fb5a62f5e0590c3f9ed98a8d9", + "ours": "bb7c838e1cd11980a607370580806cb07f4f3a7d238ff1a8acc9c983f050c373", + "theirs": "4e5f0d39c4cb0604b4363d3cbc2d3e9b47b7fbdc61a64bbc6a718d465ef846ce" + }, + "conflicts": [ + "k8", + "shared/a", + "x" + ], + "merged_ours_wins": "bb7c838e1cd11980a607370580806cb07f4f3a7d238ff1a8acc9c983f050c373" + }, + { + "name": "disjoint/swarm", + "addressing": "swarm", + "base": [ + { + "op": "put", + "key": "k0", + "value": { + "n": 0 + } + }, + { + "op": "put", + "key": "k1", + "value": { + "n": 1 + } + }, + { + "op": "put", + "key": "k2", + "value": { + "n": 2 + } + }, + { + "op": "put", + "key": "k3", + "value": { + "n": 3 + } + }, + { + "op": "put", + "key": "k4", + "value": { + "n": 4 + } + }, + { + "op": "put", + "key": "k5", + "value": { + "n": 5 + } + }, + { + "op": "put", + "key": "k6", + "value": { + "n": 6 + } + }, + { + "op": "put", + "key": "k7", + "value": { + "n": 7 + } + }, + { + "op": "put", + "key": "k8", + "value": { + "n": 8 + } + }, + { + "op": "put", + "key": "k9", + "value": { + "n": 9 + } + }, + { + "op": "put", + "key": "k10", + "value": { + "n": 10 + } + }, + { + "op": "put", + "key": "k11", + "value": { + "n": 11 + } + }, + { + "op": "put", + "key": "k12", + "value": { + "n": 12 + } + }, + { + "op": "put", + "key": "k13", + "value": { + "n": 13 + } + }, + { + "op": "put", + "key": "k14", + "value": { + "n": 14 + } + }, + { + "op": "put", + "key": "k15", + "value": { + "n": 15 + } + }, + { + "op": "put", + "key": "k16", + "value": { + "n": 16 + } + }, + { + "op": "put", + "key": "k17", + "value": { + "n": 17 + } + }, + { + "op": "put", + "key": "k18", + "value": { + "n": 18 + } + }, + { + "op": "put", + "key": "k19", + "value": { + "n": 19 + } + }, + { + "op": "put", + "key": "shared/a", + "value": 1 + }, + { + "op": "put", + "key": "shared/b", + "value": 2 + }, + { + "op": "commit" + } + ], + "ours": [ + { + "op": "put", + "key": "k1", + "value": "ours" + }, + { + "op": "delete", + "key": "k2" + }, + { + "op": "put", + "key": "new/ours", + "value": 1 + } + ], + "theirs": [ + { + "op": "put", + "key": "k3", + "value": "theirs" + }, + { + "op": "delete", + "key": "k4" + }, + { + "op": "put", + "key": "new/theirs", + "value": 2 + } + ], + "roots": { + "base": "06b870770bc54d34705254f353957716f6ac96b581debcda6f9808710215d8c6", + "ours": "e5943ea5d97c41e100de99e2b194de8bd08ce07bd79bdb97e44a0bdfd30d9b78", + "theirs": "d3908b085e14fa4bf40fa2a5495af9557ee2cf8f95c69798d3927503ce783a16" + }, + "merged": "e72b92340688ed33744fd61baf6b70e4aea66d8c583f48850a32bfbfdee05b30" + }, + { + "name": "same-change/swarm", + "addressing": "swarm", + "base": [ + { + "op": "put", + "key": "k0", + "value": { + "n": 0 + } + }, + { + "op": "put", + "key": "k1", + "value": { + "n": 1 + } + }, + { + "op": "put", + "key": "k2", + "value": { + "n": 2 + } + }, + { + "op": "put", + "key": "k3", + "value": { + "n": 3 + } + }, + { + "op": "put", + "key": "k4", + "value": { + "n": 4 + } + }, + { + "op": "put", + "key": "k5", + "value": { + "n": 5 + } + }, + { + "op": "put", + "key": "k6", + "value": { + "n": 6 + } + }, + { + "op": "put", + "key": "k7", + "value": { + "n": 7 + } + }, + { + "op": "put", + "key": "k8", + "value": { + "n": 8 + } + }, + { + "op": "put", + "key": "k9", + "value": { + "n": 9 + } + }, + { + "op": "put", + "key": "k10", + "value": { + "n": 10 + } + }, + { + "op": "put", + "key": "k11", + "value": { + "n": 11 + } + }, + { + "op": "put", + "key": "k12", + "value": { + "n": 12 + } + }, + { + "op": "put", + "key": "k13", + "value": { + "n": 13 + } + }, + { + "op": "put", + "key": "k14", + "value": { + "n": 14 + } + }, + { + "op": "put", + "key": "k15", + "value": { + "n": 15 + } + }, + { + "op": "put", + "key": "k16", + "value": { + "n": 16 + } + }, + { + "op": "put", + "key": "k17", + "value": { + "n": 17 + } + }, + { + "op": "put", + "key": "k18", + "value": { + "n": 18 + } + }, + { + "op": "put", + "key": "k19", + "value": { + "n": 19 + } + }, + { + "op": "put", + "key": "shared/a", + "value": 1 + }, + { + "op": "put", + "key": "shared/b", + "value": 2 + }, + { + "op": "commit" + } + ], + "ours": [ + { + "op": "put", + "key": "k5", + "value": "both" + }, + { + "op": "delete", + "key": "k6" + } + ], + "theirs": [ + { + "op": "put", + "key": "k5", + "value": "both" + }, + { + "op": "delete", + "key": "k6" + }, + { + "op": "put", + "key": "k7", + "value": "theirs" + } + ], + "roots": { + "base": "06b870770bc54d34705254f353957716f6ac96b581debcda6f9808710215d8c6", + "ours": "dc188c1e71f8b7b68dde08673cc5493eb8406a4a9bad7a632cbccb50334fd5a4", + "theirs": "1a4421bf9d87d2b7418800f8d03cd22f783b730ddb7f58f945e12a029fbe9753" + }, + "merged": "1a4421bf9d87d2b7418800f8d03cd22f783b730ddb7f58f945e12a029fbe9753" + }, + { + "name": "conflict/swarm", + "addressing": "swarm", + "base": [ + { + "op": "put", + "key": "k0", + "value": { + "n": 0 + } + }, + { + "op": "put", + "key": "k1", + "value": { + "n": 1 + } + }, + { + "op": "put", + "key": "k2", + "value": { + "n": 2 + } + }, + { + "op": "put", + "key": "k3", + "value": { + "n": 3 + } + }, + { + "op": "put", + "key": "k4", + "value": { + "n": 4 + } + }, + { + "op": "put", + "key": "k5", + "value": { + "n": 5 + } + }, + { + "op": "put", + "key": "k6", + "value": { + "n": 6 + } + }, + { + "op": "put", + "key": "k7", + "value": { + "n": 7 + } + }, + { + "op": "put", + "key": "k8", + "value": { + "n": 8 + } + }, + { + "op": "put", + "key": "k9", + "value": { + "n": 9 + } + }, + { + "op": "put", + "key": "k10", + "value": { + "n": 10 + } + }, + { + "op": "put", + "key": "k11", + "value": { + "n": 11 + } + }, + { + "op": "put", + "key": "k12", + "value": { + "n": 12 + } + }, + { + "op": "put", + "key": "k13", + "value": { + "n": 13 + } + }, + { + "op": "put", + "key": "k14", + "value": { + "n": 14 + } + }, + { + "op": "put", + "key": "k15", + "value": { + "n": 15 + } + }, + { + "op": "put", + "key": "k16", + "value": { + "n": 16 + } + }, + { + "op": "put", + "key": "k17", + "value": { + "n": 17 + } + }, + { + "op": "put", + "key": "k18", + "value": { + "n": 18 + } + }, + { + "op": "put", + "key": "k19", + "value": { + "n": 19 + } + }, + { + "op": "put", + "key": "shared/a", + "value": 1 + }, + { + "op": "put", + "key": "shared/b", + "value": 2 + }, + { + "op": "commit" + } + ], + "ours": [ + { + "op": "put", + "key": "k8", + "value": "ours" + }, + { + "op": "delete", + "key": "shared/a" + }, + { + "op": "put", + "key": "x", + "value": 1 + } + ], + "theirs": [ + { + "op": "put", + "key": "k8", + "value": "theirs" + }, + { + "op": "put", + "key": "shared/a", + "value": 99 + }, + { + "op": "put", + "key": "x", + "value": 2 + } + ], + "roots": { + "base": "06b870770bc54d34705254f353957716f6ac96b581debcda6f9808710215d8c6", + "ours": "8fff628e0fcfbf5bc6f4648c10e052e8b2850558b171dcd67cc81b8ef8594617", + "theirs": "98770a494e3f7514ad1f6891c7b545848eb314616b4ca7e78bedd1556f7e7196" + }, + "conflicts": [ + "k8", + "shared/a", + "x" + ], + "merged_ours_wins": "8fff628e0fcfbf5bc6f4648c10e052e8b2850558b171dcd67cc81b8ef8594617" + } + ] +} diff --git a/js/test/fixtures/stores.json b/js/test/fixtures/stores.json new file mode 100644 index 0000000..3a7a8ca --- /dev/null +++ b/js/test/fixtures/stores.json @@ -0,0 +1,10979 @@ +{ + "note": "Generated by js/tools/make_fixtures.py from the Python reference implementation; do not edit by hand.", + "cases": [ + { + "name": "scripted/sha256", + "addressing": "sha256", + "steps": [ + { + "op": "put", + "key": "users/alice", + "value": { + "name": "Alice", + "role": "admin" + } + }, + { + "op": "put", + "key": "users/bob", + "value": { + "name": "Bob" + } + }, + { + "op": "put", + "key": "users/al", + "value": "a proper prefix of alice" + }, + { + "op": "put", + "key": "u", + "value": 1 + }, + { + "op": "put", + "key": "userz", + "value": [ + 1, + 2, + 3 + ] + }, + { + "op": "commit", + "root": "a38759063ca99e0bd84acc1b6267aff9e4802bccc9a5b808ef3dbc0da96d7026" + }, + { + "op": "delete", + "key": "users/al", + "existed": true + }, + { + "op": "commit", + "root": "98c2151ebf6f2bec3265a169dd27e4d49e6c9d981f2c7db12bd5dc37c60f03f6" + }, + { + "op": "put", + "key": "users/alice", + "value": { + "name": "Alice", + "role": "admin" + } + }, + { + "op": "commit", + "root": "98c2151ebf6f2bec3265a169dd27e4d49e6c9d981f2c7db12bd5dc37c60f03f6" + }, + { + "op": "put", + "key": "é", + "value": "two bytes" + }, + { + "op": "put", + "key": "😀", + "value": "four bytes" + }, + { + "op": "put", + "key": "￿", + "value": "three bytes, sorts before the emoji" + }, + { + "op": "put", + "key": "a/b/c", + "value": null + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "never-was", + "existed": false + }, + { + "op": "commit", + "root": "4b14fa9c584f0bec2a2484c6fae26dc7efb35a7bc0c01eea568d8593b226dda6" + }, + { + "op": "put", + "key": "tmp", + "value": "staged then deleted" + }, + { + "op": "delete", + "key": "tmp", + "existed": true + }, + { + "op": "put", + "key": "users/bob", + "value": { + "name": "Bob", + "age": 0.5 + } + }, + { + "op": "commit", + "root": "a35fceeecd9a0f1792d90f8b41a0e1ffab5c9b125f6a2ad6f4d1a55128479276" + }, + { + "op": "delete", + "key": "users/alice", + "existed": true + }, + { + "op": "delete", + "key": "users/bob", + "existed": true + }, + { + "op": "delete", + "key": "userz", + "existed": true + }, + { + "op": "commit", + "root": "3a96f2f949ab2fde23ee9c0f529bd371393e5d96a240ef694874d16917aac5fc" + } + ], + "items": [ + [ + "a/b/c", + null + ], + [ + "é", + "two bytes" + ], + [ + "￿", + "three bytes, sorts before the emoji" + ], + [ + "😀", + "four bytes" + ] + ], + "blobs": { + "0375b540e87ae4afba9935315fe44e7a569569828b8741182165d92efb70a037": "7b2263223a7b223733223a2262613962346364663833626137393432323264623838646363653730323937656631383463666263623435663766643834643832653838663635303466623063222c223761223a2234646638666165386430316361626135323937376634343835363233383135656538363064346361393865306632653030663964616430376337303533623561227d2c2270223a22373336353732222c22746e223a312c2276223a6e756c6c7d", + "08e62e822c952f0d654ea91e43adf8de5b654fa32d7a4e367689df2505d6113e": "7b2263223a7b223631223a2239656663303665353762346562373162373334336461323133386463376261346532643166323133306138323063623561396663623238366539373136633362222c223632223a2239333666633932623236363539646462373837616337366662306262373163653437346332623139356561643164313663323061623635376263373465366238227d2c2270223a223266222c22746e223a312c2276223a6e756c6c7d", + "0a1e6cd41cc66fe2a711a7ad53a7d4f856aa5b3384d6087cd83327216a1a5fc9": "7b2263223a7b7d2c2270223a223266363232663633222c22746e223a312c2276223a2232306662646531306239386663633362396564366363653166316637616365663465343066356462636164633465353235396165613537366131333961393662227d", + "0ba10f3d8ff85286020a7a876655ab07217d6b22663b74ee08c831ea55c381de": "7b2263223a7b223733223a2261393939326431326565613266656335656261373035643863616534356334343933393639653365333733643933626262626339376337363164376233313436222c223761223a2234646638666165386430316361626135323937376634343835363233383135656538363064346361393865306632653030663964616430376337303533623561227d2c2270223a2236353732222c22746e223a312c2276223a6e756c6c7d", + "109986fc4ee45c8a9ebe7921452423461e82aa144adefe704819e9a2b8944125": "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2237316336653462656461333461383934326234636265383064636134623064366464333333613964346163326239653666356264666465353065336466393639227d", + "20fbde10b98fcc3b9ed6cce1f1f7acef4e40f5dbcadc4e5259aea576a139a96b": "7b22727376223a312c2276616c223a6e756c6c7d", + "2b8f14177efeed39c7929bd1c50d89e817fb75084da2c0cace2b10daf1df6fdd": "7b2263223a7b223733223a2230386536326538323263393532663064363534656139316534336164663864653562363534666133326437613465333637363839646632353035643631313365222c223761223a2234646638666165386430316361626135323937376634343835363233383135656538363064346361393865306632653030663964616430376337303533623561227d2c2270223a2236353732222c22746e223a312c2276223a6e756c6c7d", + "3a96f2f949ab2fde23ee9c0f529bd371393e5d96a240ef694874d16917aac5fc": "7b2263223a7b223631223a2230613165366364343163633636666532613731316137616435336137643466383536616135623333383464363038376364383333323732313661316135666339222c226333223a2264646231356536306363323330373638383536306364396430333730346161376263376137363130656132623734373136313335363665653463373662313933222c226566223a2265333234633434326137666433376137333031636334643333646161383534393331383162333066373435346562313964366363653163366438396339353539222c226630223a2231303939383666633465653435633861396562653739323134353234323334363165383261613134346164656665373034383139653961326238393434313235227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d", + "3e7a72fb8a26e9f35d6a2f4519f26f21cad2c79bbf634682442d5d9d56039e87": "7b2263223a7b7d2c2270223a2236663632222c22746e223a312c2276223a2263313562633163373535366431393062363638363863623131383938646461303861616630396536623639386239353963613435376165623361336237663932227d", + "442bd272d7ec9c6b0ade5fa78d7e91296bb70564138592c79c3eb1c5cd7f78ad": "7b2263223a7b223639223a2261636434316434303739623962646230396461623136386466653966363136366237313837313334366463303234356561333561636136313332326562653037227d2c2270223a223663222c22746e223a312c2276223a2265653436336234363661366334373637393234643334326461643863373064323039643739626332333139356565616465666664653161633531643537613631227d", + "4b14fa9c584f0bec2a2484c6fae26dc7efb35a7bc0c01eea568d8593b226dda6": "7b2263223a7b223631223a2230613165366364343163633636666532613731316137616435336137643466383536616135623333383464363038376364383333323732313661316135666339222c223735223a2261663862633765333630633133363635313930323566333338393434343964663532643631306164653162653830633939623337663338653866356562343036222c226333223a2264646231356536306363323330373638383536306364396430333730346161376263376137363130656132623734373136313335363665653463373662313933222c226566223a2265333234633434326137666433376137333031636334643333646161383534393331383162333066373435346562313964366363653163366438396339353539222c226630223a2231303939383666633465653435633861396562653739323134353234323334363165383261613134346164656665373034383139653961326238393434313235227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d", + "4df8fae8d01caba52977f4485623815ee860d4ca98e0f2e00f9dad07c7053b5a": "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2264663061666665313634343338363035616661646561333639303339383465316366633531336534663038333865653039646162363939633239353836326339227d", + "532ea093118282e16cb7d431f295e59109eb733661e539978cffd99b39e7b533": "7b22727376223a312c2276616c223a317d", + "71c6e4beda34a8942b4cbe80dca4b0d6dd333a9d4ac2b9e6f5bdfde50e3df969": "7b22727376223a312c2276616c223a22666f7572206279746573227d", + "8274c8dccf1afdda59ca0efa35cf974e49e3c5d7707a40ce9e14fcc11138f425": "7b22727376223a312c2276616c223a2274776f206279746573227d", + "936fc92b26659ddb787ac76fb0bb71ce474c2b195ead1d16c20ab657bc74e6b8": "7b2263223a7b7d2c2270223a2236663632222c22746e223a312c2276223a2263386431633536353664633032626635613262616666313039396431386338656435376330383061346138316366646532663161626239396139653936633165227d", + "98c2151ebf6f2bec3265a169dd27e4d49e6c9d981f2c7db12bd5dc37c60f03f6": "7b2263223a7b223733223a2232623866313431373765666565643339633739323962643163353064383965383137666237353038346461326330636163653262313064616631646636666464227d2c2270223a223735222c22746e223a312c2276223a2235333265613039333131383238326531366362376434333166323935653539313039656237333336363165353339393738636666643939623339653762353333227d", + "9efc06e57b4eb71b7343da2138dc7ba4e2d1f2130a820cb5a9fcb286e9716c3b": "7b2263223a7b7d2c2270223a223663363936333635222c22746e223a312c2276223a2265336662353131373464663631613963363339653165363235333132613462333930393062633330373932636234613664316461303162306630636664613235227d", + "a35fceeecd9a0f1792d90f8b41a0e1ffab5c9b125f6a2ad6f4d1a55128479276": "7b2263223a7b223631223a2230613165366364343163633636666532613731316137616435336137643466383536616135623333383464363038376364383333323732313661316135666339222c223735223a2230333735623534306538376165346166626139393335333135666534346537613536393536393832386238373431313832313635643932656662373061303337222c226333223a2264646231356536306363323330373638383536306364396430333730346161376263376137363130656132623734373136313335363665653463373662313933222c226566223a2265333234633434326137666433376137333031636334643333646161383534393331383162333066373435346562313964366363653163366438396339353539222c226630223a2231303939383666633465653435633861396562653739323134353234323334363165383261613134346164656665373034383139653961326238393434313235227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d", + "a38759063ca99e0bd84acc1b6267aff9e4802bccc9a5b808ef3dbc0da96d7026": "7b2263223a7b223733223a2230626131306633643866663835323836303230613761383736363535616230373231376436623232363633623734656530386338333165613535633338316465227d2c2270223a223735222c22746e223a312c2276223a2235333265613039333131383238326531366362376434333166323935653539313039656237333336363165353339393738636666643939623339653762353333227d", + "a9992d12eea2fec5eba705d8cae45c4493969e3e373d93bbbbc97c761d7b3146": "7b2263223a7b223631223a2234343262643237326437656339633662306164653566613738643765393132393662623730353634313338353932633739633365623163356364376637386164222c223632223a2239333666633932623236363539646462373837616337366662306262373163653437346332623139356561643164313663323061623635376263373465366238227d2c2270223a223266222c22746e223a312c2276223a6e756c6c7d", + "acd41d4079b9bdb09dab168dfe9f6166b71871346dc0245ea35aca61322ebe07": "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2265336662353131373464663631613963363339653165363235333132613462333930393062633330373932636234613664316461303162306630636664613235227d", + "af8bc7e360c1366519025f33894449df52d610ade1be80c99b37f38e8f5eb406": "7b2263223a7b223733223a2230386536326538323263393532663064363534656139316534336164663864653562363534666133326437613465333637363839646632353035643631313365222c223761223a2234646638666165386430316361626135323937376634343835363233383135656538363064346361393865306632653030663964616430376337303533623561227d2c2270223a22373336353732222c22746e223a312c2276223a6e756c6c7d", + "ba9b4cdf83ba794222db88dcce70297ef184cfbcb45f7fd84d82e88f6504fb0c": "7b2263223a7b223631223a2239656663303665353762346562373162373334336461323133386463376261346532643166323133306138323063623561396663623238366539373136633362222c223632223a2233653761373266623861323665396633356436613266343531396632366632316361643263373962626636333436383234343264356439643536303339653837227d2c2270223a223266222c22746e223a312c2276223a6e756c6c7d", + "c15bc1c7556d190b66868cb11898dda08aaf09e6b698b959ca457aeb3a3b7f92": "7b22727376223a312c2276616c223a7b22616765223a302e352c226e616d65223a22426f62227d7d", + "c8d1c5656dc02bf5a2baff1099d18c8ed57c080a4a81cfde2f1abb99a9e96c1e": "7b22727376223a312c2276616c223a7b226e616d65223a22426f62227d7d", + "ddb15e60cc2307688560cd9d03704aa7bc7a7610ea2b7471613566ee4c76b193": "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2238323734633864636366316166646461353963613065666133356366393734653439653363356437373037613430636539653134666363313131333866343235227d", + "df0affe164438605afadea36903984e1cfc513e4f0838ee09dab699c295862c9": "7b22727376223a312c2276616c223a5b312c322c335d7d", + "e02aaf34983040dabcf56c215587d7cb89994dbf1c393d98b8e01407970087d8": "7b22727376223a312c2276616c223a2274687265652062797465732c20736f727473206265666f72652074686520656d6f6a69227d", + "e324c442a7fd37a7301cc4d33daa85493181b30f7454eb19d6cce1c6d89c9559": "7b2263223a7b7d2c2270223a2262666266222c22746e223a312c2276223a2265303261616633343938333034306461626366353663323135353837643763623839393934646266316333393364393862386530313430373937303038376438227d", + "e3fb51174df61a9c639e1e625312a4b39090bc30792cb4a6d1da01b0f0cfda25": "7b22727376223a312c2276616c223a7b226e616d65223a22416c696365222c22726f6c65223a2261646d696e227d7d", + "ee463b466a6c4767924d342dad8c70d209d79bc23195eeadeffde1ac51d57a61": "7b22727376223a312c2276616c223a22612070726f70657220707265666978206f6620616c696365227d" + } + }, + { + "name": "scripted/swarm", + "addressing": "swarm", + "steps": [ + { + "op": "put", + "key": "users/alice", + "value": { + "name": "Alice", + "role": "admin" + } + }, + { + "op": "put", + "key": "users/bob", + "value": { + "name": "Bob" + } + }, + { + "op": "put", + "key": "users/al", + "value": "a proper prefix of alice" + }, + { + "op": "put", + "key": "u", + "value": 1 + }, + { + "op": "put", + "key": "userz", + "value": [ + 1, + 2, + 3 + ] + }, + { + "op": "commit", + "root": "ed32291ea33f8f6b24e9d0fe8b6011e82ca4ec5e4c3564cdf2cc38f8b41bcc5d" + }, + { + "op": "delete", + "key": "users/al", + "existed": true + }, + { + "op": "commit", + "root": "361d6fe0c0712a6c9a3ba78010f1a1d0adaa17d4aed9857768cb96a131a53969" + }, + { + "op": "put", + "key": "users/alice", + "value": { + "name": "Alice", + "role": "admin" + } + }, + { + "op": "commit", + "root": "361d6fe0c0712a6c9a3ba78010f1a1d0adaa17d4aed9857768cb96a131a53969" + }, + { + "op": "put", + "key": "é", + "value": "two bytes" + }, + { + "op": "put", + "key": "😀", + "value": "four bytes" + }, + { + "op": "put", + "key": "￿", + "value": "three bytes, sorts before the emoji" + }, + { + "op": "put", + "key": "a/b/c", + "value": null + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "never-was", + "existed": false + }, + { + "op": "commit", + "root": "aa03a7c124e6029aa5e8c919532214187f0d8cff77ccd05185c8b0f6d5048c10" + }, + { + "op": "put", + "key": "tmp", + "value": "staged then deleted" + }, + { + "op": "delete", + "key": "tmp", + "existed": true + }, + { + "op": "put", + "key": "users/bob", + "value": { + "name": "Bob", + "age": 0.5 + } + }, + { + "op": "commit", + "root": "d02fbbe6d9be8d6a91d383d3668fc3753bd4f0c5a5abec219242f032b7ba0e3f" + }, + { + "op": "delete", + "key": "users/alice", + "existed": true + }, + { + "op": "delete", + "key": "users/bob", + "existed": true + }, + { + "op": "delete", + "key": "userz", + "existed": true + }, + { + "op": "commit", + "root": "1c875ddfe7852119d0a8e25ec0e5d0be616f811ff01ba73e6f4fb529d8211f86" + } + ], + "items": [ + [ + "a/b/c", + null + ], + [ + "é", + "two bytes" + ], + [ + "￿", + "three bytes, sorts before the emoji" + ], + [ + "😀", + "four bytes" + ] + ], + "blobs": { + "05b5d567f16c3c5e28807ce75d5c312fd34cd8682f87d3ca304a781766275bd7": "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2235356462333731666261346231363234356466636461313666653563633565653036356237363136323735373039626432383866376234636536333663396231227d", + "06698071c1052b018fbc0550819448526e9da9514b4fdf4baca998586a2a287a": "7b2263223a7b223733223a2262636230303732656363313437646262326563356563316338623839313463663763316437376239646238616431343166386538396130633664323738623138222c223761223a2231636331303261633538393437316361373665313535316364316233366135343431386461313636363866653334656332313331643639373031313839656639227d2c2270223a22373336353732222c22746e223a312c2276223a6e756c6c7d", + "0721f9178381ea0ede44b5b4bf63c69fd367f6e6f424644646c5f358b580db17": "7b22727376223a312c2276616c223a7b22616765223a302e352c226e616d65223a22426f62227d7d", + "0ce8f00f8394bd668f57d22a546e442bcc8e005d792dd7a1d57f37169239ade5": "7b22727376223a312c2276616c223a317d", + "0e27ffdc6908d04b79a1136c92aa0c38b3dc476e72d0fe36e50fa8d75160575e": "7b2263223a7b7d2c2270223a2236663632222c22746e223a312c2276223a2230373231663931373833383165613065646534346235623462663633633639666433363766366536663432343634343634366335663335386235383064623137227d", + "108db57d052f243c2d58f670ca19b60b1132461d68faa5e072b1a91db1be2617": "7b2263223a7b223733223a2232663866636536373734616463356264643230396638326563303235356238653930653130303234313233373339393065363463616230303135333531353463222c223761223a2231636331303261633538393437316361373665313535316364316233366135343431386461313636363866653334656332313331643639373031313839656639227d2c2270223a2236353732222c22746e223a312c2276223a6e756c6c7d", + "17596c4eea79c7ca2075b252c150f4849d0a1e749d82d28a5e4a37236823b24b": "7b2263223a7b223639223a2230356235643536376631366333633565323838303763653735643563333132666433346364383638326638376433636133303461373831373636323735626437227d2c2270223a223663222c22746e223a312c2276223a2265613531396363316533323930383131313632366365643235353936326430383232326334326330306264343935393862656637663731376331636561306163227d", + "1a96b018b3e7cefac9add8a7c383ae8d7bd67b723ad4880a3eac13ea3065c05a": "7b22727376223a312c2276616c223a6e756c6c7d", + "1c875ddfe7852119d0a8e25ec0e5d0be616f811ff01ba73e6f4fb529d8211f86": "7b2263223a7b223631223a2233363266386638383365313864346366306661653134613838323932396361336232396338343063343963616336666637616434623964303939333765613465222c226333223a2262343533653739393939393235343333333238626137333538633065393938666630313430623332386266623665666163663638656362353761393831313333222c226566223a2264663039613364373531353832643535386465616337396466373665633064356439363236353231303666613736373866613930353562376237626561316564222c226630223a2236636230666634643537313362313932383761626439383032333532343133646533616338343632626631653037393164656137393432386461303538653063227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d", + "1cc102ac589471ca76e1551cd1b36a54418da16668fe34ec2131d69701189ef9": "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2261633936393232613038353965316133333139343565613861626433316664373965643933633862333665366339366536353935316435646334393132623332227d", + "2f8fce6774adc5bdd209f82ec0255b8e90e1002412373990e64cab001535154c": "7b2263223a7b223631223a2235626534306361316364663430396639646236346562343465613063353266623030663864333532303936323739313136643061613131383966373339663934222c223632223a2236393137613361386531373062636665356131386566303263393065393765626266313562663835386337363763326561613561643865623531336362383030227d2c2270223a223266222c22746e223a312c2276223a6e756c6c7d", + "361cf9760f0af3d3be81ef27ae8300c8cef7ead23727a6b77014a496c3474ae5": "7b22727376223a312c2276616c223a22666f7572206279746573227d", + "361d6fe0c0712a6c9a3ba78010f1a1d0adaa17d4aed9857768cb96a131a53969": "7b2263223a7b223733223a2231303864623537643035326632343363326435386636373063613139623630623131333234363164363866616135653037326231613931646231626532363137227d2c2270223a223735222c22746e223a312c2276223a2230636538663030663833393462643636386635376432326135343665343432626363386530303564373932646437613164353766333731363932333961646535227d", + "362f8f883e18d4cf0fae14a882929ca3b29c840c49cac6ff7ad4b9d09937ea4e": "7b2263223a7b7d2c2270223a223266363232663633222c22746e223a312c2276223a2231613936623031386233653763656661633961646438613763333833616538643762643637623732336164343838306133656163313365613330363563303561227d", + "49035f01f2d1909198278fa4108e5aa829d04737565d21d1dd3cf77715722a34": "7b2263223a7b223631223a2231373539366334656561373963376361323037356232353263313530663438343964306131653734396438326432386135653461333732333638323362323462222c223632223a2236393137613361386531373062636665356131386566303263393065393765626266313562663835386337363763326561613561643865623531336362383030227d2c2270223a223266222c22746e223a312c2276223a6e756c6c7d", + "55db371fba4b16245dfcda16fe5cc5ee065b7616275709bd288f7b4ce636c9b1": "7b22727376223a312c2276616c223a7b226e616d65223a22416c696365222c22726f6c65223a2261646d696e227d7d", + "5be40ca1cdf409f9db64eb44ea0c52fb00f8d352096279116d0aa1189f739f94": "7b2263223a7b7d2c2270223a223663363936333635222c22746e223a312c2276223a2235356462333731666261346231363234356466636461313666653563633565653036356237363136323735373039626432383866376234636536333663396231227d", + "6917a3a8e170bcfe5a18ef02c90e97ebbf15bf858c767c2eaa5ad8eb513cb800": "7b2263223a7b7d2c2270223a2236663632222c22746e223a312c2276223a2262323930313663396232396337343335626138333463323835653665383761393136393765366330376537336430623136343864656634653165386165303165227d", + "6cb0ff4d5713b19287abd9802352413de3ac8462bf1e0791dea79428da058e0c": "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2233363163663937363066306166336433626538316566323761653833303063386365663765616432333732376136623737303134613439366333343734616535227d", + "85e8cd8d7bfc91b0eecab242bbb15c60ff8940202701bc069f92cab2a471459d": "7b2263223a7b223733223a2234393033356630316632643139303931393832373866613431303865356161383239643034373337353635643231643164643363663737373135373232613334222c223761223a2231636331303261633538393437316361373665313535316364316233366135343431386461313636363866653334656332313331643639373031313839656639227d2c2270223a2236353732222c22746e223a312c2276223a6e756c6c7d", + "a1492bc0c8cce3faa685187ae430d1d2dfaf15cbbdcaeff0ae3df52bd3ba29f8": "7b2263223a7b223733223a2232663866636536373734616463356264643230396638326563303235356238653930653130303234313233373339393065363463616230303135333531353463222c223761223a2231636331303261633538393437316361373665313535316364316233366135343431386461313636363866653334656332313331643639373031313839656639227d2c2270223a22373336353732222c22746e223a312c2276223a6e756c6c7d", + "a4e595501cd6b69bb3f9799f2716ec3eae80a5843a512f3ae501f753932c27f5": "7b22727376223a312c2276616c223a2274687265652062797465732c20736f727473206265666f72652074686520656d6f6a69227d", + "aa03a7c124e6029aa5e8c919532214187f0d8cff77ccd05185c8b0f6d5048c10": "7b2263223a7b223631223a2233363266386638383365313864346366306661653134613838323932396361336232396338343063343963616336666637616434623964303939333765613465222c223735223a2261313439326263306338636365336661613638353138376165343330643164326466616631356362626463616566663061653364663532626433626132396638222c226333223a2262343533653739393939393235343333333238626137333538633065393938666630313430623332386266623665666163663638656362353761393831313333222c226566223a2264663039613364373531353832643535386465616337396466373665633064356439363236353231303666613736373866613930353562376237626561316564222c226630223a2236636230666634643537313362313932383761626439383032333532343133646533616338343632626631653037393164656137393432386461303538653063227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d", + "ac96922a0859e1a331945ea8abd31fd79ed93c8b36e6c96e65951d5dc4912b32": "7b22727376223a312c2276616c223a5b312c322c335d7d", + "b29016c9b29c7435ba834c285e6e87a91697e6c07e73d0b1648def4e1e8ae01e": "7b22727376223a312c2276616c223a7b226e616d65223a22426f62227d7d", + "b453e79999925433328ba7358c0e998ff0140b328bfb6efacf68ecb57a981133": "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2262633537346462653635616562303233303363336665653733613933633534633665623265323563623031383530346264396166623031623232303962616536227d", + "bc574dbe65aeb02303c3fee73a93c54c6eb2e25cb018504bd9afb01b2209bae6": "7b22727376223a312c2276616c223a2274776f206279746573227d", + "bcb0072ecc147dbb2ec5ec1c8b8914cf7c1d77b9db8ad141f8e89a0c6d278b18": "7b2263223a7b223631223a2235626534306361316364663430396639646236346562343465613063353266623030663864333532303936323739313136643061613131383966373339663934222c223632223a2230653237666664633639303864303462373961313133366339326161306333386233646334373665373264306665333665353066613864373531363035373565227d2c2270223a223266222c22746e223a312c2276223a6e756c6c7d", + "d02fbbe6d9be8d6a91d383d3668fc3753bd4f0c5a5abec219242f032b7ba0e3f": "7b2263223a7b223631223a2233363266386638383365313864346366306661653134613838323932396361336232396338343063343963616336666637616434623964303939333765613465222c223735223a2230363639383037316331303532623031386662633035353038313934343835323665396461393531346234666466346261636139393835383661326132383761222c226333223a2262343533653739393939393235343333333238626137333538633065393938666630313430623332386266623665666163663638656362353761393831313333222c226566223a2264663039613364373531353832643535386465616337396466373665633064356439363236353231303666613736373866613930353562376237626561316564222c226630223a2236636230666634643537313362313932383761626439383032333532343133646533616338343632626631653037393164656137393432386461303538653063227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d", + "df09a3d751582d558deac79df76ec0d5d962652106fa7678fa9055b7b7bea1ed": "7b2263223a7b7d2c2270223a2262666266222c22746e223a312c2276223a2261346535393535303163643662363962623366393739396632373136656333656165383061353834336135313266336165353031663735333933326332376635227d", + "ea519cc1e32908111626ced255962d08222c42c00bd49598bef7f717c1cea0ac": "7b22727376223a312c2276616c223a22612070726f70657220707265666978206f6620616c696365227d", + "ed32291ea33f8f6b24e9d0fe8b6011e82ca4ec5e4c3564cdf2cc38f8b41bcc5d": "7b2263223a7b223733223a2238356538636438643762666339316230656563616232343262626231356336306666383934303230323730316263303639663932636162326134373134353964227d2c2270223a223735222c22746e223a312c2276223a2230636538663030663833393462643636386635376432326135343665343432626363386530303564373932646437613164353766333731363932333961646535227d" + } + }, + { + "name": "random-1/sha256", + "addressing": "sha256", + "steps": [ + { + "op": "put", + "key": "ab", + "value": true + }, + { + "op": "put", + "key": "u", + "value": {} + }, + { + "op": "commit", + "root": "f49bddb1249c6dfd201087eee693923693ab7300822a63afb46d24765ef15b2f" + }, + { + "op": "put", + "key": "😀", + "value": "😀x/" + }, + { + "op": "put", + "key": "￿", + "value": null + }, + { + "op": "commit", + "root": "97af82cb8e859e0e6dc098be627095926fa8dac364950a43627a70527efedfc2" + }, + { + "op": "put", + "key": "users/￿", + "value": 527401.990262979 + }, + { + "op": "delete", + "key": "users/￿", + "existed": true + }, + { + "op": "commit", + "root": "97af82cb8e859e0e6dc098be627095926fa8dac364950a43627a70527efedfc2" + }, + { + "op": "put", + "key": "u/user/ab", + "value": false + }, + { + "op": "commit", + "root": "fdf60350995af2f50c18457ecbdaad82f89ef4972f5ed89eb58582f60faddac9" + }, + { + "op": "commit", + "root": "fdf60350995af2f50c18457ecbdaad82f89ef4972f5ed89eb58582f60faddac9" + }, + { + "op": "put", + "key": "a/ab/ab", + "value": { + "😀b\u00011\\\n": 879078303060946, + "c\téxz": [ + 4193135724629501, + null, + -212800.62724417204 + ], + "中1\\é\"😀\"\t": "￿a\\éyé中0", + "b\u0001\"😀中0": [ + 639.8696662871351, + false, + false + ] + } + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "put", + "key": "a/ab", + "value": false + }, + { + "op": "put", + "key": "user", + "value": "z9
 \t/\u0001\u0001" + }, + { + "op": "put", + "key": "é/a", + "value": "9" + }, + { + "op": "commit", + "root": "256f3780a2f49185dbf58b04518d30f44511dc21a0e8503726e8d9ad3e13f9dd" + }, + { + "op": "put", + "key": "é/users/a", + "value": null + }, + { + "op": "put", + "key": "user", + "value": { + "中1
é\t1": [], + "😀/
\nb ": -5185773715815691, + "": "c", + " z\n😀": "a中" + } + }, + { + "op": "commit", + "root": "d68880432d31c5687aa4a45a29537a9e7100746732a8025b08f6795954c3e325" + }, + { + "op": "put", + "key": "😀/user/x", + "value": [] + }, + { + "op": "put", + "key": "x", + "value": { + "\u0001x\\": "\u0001a/￿\\ az", + "/😀y": 1152921504606847415, + "x\\中\"": [ + "cbc", + true, + "9/￿" + ], + "9\"//x 1￿": { + "x/b\nc\\yy": 2075006567467046 + } + } + }, + { + "op": "put", + "key": "x", + "value": [ + false + ] + }, + { + "op": "put", + "key": "u/😀/ab", + "value": false + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": 571024.2686891332 + }, + { + "op": "delete", + "key": "a/ab", + "existed": true + }, + { + "op": "put", + "key": "ab/￿/ab", + "value": { + "0": 1152921504606847016, + "": "\t\\/\\c" + } + }, + { + "op": "put", + "key": "😀/u/ab", + "value": -288346.0334079566 + }, + { + "op": "put", + "key": "😀", + "value": false + }, + { + "op": "delete", + "key": "users/abc", + "existed": false + }, + { + "op": "put", + "key": "ab/a/abc", + "value": false + }, + { + "op": "put", + "key": "a", + "value": null + }, + { + "op": "delete", + "key": "ab/￿/ab", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "delete", + "key": "😀/u/ab", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": [ + false, + 4975737563517114 + ] + }, + { + "op": "put", + "key": "abc", + "value": "é" + }, + { + "op": "delete", + "key": "ab", + "existed": true + }, + { + "op": "commit", + "root": "35c8a0d0b0acf279b49910699e01866ffe34d9707218df523565d1ba01d49e3a" + }, + { + "op": "put", + "key": "abc", + "value": [ + -1213485891030941 + ] + }, + { + "op": "put", + "key": "a", + "value": "\t" + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x/users", + "value": null + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "é/x", + "value": { + "1": {} + } + }, + { + "op": "put", + "key": "😀/a/a", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "c764a1ece6c78195ee8c786e56e19f189a925e7787c42a5d882ef420b192c7f1" + }, + { + "op": "delete", + "key": "é/a", + "existed": true + }, + { + "op": "commit", + "root": "d2727532869754008b0e3d5cbdbbf86d2bda6e97b6c6aed4f94abfd35abb8bfd" + }, + { + "op": "commit", + "root": "d2727532869754008b0e3d5cbdbbf86d2bda6e97b6c6aed4f94abfd35abb8bfd" + }, + { + "op": "put", + "key": "u", + "value": [ + 4792331001286011, + -376395.9363293954 + ] + }, + { + "op": "delete", + "key": "é/users/a", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": [ + { + "9\n": false + }, + null, + { + "\\éz中b": "9", + "9": 1152921504606847910, + "￿c": -0.5180194084166361 + }, + -3144101130451274 + ] + }, + { + "op": "put", + "key": "a/u", + "value": { + "\nx 91\\中a": 0.1581652465616863, + "z y": [ + 1544518820599314 + ], + "\tz中\"": { + "0": "0 xax😀", + "": "
ycé", + "😀 \né\"": "ax\t\t\"" + }, + "中\\/😀": {} + } + }, + { + "op": "delete", + "key": "ab/a/abc", + "existed": true + }, + { + "op": "put", + "key": "x/￿/a", + "value": { + "\n z\t￿é0\"": [], + "😀\n\\/￿😀": true, + "

 ": null, + "
y
\\9z": false + } + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "é/x", + "existed": true + }, + { + "op": "put", + "key": "ab", + "value": { + "éb9éx😀\n": true + } + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "delete", + "key": "a/ab/ab", + "existed": true + }, + { + "op": "put", + "key": "ab/a/￿", + "value": 0.023037844265266116 + }, + { + "op": "delete", + "key": "x/users", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": [ + 5219989364590686, + "19￿1a", + { + "190c
z": "😀y￿9\téz", + "y\t": 4528391509020087 + } + ] + }, + { + "op": "put", + "key": "a", + "value": "x" + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "put", + "key": "users", + "value": { + "/9x￿zx1": { + "\t\\z11 \t": "0\t9/\u0001😀" + }, + "0": true, + "": { + "😀 0\\z
": false, + "": null + }, + "y中b😀\\9": -668528497962439 + } + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "commit", + "root": "86851839ac8cb1de3625a150c1d93526d3a00a14a3e5408fac000c07943972a1" + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "put", + "key": "ab/a/😀", + "value": { + "9

1": 0.8703319994801906, + "
éb\"中\n中0": [ + -18446744073709551616, + 1152921504606847601, + 8582122809182276 + ] + } + }, + { + "op": "put", + "key": "a", + "value": {} + }, + { + "op": "put", + "key": "￿", + "value": { + "x/by中": null, + "y\\\taé9c": "c b\\b", + "/y9\\": false, + "x\n1é": [ + -2906383224920196 + ] + } + }, + { + "op": "put", + "key": "😀/u/user", + "value": { + "😀é中a z0\"": { + "x\n\"y😀": null, + "": 5385660561127926, + "/\n /\"9/é": "", + "xy///😀c\t": -826100539360698 + }, + "\\c😀by": null, + "\u0001😀91😀/\"
": -18446744073709551616, + "￿/中ézay": "😀yx" + } + }, + { + "op": "put", + "key": "x/users/u", + "value": [ + true, + false + ] + }, + { + "op": "put", + "key": "u", + "value": 8.756506606108011e-11 + }, + { + "op": "put", + "key": "ab/a/a", + "value": { + "\t\"中0\u0001c": "0a中\\é\u0001" + } + }, + { + "op": "put", + "key": "x/x/é", + "value": null + }, + { + "op": "put", + "key": "a", + "value": "" + }, + { + "op": "put", + "key": "abc/￿/x", + "value": [ + [ + "\n￿
😀 \t y", + "😀y中z9
a", + 0.13181702846660892 + ], + null + ] + }, + { + "op": "commit", + "root": "433bdb69ba8ba6c13e24a7d604a71f78536e008007200031f606d7b406193b6d" + }, + { + "op": "commit", + "root": "433bdb69ba8ba6c13e24a7d604a71f78536e008007200031f606d7b406193b6d" + }, + { + "op": "delete", + "key": "x/￿/a", + "existed": true + }, + { + "op": "put", + "key": "😀/abc/é", + "value": {} + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "commit", + "root": "3c3c82b2e33a81f0e44b13f08518f2fb0866659263d0aa3c387a3f5d873e949d" + }, + { + "op": "delete", + "key": "ab/a/￿", + "existed": true + }, + { + "op": "put", + "key": "ab/￿", + "value": "9\n/\u00010\u0001" + }, + { + "op": "commit", + "root": "654f26a01f27360b7d73587b09206afe38d9b59b404eaf606b96209f96619c85" + }, + { + "op": "put", + "key": "user", + "value": 933029.5942664645 + }, + { + "op": "put", + "key": "user/users", + "value": true + }, + { + "op": "commit", + "root": "3e29ede181ab48b84015b9d41a9ca4008154fbd59246046555ea864f089c7821" + }, + { + "op": "put", + "key": "abc/users", + "value": false + }, + { + "op": "delete", + "key": "ab/a/a", + "existed": true + }, + { + "op": "commit", + "root": "2a7d11f0717fe04916de94a4b052a14f4b87b838776417831428e780a0e9f147" + }, + { + "op": "delete", + "key": "a/u", + "existed": true + }, + { + "op": "delete", + "key": "ab/a/😀", + "existed": true + }, + { + "op": "commit", + "root": "b607517cee2f7a69a1ba6da7034e45bce869543ca6d788d07da657391414af70" + }, + { + "op": "put", + "key": "￿/😀/é", + "value": false + }, + { + "op": "put", + "key": "x", + "value": -0.48478091632419784 + }, + { + "op": "commit", + "root": "3fb232a0295f5f654ca483f3b4775d30d474525a0001be5765d83f31081efe2e" + }, + { + "op": "delete", + "key": "u/user/ab", + "existed": true + }, + { + "op": "delete", + "key": "￿/😀/é", + "existed": true + }, + { + "op": "put", + "key": "abc/ab/users", + "value": [ + "1", + [ + -4569361133236593, + 4.669460788724153e-09 + ], + [ + -459675.57609392283 + ] + ] + }, + { + "op": "put", + "key": "abc", + "value": 4664229710873395 + }, + { + "op": "put", + "key": "😀/￿/user", + "value": null + }, + { + "op": "put", + "key": "a", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "user", + "value": "c
中" + }, + { + "op": "delete", + "key": "abc/users", + "existed": true + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "put", + "key": "é/abc", + "value": null + }, + { + "op": "delete", + "key": "😀/u/user", + "existed": true + }, + { + "op": "put", + "key": "abc/ab", + "value": [ + null, + true, + -4488798683016691 + ] + }, + { + "op": "commit", + "root": "24c389e8f1d74be4b547edad67f0e4f202287d798544ce6802c71f79fb3ec22b" + }, + { + "op": "put", + "key": "u/u", + "value": true + }, + { + "op": "put", + "key": "users/u", + "value": -1813558849521113 + }, + { + "op": "put", + "key": "a/u/abc", + "value": "" + }, + { + "op": "put", + "key": "abc", + "value": 7696377839440766 + }, + { + "op": "commit", + "root": "5995016dd55223792bef2364e3c53d95be7523770da42c7bd04d0277fd376937" + }, + { + "op": "put", + "key": "￿/😀/x", + "value": { + " 1
 中y": null, + "xz10\n9中a": "9é9\u0001y\\x\"", + "
": [ + "é😀a￿ \tyy", + null + ], + "0\u0001": -18446744073709551616 + } + }, + { + "op": "put", + "key": "😀/é", + "value": false + }, + { + "op": "put", + "key": "x/users/￿", + "value": null + }, + { + "op": "commit", + "root": "c40fe5a1e014e3a4f35af9c6c8141f09a6b9fcae2ebaee3ca887be737d9938e5" + }, + { + "op": "delete", + "key": "x/x/é", + "existed": true + }, + { + "op": "commit", + "root": "edc11c4e2e58ca6f5af2882b8a17ed8ab5d774b8922c1f70ca35e93fb7af4a75" + }, + { + "op": "put", + "key": "é", + "value": { + "\n\\￿\tb": true, + "": null + } + }, + { + "op": "delete", + "key": "é/abc", + "existed": true + }, + { + "op": "put", + "key": "￿/ab", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "😀/é", + "existed": true + }, + { + "op": "delete", + "key": "user/users", + "existed": true + }, + { + "op": "commit", + "root": "1286a718dbae402bceba24143d2025ec6962ace780e6bbc5739dff5e82316179" + }, + { + "op": "put", + "key": "abc", + "value": { + "
b￿\n": { + " /\t1
": "yb/xéz中
", + "/x😀a\u00010\\": 1152921504606847829, + "1x1//1": -18446744073709551616 + }, + "\u0001
0\n\t": { + "😀": 0.6794943530144515, + "a": 1152921504606847052, + "\\é yyéx": -640753205298490, + "
1中\\a中": false + } + } + }, + { + "op": "commit", + "root": "bb3475c9b412b933bce07868c3db24b005b0564c0689f8a4b79f60bb50947ba2" + }, + { + "op": "delete", + "key": "abc/￿/x", + "existed": true + }, + { + "op": "put", + "key": "user/user/é", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": 400611.7210392563 + }, + { + "op": "put", + "key": "x", + "value": null + }, + { + "op": "put", + "key": "😀/users/￿", + "value": -0.9582978493681851 + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "commit", + "root": "1ac630e72a7075785fcfa208b3f3955d5e6fc06590c60eec3d64346b0055e841" + }, + { + "op": "delete", + "key": "￿/ab", + "existed": true + }, + { + "op": "delete", + "key": "user/é/é", + "existed": false + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": "￿c" + }, + { + "op": "delete", + "key": "abc/ab", + "existed": true + }, + { + "op": "put", + "key": "ab/x", + "value": {} + }, + { + "op": "put", + "key": "ab", + "value": 5.838011418028465 + }, + { + "op": "put", + "key": "user/￿/😀", + "value": { + "
": true + } + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "x/user/￿", + "value": [] + }, + { + "op": "commit", + "root": "85a8506710498b6467bd3dc081d51b2bf471ced17d2853bfe8df425a691e3a32" + }, + { + "op": "delete", + "key": "u/u", + "existed": true + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "😀", + "value": [ + false, + "\ta " + ] + }, + { + "op": "delete", + "key": "ab/￿", + "existed": true + }, + { + "op": "delete", + "key": "users/u", + "existed": true + }, + { + "op": "delete", + "key": "x/users/￿", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": false + }, + { + "op": "delete", + "key": "user/user/é", + "existed": true + }, + { + "op": "put", + "key": "a/é/￿", + "value": 3895616142526731 + }, + { + "op": "put", + "key": "u/a/x", + "value": { + "￿19b
z中é": 0.9355034421934179, + "\u0001xyz": [], + "b\u00010\\中/1": true + } + }, + { + "op": "delete", + "key": "￿/😀/x", + "existed": true + }, + { + "op": "put", + "key": "￿", + "value": -0.23039267179624923 + }, + { + "op": "delete", + "key": "ab", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": -6346746654702813 + }, + { + "op": "commit", + "root": "a3b3e80d408a76fbec1bc0ca819ba7d65d4ae355efc6265ab4cc9cc100f9ca76" + }, + { + "op": "commit", + "root": "a3b3e80d408a76fbec1bc0ca819ba7d65d4ae355efc6265ab4cc9cc100f9ca76" + }, + { + "op": "put", + "key": "x", + "value": {} + }, + { + "op": "commit", + "root": "822c6fad39d7e117738572efa3e7d5c2e671880d51066fe348b40d855041ff3f" + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "users/￿", + "value": {} + }, + { + "op": "put", + "key": "user/x", + "value": [ + false, + -18446744073709551616 + ] + }, + { + "op": "commit", + "root": "be9aa988aa46937e0484880119f0dac43d42add57d593e9eb202e1e185be8c97" + }, + { + "op": "delete", + "key": "abc/ab/users", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "x/user/￿", + "existed": true + }, + { + "op": "put", + "key": "u/￿/😀", + "value": 1152921504606847488 + }, + { + "op": "commit", + "root": "7559fb5723a11392d9d261b8dd6500b2ea1903d3283faacf13d7a4f85c58d90e" + }, + { + "op": "commit", + "root": "7559fb5723a11392d9d261b8dd6500b2ea1903d3283faacf13d7a4f85c58d90e" + }, + { + "op": "put", + "key": "x", + "value": -7063685366348132 + }, + { + "op": "put", + "key": "abc/abc", + "value": "bc
19" + }, + { + "op": "delete", + "key": "user/x", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": "cy 中
9" + }, + { + "op": "put", + "key": "😀", + "value": null + }, + { + "op": "delete", + "key": "u/a/x", + "existed": true + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "user/users/😀", + "value": 1152921504606847794 + }, + { + "op": "put", + "key": "user/￿/users", + "value": [ + 7263133179168950, + -534321.9471758215, + "0b\"\t/￿\"" + ] + }, + { + "op": "put", + "key": "users", + "value": {} + }, + { + "op": "put", + "key": "users", + "value": 8.353777129648925e-10 + }, + { + "op": "put", + "key": "😀", + "value": [ + 1152921504606847377, + -0.6249474185050428 + ] + }, + { + "op": "delete", + "key": "x/users/u", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": { + "/c
\n0": [ + "é\u0001￿😀\t￿\u0001z", + 5753815969464206, + "😀\\￿中" + ], + "9 a￿": null, + "\t\"1é\t0\u0001": 1152921504606847688, + "\nb
x\"a": "b \\a// 😀" + } + }, + { + "op": "commit", + "root": "7d0e8c1eaad8ea25881dd3265c51175d05e7c702dcf65f14b89988c9826829f0" + }, + { + "op": "delete", + "key": "😀/￿/user", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": 3436522652239250 + }, + { + "op": "put", + "key": "a/users", + "value": 4795755137747556 + }, + { + "op": "delete", + "key": "😀/abc/é", + "existed": true + }, + { + "op": "put", + "key": "￿/😀", + "value": false + }, + { + "op": "put", + "key": "x", + "value": 45.521129922139956 + }, + { + "op": "put", + "key": "abc/users/ab", + "value": { + "ya": -18446744073709551616, + "\u0001/￿x😀
 中": "a c
\u0001x", + "1￿
9\n\"1b": false + } + }, + { + "op": "put", + "key": "ab", + "value": null + }, + { + "op": "commit", + "root": "ddd3830da592611a35f2e9660ddcbefca96b69383eb23f523f6b79fa3cae4a49" + }, + { + "op": "put", + "key": "users", + "value": {} + }, + { + "op": "put", + "key": "￿", + "value": 1152921504606847316 + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "￿/ab/a", + "value": -1955416100330730 + }, + { + "op": "commit", + "root": "9f20c1f12a971b293b357d3d304772f04c9d0a6372c1c9f219db7a9eea90b532" + }, + { + "op": "put", + "key": "users/abc", + "value": 1152921504606847392 + }, + { + "op": "put", + "key": "u", + "value": -4955888810519122 + }, + { + "op": "put", + "key": "users", + "value": 1152921504606847875 + }, + { + "op": "put", + "key": "users", + "value": "b😀c\t0" + }, + { + "op": "delete", + "key": "ab", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": false + }, + { + "op": "delete", + "key": "u/😀/ab", + "existed": true + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "abc/users/ab", + "existed": true + }, + { + "op": "put", + "key": "x/é/é", + "value": true + }, + { + "op": "put", + "key": "user/x", + "value": true + }, + { + "op": "delete", + "key": "user/users/😀", + "existed": true + }, + { + "op": "put", + "key": "abc/é", + "value": { + "/\\\u0001éa": -18446744073709551616, + " 😀": 874222861798044 + } + }, + { + "op": "delete", + "key": "users/￿", + "existed": true + }, + { + "op": "delete", + "key": "abc/abc", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "7062cc2bc12b1a294f7c53e3f7e3b405b5a2c95fb5a4d27b8a08cae008619e0a" + }, + { + "op": "put", + "key": "abc/😀/u", + "value": -3317481339064278 + }, + { + "op": "put", + "key": "￿", + "value": null + }, + { + "op": "commit", + "root": "80aefb3d3de442cf84c368a0135f20bbec8ce6d0672d1dd7d92c4cf9d686305e" + }, + { + "op": "delete", + "key": "user/￿/😀", + "existed": true + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "commit", + "root": "7aded1871f13105d8b0e6ac93c63159f4f23efab4ccf15eb6d56fb105224d819" + }, + { + "op": "delete", + "key": "😀/users/￿", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": null + }, + { + "op": "commit", + "root": "c696f7d2ad7b9352684b6416758f0a1d81a3bca30b8682c85aa535dc6111b2d4" + }, + { + "op": "commit", + "root": "c696f7d2ad7b9352684b6416758f0a1d81a3bca30b8682c85aa535dc6111b2d4" + }, + { + "op": "delete", + "key": "a/users", + "existed": true + }, + { + "op": "put", + "key": "x/é", + "value": -280519.6378761772 + }, + { + "op": "put", + "key": "abc", + "value": [ + -4898538352726972, + true + ] + }, + { + "op": "put", + "key": "x/￿/é", + "value": { + "\u0001zé\"0\nc9": -542121.4288026612, + "\"zb\"c￿1": -825844.6389250358, + "": -0.03927420747133792, + "中\"y\u0001cé/": "😀c\u0001/\n" + } + }, + { + "op": "commit", + "root": "d33f7f69a6993836d8f70662b92f9d42a4c6d00d2c3a142c64bccde85becd468" + }, + { + "op": "put", + "key": "abc/users/user", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "😀/é", + "value": null + }, + { + "op": "put", + "key": "u", + "value": { + "9\"
": { + "￿9z/": true, + "\"é😀0\\\t": false, + "b
1": null, + "b": "\u0001😀\u0001/éz😀\u0001" + }, + "a\\中中\tz": -18446744073709551616, + "\t1
中 ": true + } + }, + { + "op": "commit", + "root": "464186378824ab72aeb5aeec65dfc188d05609cee51db80404f29703ef37a9f0" + }, + { + "op": "put", + "key": "users", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "67b8bdd634743e1af936ad1b08f8501ffbb3f64bc6784fa62cb541ff0de5da89" + }, + { + "op": "put", + "key": "x", + "value": 0.04996968708111881 + }, + { + "op": "put", + "key": "u/abc", + "value": "z/" + }, + { + "op": "put", + "key": "x/￿/ab", + "value": -0.14518588083877848 + }, + { + "op": "put", + "key": "x/a", + "value": "a/" + }, + { + "op": "put", + "key": "￿/user/x", + "value": { + "\t\"0bcxx中": { + "\\z\u0001\té😀b": "😀\t\u0001" + }, + " \"z￿9z": null, + "bc中1\t/\t/": true + } + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": [ + { + "/y": -6535351210100194, + "10\t": 1152921504606847874, + "x": -0.6965929718411152, + "a\\\u0001\t ": -18446744073709551616 + }, + -18446744073709551616, + -191842873337320 + ] + }, + { + "op": "put", + "key": "u", + "value": [] + }, + { + "op": "put", + "key": "￿/abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a/user/a", + "value": [ + -0.35811442277877137 + ] + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a/abc", + "value": { + "a\\": 1152921504606847594, + "c￿/": { + "x\na\"": null + }, + "yx中z/y": { + "中
é90": true, + "zyx\t😀éy\n": false, + "￿ya\"z": true + } + } + }, + { + "op": "put", + "key": "u/user/￿", + "value": { + "0\"": 7091232726655776, + "c\\\u0001aé": { + "0a ": null + }, + "é0cx": false, + "x\t😀é
": { + "\n\"": -18446744073709551616, + "\n\"中00c": true + } + } + }, + { + "op": "put", + "key": "x/😀", + "value": true + }, + { + "op": "delete", + "key": "a/abc", + "existed": true + }, + { + "op": "put", + "key": "é/user", + "value": 9151900423.560808 + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "ab/x", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": { + "": 1152921504606847109 + } + }, + { + "op": "put", + "key": "😀", + "value": false + }, + { + "op": "commit", + "root": "b130b6af519d5e2217072a1f6add8b3f5207d9cae57a758f0ea932e91636d594" + }, + { + "op": "put", + "key": "￿", + "value": [] + }, + { + "op": "put", + "key": "é/x/￿", + "value": { + "\\\u0001/\txc0😀": 1152921504606847082 + } + }, + { + "op": "put", + "key": "u", + "value": true + }, + { + "op": "put", + "key": "ab", + "value": { + "": { + "\\
b￿": null, + "￿\u0001\t1": 8931341152562429, + "\u0001\t中b9": "\t\t", + "😀😀z/": "\n中￿\\\u0001
" + }, + " ac": 8515762626616835, + "\"": "中 yx", + "y\tb\t\u0001😀/中": 1152921504606847104 + } + }, + { + "op": "put", + "key": "u/😀/ab", + "value": null + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "delete", + "key": "abc/😀/u", + "existed": true + }, + { + "op": "put", + "key": "x/😀/u", + "value": { + "😀b0zb": false, + "中": [ + true, + true + ], + "cé\"": { + " 😀": true, + "ba\n￿": -457337379954808 + }, + "\ncz0b
": { + "\"éyz1": false, + "": 6623239290182256, + "/019yé\\": null + } + } + }, + { + "op": "put", + "key": "users/😀/ab", + "value": "0y
\\" + }, + { + "op": "delete", + "key": "x/😀/u", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": {} + }, + { + "op": "commit", + "root": "b0bffe20bba991325eca20de5b47d8114a0b108db726b177111ad7a53255602a" + }, + { + "op": "delete", + "key": "😀/a/a", + "existed": true + }, + { + "op": "put", + "key": "x/u", + "value": -2549254231391525 + }, + { + "op": "delete", + "key": "é/é/￿", + "existed": false + }, + { + "op": "commit", + "root": "e2785c18856dc1e4e87e0daf1fa40dde38399a23c189ef23f891cf610828db01" + }, + { + "op": "delete", + "key": "x/😀", + "existed": true + }, + { + "op": "commit", + "root": "36e2cb997f04a51f2e8d4cb15af27ba40119489481efb2f4a0f9c3020827b162" + }, + { + "op": "put", + "key": "ab", + "value": { + "
x0￿9\u0001": { + "x/y中中9a中": 1152921504606847726, + "\t9x y": null + }, + "\\a\u0001😀y中": { + "éa\\": null, + "￿c1b\tc": -8294236355241357, + "bccb😀": -3460883124835219 + }, + "中": { + "/z
\"é": true, + "1
0": 8872401519835205, + "中/ 😀": null, + "91y1zc9": -0.6700163064224591 + }, + "0zb\t0\\": false + } + }, + { + "op": "put", + "key": "ab/￿/😀", + "value": 1152921504606847061 + }, + { + "op": "put", + "key": "😀/￿/😀", + "value": null + }, + { + "op": "delete", + "key": "é/user", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": "é￿ " + }, + { + "op": "commit", + "root": "2d063f02bcce304a1b74d05eb9b29d54aec6e889865d4c13e43d4be6eca394db" + }, + { + "op": "put", + "key": "users/users", + "value": null + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": true + }, + { + "op": "put", + "key": "x/users/😀", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": null + }, + { + "op": "put", + "key": "user", + "value": 1152921504606847911 + }, + { + "op": "put", + "key": "users/￿", + "value": "9中\\\\é" + }, + { + "op": "put", + "key": "user", + "value": { + "y￿0éaé": null, + "y1/\\b": { + "écby中\n中": 29584211.613887373, + " éa😀90": "中z1c0\u0001zb", + " ayxb😀": -0.649562911066861, + "\\": true + }, + "é\u0001\u0001é\"": { + "\t9\\": 8884777165940577, + "\\￿x": false, + "ca\n😀\u0001": null, + "x

1\t\"é": null + }, + "b9😀é￿": 1152921504606847108 + } + }, + { + "op": "put", + "key": "a/😀", + "value": {} + }, + { + "op": "commit", + "root": "2b79cf1c6588589731d30522b2b76bf112caddd905d7bb5b83d244f5d4a99bc9" + }, + { + "op": "put", + "key": "x", + "value": true + }, + { + "op": "put", + "key": "ab", + "value": true + }, + { + "op": "put", + "key": "😀/u", + "value": { + "b": null, + "": "", + " z中\u0001": -18446744073709551616, + "11\"ba": { + "\\yz": true, + "": true, + "中￿ézb\n1": 344532283821597, + "0b￿\\\n\\é": -0.3260019933467111 + } + } + }, + { + "op": "delete", + "key": "u/abc", + "existed": true + }, + { + "op": "commit", + "root": "90d007c4a4cdd9f04ab336caeb117c93a17c2afbaa587397c47968a67fecf5f2" + }, + { + "op": "put", + "key": "￿/x/user", + "value": "\u0001 \"\t\\中" + }, + { + "op": "put", + "key": "￿/￿/a", + "value": "" + }, + { + "op": "commit", + "root": "dc4b6ef34887aac83efee7f922bb10c775a3f41c9278686ebfbbda01d9ae1e42" + }, + { + "op": "put", + "key": "user/ab", + "value": "az\u0001x1yx" + }, + { + "op": "put", + "key": "u", + "value": { + "ab9bé\u0001
": -0.12433582379621178, + " c1\t中\"": { + "😀9zyb/": -2225304300974304, + "
": "😀zy
x", + "0\u00011\"￿é
z": true + }, + "
\\": { + "b中ca\"": true, + "\té😀9\n￿": -324841581102270, + "c😀￿by": "zcacaz9" + }, + "\t\\中": [ + -18446744073709551616, + "x\\\t1c/" + ] + } + }, + { + "op": "put", + "key": "é/users/ab", + "value": 1152921504606847765 + }, + { + "op": "delete", + "key": "￿/x/user", + "existed": true + }, + { + "op": "put", + "key": "é/users/x", + "value": {} + }, + { + "op": "put", + "key": "users/users/￿", + "value": { + "\\b": 7927229420658014, + "é￿\n
": 1265880572805915 + } + }, + { + "op": "put", + "key": "ab/users/é", + "value": [ + { + "z\t\\😀中": 1152921504606847104 + }, + { + "\u0001b
 z
": -4068212830225738, + "9": null + }, + false, + true + ] + }, + { + "op": "put", + "key": "users", + "value": 1.8686463799365743e-10 + }, + { + "op": "delete", + "key": "x/a", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": 0.1482451034984853 + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "delete", + "key": "abc/é", + "existed": true + }, + { + "op": "put", + "key": "users/users/x", + "value": "\t" + }, + { + "op": "put", + "key": "ab", + "value": [ + "\\x
1 
y", + [ + 1152921504606847937, + 3772187621166608, + -811193.049054771, + 4674140977728952 + ] + ] + }, + { + "op": "delete", + "key": "￿/￿/a", + "existed": true + }, + { + "op": "put", + "key": "users/a/😀", + "value": 1152921504606847632 + }, + { + "op": "put", + "key": "😀/u/ab", + "value": null + }, + { + "op": "put", + "key": "a/a", + "value": -0.515913436172214 + }, + { + "op": "delete", + "key": "x/é", + "existed": true + }, + { + "op": "put", + "key": "u/a", + "value": true + }, + { + "op": "put", + "key": "a/user/a", + "value": "/\"9😀y" + }, + { + "op": "put", + "key": "x/￿", + "value": null + }, + { + "op": "put", + "key": "a", + "value": "c\n\"" + }, + { + "op": "delete", + "key": "u/a", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "é/x/￿", + "existed": true + }, + { + "op": "delete", + "key": "users/￿", + "existed": true + }, + { + "op": "delete", + "key": "users/users", + "existed": true + }, + { + "op": "delete", + "key": "a/x", + "existed": false + }, + { + "op": "put", + "key": "users/u", + "value": [] + }, + { + "op": "put", + "key": "😀/ab/é", + "value": { + "": true, + "\u0001\\\u0001": true, + "\u00010": "b y9
é ", + "y\n/é/09": null + } + }, + { + "op": "put", + "key": "ab/😀/ab", + "value": " 9" + }, + { + "op": "delete", + "key": "ab/😀/ab", + "existed": true + }, + { + "op": "put", + "key": "😀/user", + "value": {} + }, + { + "op": "put", + "key": "users/￿", + "value": "" + }, + { + "op": "put", + "key": "x", + "value": 1152921504606847108 + }, + { + "op": "put", + "key": "é/a/￿", + "value": true + }, + { + "op": "put", + "key": "ab", + "value": 704189.2676369569 + }, + { + "op": "put", + "key": "u/é/ab", + "value": {} + }, + { + "op": "commit", + "root": "341a3436bea484f671cf413741497a948aeef15d50681c230bc047ed49b805b0" + }, + { + "op": "put", + "key": "x/a", + "value": [] + }, + { + "op": "put", + "key": "user/a/￿", + "value": {} + }, + { + "op": "delete", + "key": "users/users/x", + "existed": true + }, + { + "op": "put", + "key": "users/ab", + "value": true + }, + { + "op": "put", + "key": "ab/u", + "value": "" + }, + { + "op": "put", + "key": "a/user/users", + "value": { + "": [ + null, + -18446744073709551616 + ], + "9": 3545523840262856, + "cz😀": { + "": -18446744073709551616, + "b\\": 1152921504606847400, + "1c": "中中/90é￿" + }, + "\\1 ￿": "
9" + } + }, + { + "op": "put", + "key": "a/a/￿", + "value": null + }, + { + "op": "put", + "key": "abc/a/user", + "value": null + }, + { + "op": "delete", + "key": "u/user/￿", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": -52867956797762 + }, + { + "op": "commit", + "root": "8d5f4cd8ed87980e2abfcfad449cfff7f2e36ae63b1fd3b198998bf8ae43aec6" + }, + { + "op": "put", + "key": "abc/x", + "value": -694680.7535715397 + }, + { + "op": "put", + "key": "😀", + "value": "" + }, + { + "op": "put", + "key": "x/😀/é", + "value": { + "￿\u00010\n\\": "c", + "/\"": { + "\\": null, + "￿0x1\u0001\\": false, + "￿x\"": -1090520089078268 + }, + "\\
": { + "bé": false, + "x9": "", + "": 5979433220707924, + "b y": true + } + } + }, + { + "op": "put", + "key": "u", + "value": 47507468532721.555 + }, + { + "op": "delete", + "key": "a/user/a", + "existed": true + }, + { + "op": "delete", + "key": "a/user/users", + "existed": true + }, + { + "op": "delete", + "key": "abc/users/user", + "existed": true + }, + { + "op": "delete", + "key": "ab/￿/😀", + "existed": true + }, + { + "op": "put", + "key": "abc/u/abc", + "value": [ + 1205982392274335, + { + "😀中1y中😀x\n": -18446744073709551616, + "\t中\\": 1210956020351507, + "bb": 2144588251547270 + } + ] + }, + { + "op": "put", + "key": "x/abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x/a", + "value": -5037139323945801 + }, + { + "op": "put", + "key": "a", + "value": { + " \u0001b": { + "": -3613743620030602 + }, + "\na\t": 1152921504606847405, + "z": [], + "zaé\u0001\u0001\"": [ + null, + -18446744073709551616, + 1.0730825888967211e-10 + ] + } + }, + { + "op": "commit", + "root": "38b8c9da21faa9a5ca1e2f2174188eafe7b71daf820ba2bf069943a48a392118" + }, + { + "op": "delete", + "key": "😀/user/x", + "existed": true + }, + { + "op": "commit", + "root": "28c3cf1a70516f3366dfa08c36851aef9ecf03c5c9bec5545e831013fb99b761" + }, + { + "op": "put", + "key": "user/user", + "value": "\\céxc\\\u0001\"" + }, + { + "op": "put", + "key": "😀", + "value": 9791490748.585184 + }, + { + "op": "delete", + "key": "x/￿/a", + "existed": false + }, + { + "op": "delete", + "key": "users/users/x", + "existed": false + }, + { + "op": "put", + "key": "user/😀/￿", + "value": true + }, + { + "op": "put", + "key": "user/abc/abc", + "value": { + "\tc\\\"a ": 168879.26095807785 + } + }, + { + "op": "put", + "key": "ab/user", + "value": "//0" + }, + { + "op": "commit", + "root": "08dafe1afdc021b350dfbdd83eeac878de19b5a82fa45f51a20e054387fdacde" + }, + { + "op": "put", + "key": "user", + "value": { + "": "9c
1ay\n\"", + "\u0001b\\x": "11é😀\n😀" + } + }, + { + "op": "put", + "key": "user", + "value": { + "c￿1中
": [ + true + ] + } + }, + { + "op": "delete", + "key": "user/abc/abc", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": -8405007150166980 + }, + { + "op": "put", + "key": "users/user", + "value": null + }, + { + "op": "put", + "key": "é/é/ab", + "value": -1331536963664203 + }, + { + "op": "delete", + "key": "user/x", + "existed": true + }, + { + "op": "put", + "key": "users/ab", + "value": 390423.53405178036 + }, + { + "op": "put", + "key": "a/user/é", + "value": { + "z0zc": 1152921504606847331 + } + }, + { + "op": "put", + "key": "a/a/abc", + "value": [ + {}, + { + "zz￿x\"": false, + "￿c\t\n0cc": -18446744073709551616, + "\\\"😀/\n": "0", + "\"é\u0001\"x\t": -4949069302991004 + }, + "y09é￿b", + 1442236461640640 + ] + }, + { + "op": "put", + "key": "ab", + "value": false + }, + { + "op": "delete", + "key": "x/a", + "existed": true + }, + { + "op": "commit", + "root": "f80026d2ae4eec9ae1047f2ce815aca89f40d3e9b9f931f56560ac243f94c84d" + } + ], + "items": [ + [ + "a", + { + "\na\t": 1152921504606847405, + " \u0001b": { + "": -3613743620030602 + }, + "z": [], + "zaé\u0001\u0001\"": [ + null, + -18446744073709551616, + 1.0730825888967211e-10 + ] + } + ], + [ + "a/a", + -0.515913436172214 + ], + [ + "a/a/abc", + [ + {}, + { + "\"é\u0001\"x\t": -4949069302991004, + "\\\"😀/\n": "0", + "zz￿x\"": false, + "￿c\t\n0cc": -18446744073709551616 + }, + "y09é￿b", + 1442236461640640 + ] + ], + [ + "a/a/￿", + null + ], + [ + "a/u/abc", + "" + ], + [ + "a/user/é", + { + "z0zc": 1152921504606847331 + } + ], + [ + "a/é/￿", + 3895616142526731 + ], + [ + "a/😀", + {} + ], + [ + "ab", + false + ], + [ + "ab/u", + "" + ], + [ + "ab/user", + "//0" + ], + [ + "ab/users/é", + [ + { + "z\t\\😀中": 1152921504606847104 + }, + { + "\u0001b
 z
": -4068212830225738, + "9": null + }, + false, + true + ] + ], + [ + "abc", + -18446744073709551616 + ], + [ + "abc/a/user", + null + ], + [ + "abc/u/abc", + [ + 1205982392274335, + { + "\t中\\": 1210956020351507, + "bb": 2144588251547270, + "😀中1y中😀x\n": -18446744073709551616 + } + ] + ], + [ + "abc/x", + -694680.7535715397 + ], + [ + "u", + -8405007150166980 + ], + [ + "u/é/ab", + {} + ], + [ + "u/￿/😀", + 1152921504606847488 + ], + [ + "u/😀/ab", + null + ], + [ + "user", + { + "c￿1中
": [ + true + ] + } + ], + [ + "user/a/￿", + {} + ], + [ + "user/ab", + "az\u0001x1yx" + ], + [ + "user/user", + "\\céxc\\\u0001\"" + ], + [ + "user/￿/users", + [ + 7263133179168950, + -534321.9471758215, + "0b\"\t/￿\"" + ] + ], + [ + "user/😀/￿", + true + ], + [ + "users", + -52867956797762 + ], + [ + "users/a/😀", + 1152921504606847632 + ], + [ + "users/ab", + 390423.53405178036 + ], + [ + "users/abc", + 1152921504606847392 + ], + [ + "users/u", + [] + ], + [ + "users/user", + null + ], + [ + "users/users/￿", + { + "\\b": 7927229420658014, + "é￿\n
": 1265880572805915 + } + ], + [ + "users/￿", + "" + ], + [ + "users/😀/ab", + "0y
\\" + ], + [ + "x", + 1152921504606847108 + ], + [ + "x/abc", + -18446744073709551616 + ], + [ + "x/u", + -2549254231391525 + ], + [ + "x/users/😀", + null + ], + [ + "x/é/é", + true + ], + [ + "x/￿", + null + ], + [ + "x/￿/ab", + -0.14518588083877848 + ], + [ + "x/￿/é", + { + "": -0.03927420747133792, + "\u0001zé\"0\nc9": -542121.4288026612, + "\"zb\"c￿1": -825844.6389250358, + "中\"y\u0001cé/": "😀c\u0001/\n" + } + ], + [ + "x/😀/é", + { + "/\"": { + "\\": null, + "￿0x1\u0001\\": false, + "￿x\"": -1090520089078268 + }, + "\\
": { + "": 5979433220707924, + "b y": true, + "bé": false, + "x9": "" + }, + "￿\u00010\n\\": "c" + } + ], + [ + "é", + -18446744073709551616 + ], + [ + "é/a/￿", + true + ], + [ + "é/users/ab", + 1152921504606847765 + ], + [ + "é/users/x", + {} + ], + [ + "é/é/ab", + -1331536963664203 + ], + [ + "￿", + null + ], + [ + "￿/ab/a", + -1955416100330730 + ], + [ + "￿/abc", + -18446744073709551616 + ], + [ + "￿/user/x", + { + "\t\"0bcxx中": { + "\\z\u0001\té😀b": "😀\t\u0001" + }, + " \"z￿9z": null, + "bc中1\t/\t/": true + } + ], + [ + "￿/😀", + false + ], + [ + "😀", + 9791490748.585184 + ], + [ + "😀/ab/é", + { + "": true, + "\u00010": "b y9
é ", + "\u0001\\\u0001": true, + "y\n/é/09": null + } + ], + [ + "😀/u", + { + "": "", + " z中\u0001": -18446744073709551616, + "11\"ba": { + "": true, + "0b￿\\\n\\é": -0.3260019933467111, + "\\yz": true, + "中￿ézb\n1": 344532283821597 + }, + "b": null + } + ], + [ + "😀/u/ab", + null + ], + [ + "😀/user", + {} + ], + [ + "😀/é", + null + ], + [ + "😀/￿/😀", + null + ] + ] + }, + { + "name": "random-1/swarm", + "addressing": "swarm", + "steps": [ + { + "op": "put", + "key": "ab", + "value": true + }, + { + "op": "put", + "key": "u", + "value": {} + }, + { + "op": "commit", + "root": "6f94e53b65e3e3e9ef3df69e192f66c38aa05378c5a95fe75da3c768d4d44e71" + }, + { + "op": "put", + "key": "😀", + "value": "😀x/" + }, + { + "op": "put", + "key": "￿", + "value": null + }, + { + "op": "commit", + "root": "99e156f059cbe809c23b477f0fe263d7a67727ea700c1311093bfd550d310665" + }, + { + "op": "put", + "key": "users/￿", + "value": 527401.990262979 + }, + { + "op": "delete", + "key": "users/￿", + "existed": true + }, + { + "op": "commit", + "root": "99e156f059cbe809c23b477f0fe263d7a67727ea700c1311093bfd550d310665" + }, + { + "op": "put", + "key": "u/user/ab", + "value": false + }, + { + "op": "commit", + "root": "dc7b46c23c17feddadd9a95017db90f4a3c95feb04635b9798eb1525c2745b7d" + }, + { + "op": "commit", + "root": "dc7b46c23c17feddadd9a95017db90f4a3c95feb04635b9798eb1525c2745b7d" + }, + { + "op": "put", + "key": "a/ab/ab", + "value": { + "😀b\u00011\\\n": 879078303060946, + "c\téxz": [ + 4193135724629501, + null, + -212800.62724417204 + ], + "中1\\é\"😀\"\t": "￿a\\éyé中0", + "b\u0001\"😀中0": [ + 639.8696662871351, + false, + false + ] + } + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "put", + "key": "a/ab", + "value": false + }, + { + "op": "put", + "key": "user", + "value": "z9
 \t/\u0001\u0001" + }, + { + "op": "put", + "key": "é/a", + "value": "9" + }, + { + "op": "commit", + "root": "a7739da65144a97d4eaf5d42decfa6c5426350fcab6c1fd6bc6c8a3432701c95" + }, + { + "op": "put", + "key": "é/users/a", + "value": null + }, + { + "op": "put", + "key": "user", + "value": { + "中1
é\t1": [], + "😀/
\nb ": -5185773715815691, + "": "c", + " z\n😀": "a中" + } + }, + { + "op": "commit", + "root": "68e9d99c02e2a547eb29d738255a62d709e387100ebc73bce8c26f4065fac57b" + }, + { + "op": "put", + "key": "😀/user/x", + "value": [] + }, + { + "op": "put", + "key": "x", + "value": { + "\u0001x\\": "\u0001a/￿\\ az", + "/😀y": 1152921504606847415, + "x\\中\"": [ + "cbc", + true, + "9/￿" + ], + "9\"//x 1￿": { + "x/b\nc\\yy": 2075006567467046 + } + } + }, + { + "op": "put", + "key": "x", + "value": [ + false + ] + }, + { + "op": "put", + "key": "u/😀/ab", + "value": false + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": 571024.2686891332 + }, + { + "op": "delete", + "key": "a/ab", + "existed": true + }, + { + "op": "put", + "key": "ab/￿/ab", + "value": { + "0": 1152921504606847016, + "": "\t\\/\\c" + } + }, + { + "op": "put", + "key": "😀/u/ab", + "value": -288346.0334079566 + }, + { + "op": "put", + "key": "😀", + "value": false + }, + { + "op": "delete", + "key": "users/abc", + "existed": false + }, + { + "op": "put", + "key": "ab/a/abc", + "value": false + }, + { + "op": "put", + "key": "a", + "value": null + }, + { + "op": "delete", + "key": "ab/￿/ab", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "delete", + "key": "😀/u/ab", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": [ + false, + 4975737563517114 + ] + }, + { + "op": "put", + "key": "abc", + "value": "é" + }, + { + "op": "delete", + "key": "ab", + "existed": true + }, + { + "op": "commit", + "root": "7c769d883dbf093b6f60d55dbbaae2c20dd446ea8b1370da221e3b484d905760" + }, + { + "op": "put", + "key": "abc", + "value": [ + -1213485891030941 + ] + }, + { + "op": "put", + "key": "a", + "value": "\t" + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x/users", + "value": null + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "é/x", + "value": { + "1": {} + } + }, + { + "op": "put", + "key": "😀/a/a", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "71e2102081945da53dad7940d53416ae183780e77822a4fd529978605fb368be" + }, + { + "op": "delete", + "key": "é/a", + "existed": true + }, + { + "op": "commit", + "root": "6836664a2d659d336f6d2cb8bca2a4364c1010590c844591726b32f0775c9300" + }, + { + "op": "commit", + "root": "6836664a2d659d336f6d2cb8bca2a4364c1010590c844591726b32f0775c9300" + }, + { + "op": "put", + "key": "u", + "value": [ + 4792331001286011, + -376395.9363293954 + ] + }, + { + "op": "delete", + "key": "é/users/a", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": [ + { + "9\n": false + }, + null, + { + "\\éz中b": "9", + "9": 1152921504606847910, + "￿c": -0.5180194084166361 + }, + -3144101130451274 + ] + }, + { + "op": "put", + "key": "a/u", + "value": { + "\nx 91\\中a": 0.1581652465616863, + "z y": [ + 1544518820599314 + ], + "\tz中\"": { + "0": "0 xax😀", + "": "
ycé", + "😀 \né\"": "ax\t\t\"" + }, + "中\\/😀": {} + } + }, + { + "op": "delete", + "key": "ab/a/abc", + "existed": true + }, + { + "op": "put", + "key": "x/￿/a", + "value": { + "\n z\t￿é0\"": [], + "😀\n\\/￿😀": true, + "

 ": null, + "
y
\\9z": false + } + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "é/x", + "existed": true + }, + { + "op": "put", + "key": "ab", + "value": { + "éb9éx😀\n": true + } + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "delete", + "key": "a/ab/ab", + "existed": true + }, + { + "op": "put", + "key": "ab/a/￿", + "value": 0.023037844265266116 + }, + { + "op": "delete", + "key": "x/users", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": [ + 5219989364590686, + "19￿1a", + { + "190c
z": "😀y￿9\téz", + "y\t": 4528391509020087 + } + ] + }, + { + "op": "put", + "key": "a", + "value": "x" + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "put", + "key": "users", + "value": { + "/9x￿zx1": { + "\t\\z11 \t": "0\t9/\u0001😀" + }, + "0": true, + "": { + "😀 0\\z
": false, + "": null + }, + "y中b😀\\9": -668528497962439 + } + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "commit", + "root": "8720bcf3deee27aab02f74cbdbdd4275ab9d09f29e2680d2ca5bd81dd6a8caf8" + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "put", + "key": "ab/a/😀", + "value": { + "9

1": 0.8703319994801906, + "
éb\"中\n中0": [ + -18446744073709551616, + 1152921504606847601, + 8582122809182276 + ] + } + }, + { + "op": "put", + "key": "a", + "value": {} + }, + { + "op": "put", + "key": "￿", + "value": { + "x/by中": null, + "y\\\taé9c": "c b\\b", + "/y9\\": false, + "x\n1é": [ + -2906383224920196 + ] + } + }, + { + "op": "put", + "key": "😀/u/user", + "value": { + "😀é中a z0\"": { + "x\n\"y😀": null, + "": 5385660561127926, + "/\n /\"9/é": "", + "xy///😀c\t": -826100539360698 + }, + "\\c😀by": null, + "\u0001😀91😀/\"
": -18446744073709551616, + "￿/中ézay": "😀yx" + } + }, + { + "op": "put", + "key": "x/users/u", + "value": [ + true, + false + ] + }, + { + "op": "put", + "key": "u", + "value": 8.756506606108011e-11 + }, + { + "op": "put", + "key": "ab/a/a", + "value": { + "\t\"中0\u0001c": "0a中\\é\u0001" + } + }, + { + "op": "put", + "key": "x/x/é", + "value": null + }, + { + "op": "put", + "key": "a", + "value": "" + }, + { + "op": "put", + "key": "abc/￿/x", + "value": [ + [ + "\n￿
😀 \t y", + "😀y中z9
a", + 0.13181702846660892 + ], + null + ] + }, + { + "op": "commit", + "root": "3043750bb9bd4b9117ce36974f97e193174d5c6307e0e8e8f2d731c7f30b2bf3" + }, + { + "op": "commit", + "root": "3043750bb9bd4b9117ce36974f97e193174d5c6307e0e8e8f2d731c7f30b2bf3" + }, + { + "op": "delete", + "key": "x/￿/a", + "existed": true + }, + { + "op": "put", + "key": "😀/abc/é", + "value": {} + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "commit", + "root": "9e291fe7e5bf6f73a0dca49a0804c349c0b21b5861fa0365d403c4c61b60f7d9" + }, + { + "op": "delete", + "key": "ab/a/￿", + "existed": true + }, + { + "op": "put", + "key": "ab/￿", + "value": "9\n/\u00010\u0001" + }, + { + "op": "commit", + "root": "03c9ace26ac8b4a62015ded599bfd8c5203b1ad3a2d72c82cc77df95ace92316" + }, + { + "op": "put", + "key": "user", + "value": 933029.5942664645 + }, + { + "op": "put", + "key": "user/users", + "value": true + }, + { + "op": "commit", + "root": "871c77a93e53a44fef69ac693dab4deb1c62e6ab74faf954318bc582217c3415" + }, + { + "op": "put", + "key": "abc/users", + "value": false + }, + { + "op": "delete", + "key": "ab/a/a", + "existed": true + }, + { + "op": "commit", + "root": "9a43a6b02e23edab1ef56db124bf796da20080ea232f9cf873c2cffe4b648644" + }, + { + "op": "delete", + "key": "a/u", + "existed": true + }, + { + "op": "delete", + "key": "ab/a/😀", + "existed": true + }, + { + "op": "commit", + "root": "2f9617115bfc1c752d47dadbd27406a2b3f5f17910e96aa44c1c543d964db9cb" + }, + { + "op": "put", + "key": "￿/😀/é", + "value": false + }, + { + "op": "put", + "key": "x", + "value": -0.48478091632419784 + }, + { + "op": "commit", + "root": "b1f4d90acc53bcdf8d280e7c75283ce6d09b7c1e5fef7fff50940e984e7f2775" + }, + { + "op": "delete", + "key": "u/user/ab", + "existed": true + }, + { + "op": "delete", + "key": "￿/😀/é", + "existed": true + }, + { + "op": "put", + "key": "abc/ab/users", + "value": [ + "1", + [ + -4569361133236593, + 4.669460788724153e-09 + ], + [ + -459675.57609392283 + ] + ] + }, + { + "op": "put", + "key": "abc", + "value": 4664229710873395 + }, + { + "op": "put", + "key": "😀/￿/user", + "value": null + }, + { + "op": "put", + "key": "a", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "user", + "value": "c
中" + }, + { + "op": "delete", + "key": "abc/users", + "existed": true + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "put", + "key": "é/abc", + "value": null + }, + { + "op": "delete", + "key": "😀/u/user", + "existed": true + }, + { + "op": "put", + "key": "abc/ab", + "value": [ + null, + true, + -4488798683016691 + ] + }, + { + "op": "commit", + "root": "1076255edf73b1701cd42797826dd914fb5aa837246b904fee5a870025b2d0f5" + }, + { + "op": "put", + "key": "u/u", + "value": true + }, + { + "op": "put", + "key": "users/u", + "value": -1813558849521113 + }, + { + "op": "put", + "key": "a/u/abc", + "value": "" + }, + { + "op": "put", + "key": "abc", + "value": 7696377839440766 + }, + { + "op": "commit", + "root": "c1a4855512935f759d0287f54944eedc1337ad7461b2fdac4d550715b3576a40" + }, + { + "op": "put", + "key": "￿/😀/x", + "value": { + " 1
 中y": null, + "xz10\n9中a": "9é9\u0001y\\x\"", + "
": [ + "é😀a￿ \tyy", + null + ], + "0\u0001": -18446744073709551616 + } + }, + { + "op": "put", + "key": "😀/é", + "value": false + }, + { + "op": "put", + "key": "x/users/￿", + "value": null + }, + { + "op": "commit", + "root": "17c1f0469c29cb3c0a6f52bf43bd6f19038c88a0e7da1ad0221815996d0e942d" + }, + { + "op": "delete", + "key": "x/x/é", + "existed": true + }, + { + "op": "commit", + "root": "c6aa001b7e3c6ea5940bb0eada6954b543e6b6cf78893fb21de32b474158302c" + }, + { + "op": "put", + "key": "é", + "value": { + "\n\\￿\tb": true, + "": null + } + }, + { + "op": "delete", + "key": "é/abc", + "existed": true + }, + { + "op": "put", + "key": "￿/ab", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "😀/é", + "existed": true + }, + { + "op": "delete", + "key": "user/users", + "existed": true + }, + { + "op": "commit", + "root": "e67a57f4f50b408ae3644e8457ff3bf8ed530474d65400037789cc28916669bd" + }, + { + "op": "put", + "key": "abc", + "value": { + "
b￿\n": { + " /\t1
": "yb/xéz中
", + "/x😀a\u00010\\": 1152921504606847829, + "1x1//1": -18446744073709551616 + }, + "\u0001
0\n\t": { + "😀": 0.6794943530144515, + "a": 1152921504606847052, + "\\é yyéx": -640753205298490, + "
1中\\a中": false + } + } + }, + { + "op": "commit", + "root": "e556807daddbc04909eed5e8b8a1fe4c7bf7710b3e0a9ba05976761ccaa67977" + }, + { + "op": "delete", + "key": "abc/￿/x", + "existed": true + }, + { + "op": "put", + "key": "user/user/é", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": 400611.7210392563 + }, + { + "op": "put", + "key": "x", + "value": null + }, + { + "op": "put", + "key": "😀/users/￿", + "value": -0.9582978493681851 + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "commit", + "root": "f2c5a9ae328cfd163dc6f4f26f883b8d86778f3a422162d5290eaaab19436146" + }, + { + "op": "delete", + "key": "￿/ab", + "existed": true + }, + { + "op": "delete", + "key": "user/é/é", + "existed": false + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": "￿c" + }, + { + "op": "delete", + "key": "abc/ab", + "existed": true + }, + { + "op": "put", + "key": "ab/x", + "value": {} + }, + { + "op": "put", + "key": "ab", + "value": 5.838011418028465 + }, + { + "op": "put", + "key": "user/￿/😀", + "value": { + "
": true + } + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "x/user/￿", + "value": [] + }, + { + "op": "commit", + "root": "d43827cdca63983d2b19827d20790519e25b7d5d8417ffc46b5aefd08e2ab173" + }, + { + "op": "delete", + "key": "u/u", + "existed": true + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "😀", + "value": [ + false, + "\ta " + ] + }, + { + "op": "delete", + "key": "ab/￿", + "existed": true + }, + { + "op": "delete", + "key": "users/u", + "existed": true + }, + { + "op": "delete", + "key": "x/users/￿", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": false + }, + { + "op": "delete", + "key": "user/user/é", + "existed": true + }, + { + "op": "put", + "key": "a/é/￿", + "value": 3895616142526731 + }, + { + "op": "put", + "key": "u/a/x", + "value": { + "￿19b
z中é": 0.9355034421934179, + "\u0001xyz": [], + "b\u00010\\中/1": true + } + }, + { + "op": "delete", + "key": "￿/😀/x", + "existed": true + }, + { + "op": "put", + "key": "￿", + "value": -0.23039267179624923 + }, + { + "op": "delete", + "key": "ab", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": -6346746654702813 + }, + { + "op": "commit", + "root": "7b8b3481dac255458e5d9fb3a03d9d59004983befbcf73e6752a5dd39f35b319" + }, + { + "op": "commit", + "root": "7b8b3481dac255458e5d9fb3a03d9d59004983befbcf73e6752a5dd39f35b319" + }, + { + "op": "put", + "key": "x", + "value": {} + }, + { + "op": "commit", + "root": "b7ab0fefee987df5413586fc99006055c3d4b0d4617fb0aac1eef16d08922d7a" + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "users/￿", + "value": {} + }, + { + "op": "put", + "key": "user/x", + "value": [ + false, + -18446744073709551616 + ] + }, + { + "op": "commit", + "root": "8da5ceab70e4e90742f398d4a538499f1b4aa8078a98eeea42a10e6f7707092c" + }, + { + "op": "delete", + "key": "abc/ab/users", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "x/user/￿", + "existed": true + }, + { + "op": "put", + "key": "u/￿/😀", + "value": 1152921504606847488 + }, + { + "op": "commit", + "root": "8a683b0870d647eaf4beea9889c447ec7dbabc9f5f1bd884e66bea992cd1de37" + }, + { + "op": "commit", + "root": "8a683b0870d647eaf4beea9889c447ec7dbabc9f5f1bd884e66bea992cd1de37" + }, + { + "op": "put", + "key": "x", + "value": -7063685366348132 + }, + { + "op": "put", + "key": "abc/abc", + "value": "bc
19" + }, + { + "op": "delete", + "key": "user/x", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": "cy 中
9" + }, + { + "op": "put", + "key": "😀", + "value": null + }, + { + "op": "delete", + "key": "u/a/x", + "existed": true + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "user/users/😀", + "value": 1152921504606847794 + }, + { + "op": "put", + "key": "user/￿/users", + "value": [ + 7263133179168950, + -534321.9471758215, + "0b\"\t/￿\"" + ] + }, + { + "op": "put", + "key": "users", + "value": {} + }, + { + "op": "put", + "key": "users", + "value": 8.353777129648925e-10 + }, + { + "op": "put", + "key": "😀", + "value": [ + 1152921504606847377, + -0.6249474185050428 + ] + }, + { + "op": "delete", + "key": "x/users/u", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": { + "/c
\n0": [ + "é\u0001￿😀\t￿\u0001z", + 5753815969464206, + "😀\\￿中" + ], + "9 a￿": null, + "\t\"1é\t0\u0001": 1152921504606847688, + "\nb
x\"a": "b \\a// 😀" + } + }, + { + "op": "commit", + "root": "5cbb7f79d48a4196f63435d4d4e73b10a66babc0663b4d55493c2fd04768806d" + }, + { + "op": "delete", + "key": "😀/￿/user", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": 3436522652239250 + }, + { + "op": "put", + "key": "a/users", + "value": 4795755137747556 + }, + { + "op": "delete", + "key": "😀/abc/é", + "existed": true + }, + { + "op": "put", + "key": "￿/😀", + "value": false + }, + { + "op": "put", + "key": "x", + "value": 45.521129922139956 + }, + { + "op": "put", + "key": "abc/users/ab", + "value": { + "ya": -18446744073709551616, + "\u0001/￿x😀
 中": "a c
\u0001x", + "1￿
9\n\"1b": false + } + }, + { + "op": "put", + "key": "ab", + "value": null + }, + { + "op": "commit", + "root": "a3bb2e8ee3404754d97615153fdcf492ba97032a8936dd5568a93bb337ba5c2d" + }, + { + "op": "put", + "key": "users", + "value": {} + }, + { + "op": "put", + "key": "￿", + "value": 1152921504606847316 + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "￿/ab/a", + "value": -1955416100330730 + }, + { + "op": "commit", + "root": "5fd028b781505a2a521a502d6551a56c0bf13d8e7a39d427273435378f38dda8" + }, + { + "op": "put", + "key": "users/abc", + "value": 1152921504606847392 + }, + { + "op": "put", + "key": "u", + "value": -4955888810519122 + }, + { + "op": "put", + "key": "users", + "value": 1152921504606847875 + }, + { + "op": "put", + "key": "users", + "value": "b😀c\t0" + }, + { + "op": "delete", + "key": "ab", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": false + }, + { + "op": "delete", + "key": "u/😀/ab", + "existed": true + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "abc/users/ab", + "existed": true + }, + { + "op": "put", + "key": "x/é/é", + "value": true + }, + { + "op": "put", + "key": "user/x", + "value": true + }, + { + "op": "delete", + "key": "user/users/😀", + "existed": true + }, + { + "op": "put", + "key": "abc/é", + "value": { + "/\\\u0001éa": -18446744073709551616, + " 😀": 874222861798044 + } + }, + { + "op": "delete", + "key": "users/￿", + "existed": true + }, + { + "op": "delete", + "key": "abc/abc", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "8c1a60ff0a9ca11090d82e7c86db5b077dccb5933b05a7966eb570be34e3c13d" + }, + { + "op": "put", + "key": "abc/😀/u", + "value": -3317481339064278 + }, + { + "op": "put", + "key": "￿", + "value": null + }, + { + "op": "commit", + "root": "bc790739caad7a7114c337b693d34cc9c07320ab8032ff292d8e91853489d680" + }, + { + "op": "delete", + "key": "user/￿/😀", + "existed": true + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "commit", + "root": "1cfc5051dd4466c94879282284b646402da8c65597f1492d35a09d4165162c84" + }, + { + "op": "delete", + "key": "😀/users/￿", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": null + }, + { + "op": "commit", + "root": "8e63c6497de8035133cc32cde571dfd4df92ef8de8fd12d40b8921f079c62dec" + }, + { + "op": "commit", + "root": "8e63c6497de8035133cc32cde571dfd4df92ef8de8fd12d40b8921f079c62dec" + }, + { + "op": "delete", + "key": "a/users", + "existed": true + }, + { + "op": "put", + "key": "x/é", + "value": -280519.6378761772 + }, + { + "op": "put", + "key": "abc", + "value": [ + -4898538352726972, + true + ] + }, + { + "op": "put", + "key": "x/￿/é", + "value": { + "\u0001zé\"0\nc9": -542121.4288026612, + "\"zb\"c￿1": -825844.6389250358, + "": -0.03927420747133792, + "中\"y\u0001cé/": "😀c\u0001/\n" + } + }, + { + "op": "commit", + "root": "7ff1e39d071e2a5efdd0064bb6aaa741d08eb9232f3b789b5a3f8bf38ffead01" + }, + { + "op": "put", + "key": "abc/users/user", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "😀/é", + "value": null + }, + { + "op": "put", + "key": "u", + "value": { + "9\"
": { + "￿9z/": true, + "\"é😀0\\\t": false, + "b
1": null, + "b": "\u0001😀\u0001/éz😀\u0001" + }, + "a\\中中\tz": -18446744073709551616, + "\t1
中 ": true + } + }, + { + "op": "commit", + "root": "e5c8ad8c347cc2578fe69690270e3f2c370ed6e2bb29b460c5e67fc360fbe758" + }, + { + "op": "put", + "key": "users", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "9709292821d7f2127ac977d6af17ba8299d50c7b4d220b0273802694d624b5c0" + }, + { + "op": "put", + "key": "x", + "value": 0.04996968708111881 + }, + { + "op": "put", + "key": "u/abc", + "value": "z/" + }, + { + "op": "put", + "key": "x/￿/ab", + "value": -0.14518588083877848 + }, + { + "op": "put", + "key": "x/a", + "value": "a/" + }, + { + "op": "put", + "key": "￿/user/x", + "value": { + "\t\"0bcxx中": { + "\\z\u0001\té😀b": "😀\t\u0001" + }, + " \"z￿9z": null, + "bc中1\t/\t/": true + } + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": [ + { + "/y": -6535351210100194, + "10\t": 1152921504606847874, + "x": -0.6965929718411152, + "a\\\u0001\t ": -18446744073709551616 + }, + -18446744073709551616, + -191842873337320 + ] + }, + { + "op": "put", + "key": "u", + "value": [] + }, + { + "op": "put", + "key": "￿/abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a/user/a", + "value": [ + -0.35811442277877137 + ] + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a/abc", + "value": { + "a\\": 1152921504606847594, + "c￿/": { + "x\na\"": null + }, + "yx中z/y": { + "中
é90": true, + "zyx\t😀éy\n": false, + "￿ya\"z": true + } + } + }, + { + "op": "put", + "key": "u/user/￿", + "value": { + "0\"": 7091232726655776, + "c\\\u0001aé": { + "0a ": null + }, + "é0cx": false, + "x\t😀é
": { + "\n\"": -18446744073709551616, + "\n\"中00c": true + } + } + }, + { + "op": "put", + "key": "x/😀", + "value": true + }, + { + "op": "delete", + "key": "a/abc", + "existed": true + }, + { + "op": "put", + "key": "é/user", + "value": 9151900423.560808 + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "delete", + "key": "ab/x", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": { + "": 1152921504606847109 + } + }, + { + "op": "put", + "key": "😀", + "value": false + }, + { + "op": "commit", + "root": "62accf9d3c3867520e58faab580a6588cdb8e20e7c29e43193f2bb6798873b43" + }, + { + "op": "put", + "key": "￿", + "value": [] + }, + { + "op": "put", + "key": "é/x/￿", + "value": { + "\\\u0001/\txc0😀": 1152921504606847082 + } + }, + { + "op": "put", + "key": "u", + "value": true + }, + { + "op": "put", + "key": "ab", + "value": { + "": { + "\\
b￿": null, + "￿\u0001\t1": 8931341152562429, + "\u0001\t中b9": "\t\t", + "😀😀z/": "\n中￿\\\u0001
" + }, + " ac": 8515762626616835, + "\"": "中 yx", + "y\tb\t\u0001😀/中": 1152921504606847104 + } + }, + { + "op": "put", + "key": "u/😀/ab", + "value": null + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "delete", + "key": "abc/😀/u", + "existed": true + }, + { + "op": "put", + "key": "x/😀/u", + "value": { + "😀b0zb": false, + "中": [ + true, + true + ], + "cé\"": { + " 😀": true, + "ba\n￿": -457337379954808 + }, + "\ncz0b
": { + "\"éyz1": false, + "": 6623239290182256, + "/019yé\\": null + } + } + }, + { + "op": "put", + "key": "users/😀/ab", + "value": "0y
\\" + }, + { + "op": "delete", + "key": "x/😀/u", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": {} + }, + { + "op": "commit", + "root": "fb0b23152abc5ab7ab0eac27e4b149cf819a8e130c2582b9888770e47ad29140" + }, + { + "op": "delete", + "key": "😀/a/a", + "existed": true + }, + { + "op": "put", + "key": "x/u", + "value": -2549254231391525 + }, + { + "op": "delete", + "key": "é/é/￿", + "existed": false + }, + { + "op": "commit", + "root": "7d75654629105b050116efc139bfec06631e19e444bd1ab7df22be5687c30165" + }, + { + "op": "delete", + "key": "x/😀", + "existed": true + }, + { + "op": "commit", + "root": "cf4f6311699311716df7b4555f06ba9ec75511169d19f6b3d965a362dfde88d7" + }, + { + "op": "put", + "key": "ab", + "value": { + "
x0￿9\u0001": { + "x/y中中9a中": 1152921504606847726, + "\t9x y": null + }, + "\\a\u0001😀y中": { + "éa\\": null, + "￿c1b\tc": -8294236355241357, + "bccb😀": -3460883124835219 + }, + "中": { + "/z
\"é": true, + "1
0": 8872401519835205, + "中/ 😀": null, + "91y1zc9": -0.6700163064224591 + }, + "0zb\t0\\": false + } + }, + { + "op": "put", + "key": "ab/￿/😀", + "value": 1152921504606847061 + }, + { + "op": "put", + "key": "😀/￿/😀", + "value": null + }, + { + "op": "delete", + "key": "é/user", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": "é￿ " + }, + { + "op": "commit", + "root": "12ee732648e74f37f41d69da938e045641bee933590a3fa32fbbc7edaf444a1c" + }, + { + "op": "put", + "key": "users/users", + "value": null + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": true + }, + { + "op": "put", + "key": "x/users/😀", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": null + }, + { + "op": "put", + "key": "user", + "value": 1152921504606847911 + }, + { + "op": "put", + "key": "users/￿", + "value": "9中\\\\é" + }, + { + "op": "put", + "key": "user", + "value": { + "y￿0éaé": null, + "y1/\\b": { + "écby中\n中": 29584211.613887373, + " éa😀90": "中z1c0\u0001zb", + " ayxb😀": -0.649562911066861, + "\\": true + }, + "é\u0001\u0001é\"": { + "\t9\\": 8884777165940577, + "\\￿x": false, + "ca\n😀\u0001": null, + "x

1\t\"é": null + }, + "b9😀é￿": 1152921504606847108 + } + }, + { + "op": "put", + "key": "a/😀", + "value": {} + }, + { + "op": "commit", + "root": "7200c617c537cdc33ee0fbb1837096af4f2c355f080d0f750a7d97d5764660b3" + }, + { + "op": "put", + "key": "x", + "value": true + }, + { + "op": "put", + "key": "ab", + "value": true + }, + { + "op": "put", + "key": "😀/u", + "value": { + "b": null, + "": "", + " z中\u0001": -18446744073709551616, + "11\"ba": { + "\\yz": true, + "": true, + "中￿ézb\n1": 344532283821597, + "0b￿\\\n\\é": -0.3260019933467111 + } + } + }, + { + "op": "delete", + "key": "u/abc", + "existed": true + }, + { + "op": "commit", + "root": "c6221930fb981dc343aaaa448ab6e5fdb9e90d539f017b93422e89f6f5f95695" + }, + { + "op": "put", + "key": "￿/x/user", + "value": "\u0001 \"\t\\中" + }, + { + "op": "put", + "key": "￿/￿/a", + "value": "" + }, + { + "op": "commit", + "root": "ecd09b19462bfc59332e4ed7d4ac7e66b2f5d3deb799acfbc423e00494e6eddd" + }, + { + "op": "put", + "key": "user/ab", + "value": "az\u0001x1yx" + }, + { + "op": "put", + "key": "u", + "value": { + "ab9bé\u0001
": -0.12433582379621178, + " c1\t中\"": { + "😀9zyb/": -2225304300974304, + "
": "😀zy
x", + "0\u00011\"￿é
z": true + }, + "
\\": { + "b中ca\"": true, + "\té😀9\n￿": -324841581102270, + "c😀￿by": "zcacaz9" + }, + "\t\\中": [ + -18446744073709551616, + "x\\\t1c/" + ] + } + }, + { + "op": "put", + "key": "é/users/ab", + "value": 1152921504606847765 + }, + { + "op": "delete", + "key": "￿/x/user", + "existed": true + }, + { + "op": "put", + "key": "é/users/x", + "value": {} + }, + { + "op": "put", + "key": "users/users/￿", + "value": { + "\\b": 7927229420658014, + "é￿\n
": 1265880572805915 + } + }, + { + "op": "put", + "key": "ab/users/é", + "value": [ + { + "z\t\\😀中": 1152921504606847104 + }, + { + "\u0001b
 z
": -4068212830225738, + "9": null + }, + false, + true + ] + }, + { + "op": "put", + "key": "users", + "value": 1.8686463799365743e-10 + }, + { + "op": "delete", + "key": "x/a", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": 0.1482451034984853 + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "delete", + "key": "abc/é", + "existed": true + }, + { + "op": "put", + "key": "users/users/x", + "value": "\t" + }, + { + "op": "put", + "key": "ab", + "value": [ + "\\x
1 
y", + [ + 1152921504606847937, + 3772187621166608, + -811193.049054771, + 4674140977728952 + ] + ] + }, + { + "op": "delete", + "key": "￿/￿/a", + "existed": true + }, + { + "op": "put", + "key": "users/a/😀", + "value": 1152921504606847632 + }, + { + "op": "put", + "key": "😀/u/ab", + "value": null + }, + { + "op": "put", + "key": "a/a", + "value": -0.515913436172214 + }, + { + "op": "delete", + "key": "x/é", + "existed": true + }, + { + "op": "put", + "key": "u/a", + "value": true + }, + { + "op": "put", + "key": "a/user/a", + "value": "/\"9😀y" + }, + { + "op": "put", + "key": "x/￿", + "value": null + }, + { + "op": "put", + "key": "a", + "value": "c\n\"" + }, + { + "op": "delete", + "key": "u/a", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "é/x/￿", + "existed": true + }, + { + "op": "delete", + "key": "users/￿", + "existed": true + }, + { + "op": "delete", + "key": "users/users", + "existed": true + }, + { + "op": "delete", + "key": "a/x", + "existed": false + }, + { + "op": "put", + "key": "users/u", + "value": [] + }, + { + "op": "put", + "key": "😀/ab/é", + "value": { + "": true, + "\u0001\\\u0001": true, + "\u00010": "b y9
é ", + "y\n/é/09": null + } + }, + { + "op": "put", + "key": "ab/😀/ab", + "value": " 9" + }, + { + "op": "delete", + "key": "ab/😀/ab", + "existed": true + }, + { + "op": "put", + "key": "😀/user", + "value": {} + }, + { + "op": "put", + "key": "users/￿", + "value": "" + }, + { + "op": "put", + "key": "x", + "value": 1152921504606847108 + }, + { + "op": "put", + "key": "é/a/￿", + "value": true + }, + { + "op": "put", + "key": "ab", + "value": 704189.2676369569 + }, + { + "op": "put", + "key": "u/é/ab", + "value": {} + }, + { + "op": "commit", + "root": "1d1ee29fcfd763349037773c4d7b4a573eb80b42e318d00a6b702c7b7ef6d871" + }, + { + "op": "put", + "key": "x/a", + "value": [] + }, + { + "op": "put", + "key": "user/a/￿", + "value": {} + }, + { + "op": "delete", + "key": "users/users/x", + "existed": true + }, + { + "op": "put", + "key": "users/ab", + "value": true + }, + { + "op": "put", + "key": "ab/u", + "value": "" + }, + { + "op": "put", + "key": "a/user/users", + "value": { + "": [ + null, + -18446744073709551616 + ], + "9": 3545523840262856, + "cz😀": { + "": -18446744073709551616, + "b\\": 1152921504606847400, + "1c": "中中/90é￿" + }, + "\\1 ￿": "
9" + } + }, + { + "op": "put", + "key": "a/a/￿", + "value": null + }, + { + "op": "put", + "key": "abc/a/user", + "value": null + }, + { + "op": "delete", + "key": "u/user/￿", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": -52867956797762 + }, + { + "op": "commit", + "root": "97906b9cb970f33f1100628aaa48403679598db42883bcc930c8832b5b90943e" + }, + { + "op": "put", + "key": "abc/x", + "value": -694680.7535715397 + }, + { + "op": "put", + "key": "😀", + "value": "" + }, + { + "op": "put", + "key": "x/😀/é", + "value": { + "￿\u00010\n\\": "c", + "/\"": { + "\\": null, + "￿0x1\u0001\\": false, + "￿x\"": -1090520089078268 + }, + "\\
": { + "bé": false, + "x9": "", + "": 5979433220707924, + "b y": true + } + } + }, + { + "op": "put", + "key": "u", + "value": 47507468532721.555 + }, + { + "op": "delete", + "key": "a/user/a", + "existed": true + }, + { + "op": "delete", + "key": "a/user/users", + "existed": true + }, + { + "op": "delete", + "key": "abc/users/user", + "existed": true + }, + { + "op": "delete", + "key": "ab/￿/😀", + "existed": true + }, + { + "op": "put", + "key": "abc/u/abc", + "value": [ + 1205982392274335, + { + "😀中1y中😀x\n": -18446744073709551616, + "\t中\\": 1210956020351507, + "bb": 2144588251547270 + } + ] + }, + { + "op": "put", + "key": "x/abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x/a", + "value": -5037139323945801 + }, + { + "op": "put", + "key": "a", + "value": { + " \u0001b": { + "": -3613743620030602 + }, + "\na\t": 1152921504606847405, + "z": [], + "zaé\u0001\u0001\"": [ + null, + -18446744073709551616, + 1.0730825888967211e-10 + ] + } + }, + { + "op": "commit", + "root": "8127269bc94243125f4f3eeb5a1b23fd8d81aee0be143d19890fc7c1b43a6da2" + }, + { + "op": "delete", + "key": "😀/user/x", + "existed": true + }, + { + "op": "commit", + "root": "9ee865231e874b662259fb79a2a6525ff1eb91a1b6218d5a9257c11a8f61229e" + }, + { + "op": "put", + "key": "user/user", + "value": "\\céxc\\\u0001\"" + }, + { + "op": "put", + "key": "😀", + "value": 9791490748.585184 + }, + { + "op": "delete", + "key": "x/￿/a", + "existed": false + }, + { + "op": "delete", + "key": "users/users/x", + "existed": false + }, + { + "op": "put", + "key": "user/😀/￿", + "value": true + }, + { + "op": "put", + "key": "user/abc/abc", + "value": { + "\tc\\\"a ": 168879.26095807785 + } + }, + { + "op": "put", + "key": "ab/user", + "value": "//0" + }, + { + "op": "commit", + "root": "a531f2c57935aecf85f0cf284377bb3475990d5c57adc955331eebf9c8828ac9" + }, + { + "op": "put", + "key": "user", + "value": { + "": "9c
1ay\n\"", + "\u0001b\\x": "11é😀\n😀" + } + }, + { + "op": "put", + "key": "user", + "value": { + "c￿1中
": [ + true + ] + } + }, + { + "op": "delete", + "key": "user/abc/abc", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": -8405007150166980 + }, + { + "op": "put", + "key": "users/user", + "value": null + }, + { + "op": "put", + "key": "é/é/ab", + "value": -1331536963664203 + }, + { + "op": "delete", + "key": "user/x", + "existed": true + }, + { + "op": "put", + "key": "users/ab", + "value": 390423.53405178036 + }, + { + "op": "put", + "key": "a/user/é", + "value": { + "z0zc": 1152921504606847331 + } + }, + { + "op": "put", + "key": "a/a/abc", + "value": [ + {}, + { + "zz￿x\"": false, + "￿c\t\n0cc": -18446744073709551616, + "\\\"😀/\n": "0", + "\"é\u0001\"x\t": -4949069302991004 + }, + "y09é￿b", + 1442236461640640 + ] + }, + { + "op": "put", + "key": "ab", + "value": false + }, + { + "op": "delete", + "key": "x/a", + "existed": true + }, + { + "op": "commit", + "root": "b2278d1c39ab4ae1a86d23ade7fa01bb5e1c000bfc05b5776316768cb24a92ef" + } + ], + "items": [ + [ + "a", + { + "\na\t": 1152921504606847405, + " \u0001b": { + "": -3613743620030602 + }, + "z": [], + "zaé\u0001\u0001\"": [ + null, + -18446744073709551616, + 1.0730825888967211e-10 + ] + } + ], + [ + "a/a", + -0.515913436172214 + ], + [ + "a/a/abc", + [ + {}, + { + "\"é\u0001\"x\t": -4949069302991004, + "\\\"😀/\n": "0", + "zz￿x\"": false, + "￿c\t\n0cc": -18446744073709551616 + }, + "y09é￿b", + 1442236461640640 + ] + ], + [ + "a/a/￿", + null + ], + [ + "a/u/abc", + "" + ], + [ + "a/user/é", + { + "z0zc": 1152921504606847331 + } + ], + [ + "a/é/￿", + 3895616142526731 + ], + [ + "a/😀", + {} + ], + [ + "ab", + false + ], + [ + "ab/u", + "" + ], + [ + "ab/user", + "//0" + ], + [ + "ab/users/é", + [ + { + "z\t\\😀中": 1152921504606847104 + }, + { + "\u0001b
 z
": -4068212830225738, + "9": null + }, + false, + true + ] + ], + [ + "abc", + -18446744073709551616 + ], + [ + "abc/a/user", + null + ], + [ + "abc/u/abc", + [ + 1205982392274335, + { + "\t中\\": 1210956020351507, + "bb": 2144588251547270, + "😀中1y中😀x\n": -18446744073709551616 + } + ] + ], + [ + "abc/x", + -694680.7535715397 + ], + [ + "u", + -8405007150166980 + ], + [ + "u/é/ab", + {} + ], + [ + "u/￿/😀", + 1152921504606847488 + ], + [ + "u/😀/ab", + null + ], + [ + "user", + { + "c￿1中
": [ + true + ] + } + ], + [ + "user/a/￿", + {} + ], + [ + "user/ab", + "az\u0001x1yx" + ], + [ + "user/user", + "\\céxc\\\u0001\"" + ], + [ + "user/￿/users", + [ + 7263133179168950, + -534321.9471758215, + "0b\"\t/￿\"" + ] + ], + [ + "user/😀/￿", + true + ], + [ + "users", + -52867956797762 + ], + [ + "users/a/😀", + 1152921504606847632 + ], + [ + "users/ab", + 390423.53405178036 + ], + [ + "users/abc", + 1152921504606847392 + ], + [ + "users/u", + [] + ], + [ + "users/user", + null + ], + [ + "users/users/￿", + { + "\\b": 7927229420658014, + "é￿\n
": 1265880572805915 + } + ], + [ + "users/￿", + "" + ], + [ + "users/😀/ab", + "0y
\\" + ], + [ + "x", + 1152921504606847108 + ], + [ + "x/abc", + -18446744073709551616 + ], + [ + "x/u", + -2549254231391525 + ], + [ + "x/users/😀", + null + ], + [ + "x/é/é", + true + ], + [ + "x/￿", + null + ], + [ + "x/￿/ab", + -0.14518588083877848 + ], + [ + "x/￿/é", + { + "": -0.03927420747133792, + "\u0001zé\"0\nc9": -542121.4288026612, + "\"zb\"c￿1": -825844.6389250358, + "中\"y\u0001cé/": "😀c\u0001/\n" + } + ], + [ + "x/😀/é", + { + "/\"": { + "\\": null, + "￿0x1\u0001\\": false, + "￿x\"": -1090520089078268 + }, + "\\
": { + "": 5979433220707924, + "b y": true, + "bé": false, + "x9": "" + }, + "￿\u00010\n\\": "c" + } + ], + [ + "é", + -18446744073709551616 + ], + [ + "é/a/￿", + true + ], + [ + "é/users/ab", + 1152921504606847765 + ], + [ + "é/users/x", + {} + ], + [ + "é/é/ab", + -1331536963664203 + ], + [ + "￿", + null + ], + [ + "￿/ab/a", + -1955416100330730 + ], + [ + "￿/abc", + -18446744073709551616 + ], + [ + "￿/user/x", + { + "\t\"0bcxx中": { + "\\z\u0001\té😀b": "😀\t\u0001" + }, + " \"z￿9z": null, + "bc中1\t/\t/": true + } + ], + [ + "￿/😀", + false + ], + [ + "😀", + 9791490748.585184 + ], + [ + "😀/ab/é", + { + "": true, + "\u00010": "b y9
é ", + "\u0001\\\u0001": true, + "y\n/é/09": null + } + ], + [ + "😀/u", + { + "": "", + " z中\u0001": -18446744073709551616, + "11\"ba": { + "": true, + "0b￿\\\n\\é": -0.3260019933467111, + "\\yz": true, + "中￿ézb\n1": 344532283821597 + }, + "b": null + } + ], + [ + "😀/u/ab", + null + ], + [ + "😀/user", + {} + ], + [ + "😀/é", + null + ], + [ + "😀/￿/😀", + null + ] + ] + }, + { + "name": "random-2/sha256", + "addressing": "sha256", + "steps": [ + { + "op": "commit", + "root": null + }, + { + "op": "commit", + "root": null + }, + { + "op": "put", + "key": "user", + "value": "￿0￿b" + }, + { + "op": "put", + "key": "é", + "value": { + "中\té9b": null, + "\t/\\\né": -5810787696403591, + "1az": 1152921504606847153, + "é\"é中z\t\né": -18446744073709551616 + } + }, + { + "op": "put", + "key": "user/é", + "value": { + "\u00019\u0001": [ + 6896831261063722, + 0.09465368642821947, + 7410724576053357, + 62915024.06150362 + ], + "\u0001é\"": false, + "a0xb😀": null, + "😀1xé": -4218229742632357 + } + }, + { + "op": "put", + "key": "é", + "value": null + }, + { + "op": "put", + "key": "users", + "value": true + }, + { + "op": "put", + "key": "users", + "value": 1152921504606847237 + }, + { + "op": "delete", + "key": "user/é", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": null + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "u/ab/abc", + "value": {} + }, + { + "op": "put", + "key": "users/ab/é", + "value": 8264717403952100 + }, + { + "op": "put", + "key": "u/users", + "value": { + "😀\\\u0001é/y/9": "
a中yb9" + } + }, + { + "op": "put", + "key": "u", + "value": { + "b11\tc9c😀": 73854.84734464146 + } + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": true + }, + { + "op": "put", + "key": "a", + "value": true + }, + { + "op": "put", + "key": "￿/é", + "value": 0.5210497969853378 + }, + { + "op": "put", + "key": "é", + "value": [ + -7317684015496443, + { + "": "￿", + " \" a\n": null + }, + true, + null + ] + }, + { + "op": "delete", + "key": "￿/é", + "existed": true + }, + { + "op": "put", + "key": "x/u/users", + "value": "" + }, + { + "op": "put", + "key": "a", + "value": { + "😀": -18446744073709551616 + } + }, + { + "op": "put", + "key": "ab", + "value": true + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "commit", + "root": "037b9ae75f616f951be10d32f5f5f2b90d0c075b9885e105876c0605cda2b2ed" + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "put", + "key": "😀", + "value": "\ty\"9\u0001" + }, + { + "op": "put", + "key": "é/😀/ab", + "value": { + "\u0001￿": "\nc😀😀xc\"z" + } + }, + { + "op": "commit", + "root": "eb693734513376c73f0745b3fdf071fd3a3f2239128f52f556800595c95ee617" + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "put", + "key": "abc", + "value": { + " xy中\nx/é": 1264464186784.523 + } + }, + { + "op": "put", + "key": "ab/é", + "value": "
\u0001\"中/c" + }, + { + "op": "delete", + "key": "a/é/é", + "existed": false + }, + { + "op": "commit", + "root": "27fe864cbb0c43dfe3a1684135fd573e2b55b9011cedc4f6e0b039cc66e2db34" + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "user/u", + "value": null + }, + { + "op": "put", + "key": "x/é/a", + "value": true + }, + { + "op": "put", + "key": "x/x", + "value": 0.22385174241472972 + }, + { + "op": "put", + "key": "a/u", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x", + "value": { + "9é\tac￿\"": 4508207094872803, + "9
ybz\u0001": { + "ya 中": 5841994782166.965, + "0 \u0001
y\u0001中": -18446744073709551616, + "9": -2995669663086030 + } + } + }, + { + "op": "delete", + "key": "x/x", + "existed": true + }, + { + "op": "put", + "key": "a/é/x", + "value": [ + [] + ] + }, + { + "op": "put", + "key": "￿/a", + "value": [ + null, + true + ] + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "😀/abc", + "value": -1044675643202406 + }, + { + "op": "commit", + "root": "49e25b96b815a58fbdbe3f4d90b18169bc49a707b951fc54697cea3369768e55" + }, + { + "op": "put", + "key": "😀", + "value": -0.3475467091950055 + }, + { + "op": "put", + "key": "user/abc", + "value": -6568388855102268 + }, + { + "op": "put", + "key": "é/é/abc", + "value": "😀/\\
 中￿
" + }, + { + "op": "delete", + "key": "x/u/users", + "existed": true + }, + { + "op": "put", + "key": "😀/😀", + "value": false + }, + { + "op": "put", + "key": "users", + "value": true + }, + { + "op": "put", + "key": "é/users/￿", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a", + "value": 1152921504606847233 + }, + { + "op": "put", + "key": "a/a", + "value": 456073.1144339554 + }, + { + "op": "put", + "key": "😀", + "value": "/é" + }, + { + "op": "put", + "key": "x", + "value": { + "": { + "￿/中￿￿c😀": "\u0001\\\tz\n\\é\t", + "": null, + "😀yxézc\\": 5366988803890382 + } + } + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": {} + }, + { + "op": "put", + "key": "users/😀", + "value": -0.6007859178460306 + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": { + "": 68284553343612.805 + } + }, + { + "op": "put", + "key": "user/￿/u", + "value": 8235722742674778 + }, + { + "op": "put", + "key": "abc/é", + "value": -6658090824912385 + }, + { + "op": "put", + "key": "￿", + "value": true + }, + { + "op": "put", + "key": "a/é", + "value": 847266905937133 + }, + { + "op": "delete", + "key": "users/ab/é", + "existed": true + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "users/ab/😀", + "value": [] + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": [ + false + ] + }, + { + "op": "commit", + "root": "c29c2fe9258d5e7dd2e443144a1df0762c8ac790a40b41244de0d0438634e7f0" + }, + { + "op": "put", + "key": "user", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "a/u", + "existed": true + }, + { + "op": "put", + "key": "users/😀/ab", + "value": false + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "x/￿", + "value": -0.742495377461639 + }, + { + "op": "commit", + "root": "02e13f8596165275cdd9615c5c85fc814b463695ebf36a82a4585f2539c7a98b" + }, + { + "op": "put", + "key": "a/x/😀", + "value": { + "😀/ cz\"￿": { + " 0": "xaa0" + } + } + }, + { + "op": "commit", + "root": "3fa77e18d25950cf440b68bf6c0b93e5f8f034bd9ea763414e8a41dc0b9bcf1f" + }, + { + "op": "put", + "key": "abc/😀/u", + "value": { + "": "0y", + "￿\\": false, + "9
c\u0001\u00011y": "0￿/" + } + }, + { + "op": "put", + "key": "é/a/a", + "value": [] + }, + { + "op": "put", + "key": "é/é/u", + "value": { + "\u0001\\ 011": null, + "éc￿中ab\\\n": { + "9x\"é\"é\u0001😀": null + }, + "19aa\u0001by": -3212338297784968 + } + }, + { + "op": "put", + "key": "user", + "value": "
" + }, + { + "op": "put", + "key": "user", + "value": { + "": "9\u0001b中\"/x￿" + } + }, + { + "op": "put", + "key": "abc/ab", + "value": { + "￿yab/\n
a": 1152921504606847664, + "中": [ + -142772.81732244696, + "z ab1", + "\\c\"", + null + ] + } + }, + { + "op": "commit", + "root": "870f9da46b2294e2e9d846490676ecf9039d97efbf78776c9db2bc8071bc0640" + }, + { + "op": "commit", + "root": "870f9da46b2294e2e9d846490676ecf9039d97efbf78776c9db2bc8071bc0640" + }, + { + "op": "put", + "key": "users", + "value": {} + }, + { + "op": "put", + "key": "a/users/￿", + "value": [ + { + "y\n": true, + "b😀zé\t": 198487.23636563774, + "￿y\"ya9z": false + }, + "\u0001\t0\n\n91", + 1152921504606847744, + { + "": 0.6799907687918971, + "91\t\t": 6252070170451755, + "1中中z\t \"": -780526.7944130792, + "￿\t￿é\tc": -18446744073709551616 + } + ] + }, + { + "op": "put", + "key": "abc/￿/abc", + "value": -4802163647997278 + }, + { + "op": "delete", + "key": "é/a/a", + "existed": true + }, + { + "op": "put", + "key": "user/é/ab", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "abc/users", + "value": { + "": -0.7713840304213011, + "\\z/cé": { + "\tx￿": null, + "a \u0001yb": null, + "\\\u0001
￿a": 277760.69799884036 + }, + " \\😀￿\u0001": { + "é": false, + "\"\"z": false + }, + "0😀\t": -204132.43975000083 + } + }, + { + "op": "put", + "key": "user/a", + "value": -279452717319622 + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "commit", + "root": "7445c63dce7755c735719d87b0e41b04a77e69559dfcfa44a27ad78cbf69027c" + }, + { + "op": "commit", + "root": "7445c63dce7755c735719d87b0e41b04a77e69559dfcfa44a27ad78cbf69027c" + }, + { + "op": "put", + "key": "a/user", + "value": "￿aa\"x " + }, + { + "op": "put", + "key": "u/u", + "value": { + "\t
": true + } + }, + { + "op": "commit", + "root": "365923bdc3351189fc26ffb2e892e4e250b946e7b051d4372dc89712ada4dd1c" + }, + { + "op": "put", + "key": "ab/ab/users", + "value": [ + 0.8224996794113091, + 714451.1198869103, + 706927.5322352627, + 7602748445201485 + ] + }, + { + "op": "put", + "key": "u/abc", + "value": false + }, + { + "op": "commit", + "root": "07a7198a2dd5d3858ca317b3364450b75a28ba4e69bb0a2583fff2dfe3e4d2ea" + }, + { + "op": "commit", + "root": "07a7198a2dd5d3858ca317b3364450b75a28ba4e69bb0a2583fff2dfe3e4d2ea" + }, + { + "op": "commit", + "root": "07a7198a2dd5d3858ca317b3364450b75a28ba4e69bb0a2583fff2dfe3e4d2ea" + }, + { + "op": "delete", + "key": "users/ab/😀", + "existed": true + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "delete", + "key": "user/a", + "existed": true + }, + { + "op": "delete", + "key": "￿/a", + "existed": true + }, + { + "op": "delete", + "key": "é/é/abc", + "existed": true + }, + { + "op": "commit", + "root": "4e7b284109bf0d895dc713116468ed95bad5b551dd57519fe1401f0e86aa0446" + }, + { + "op": "delete", + "key": "u/u", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "delete", + "key": "user/ab/x", + "existed": false + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": false + }, + { + "op": "delete", + "key": "a/users/￿", + "existed": true + }, + { + "op": "delete", + "key": "😀/abc", + "existed": true + }, + { + "op": "delete", + "key": "abc/é", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": 8136182827453579 + }, + { + "op": "put", + "key": "a/ab/x", + "value": [] + }, + { + "op": "put", + "key": "x/user/a", + "value": false + }, + { + "op": "put", + "key": "😀", + "value": { + "/": 1.1641864334751701e-05, + "\"中1/": { + "": true + }, + "z\\": -7127687235979652 + } + }, + { + "op": "delete", + "key": "é/users/￿", + "existed": true + }, + { + "op": "put", + "key": "u/users/a", + "value": {} + }, + { + "op": "put", + "key": "user/user", + "value": "9😀1é" + }, + { + "op": "commit", + "root": "6eb88bd89d2a795ff56ae2fb3d59eadfc9e9a3c3600d05a0777fd6e7f8326877" + }, + { + "op": "put", + "key": "😀/é", + "value": "z
\n 😀\t" + }, + { + "op": "put", + "key": "￿/u", + "value": null + }, + { + "op": "put", + "key": "😀/u/￿", + "value": [ + [ + false, + -7095750625121347 + ], + 9.745363582635186, + { + " ": -717185.8436805888 + } + ] + }, + { + "op": "commit", + "root": "21312237733c6ce53c1fefedf5217868168438af2ebfc89da49baab9b71802da" + }, + { + "op": "put", + "key": "é", + "value": [ + 8.123583920255413, + 3788.350462644594 + ] + }, + { + "op": "put", + "key": "￿/users", + "value": null + }, + { + "op": "delete", + "key": "user/abc", + "existed": true + }, + { + "op": "commit", + "root": "1021665dab3314d615ed197e0b890db2ba2e01039279dce620108ad5a50557f3" + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": "9é9b/a\n" + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "u/user", + "value": null + }, + { + "op": "put", + "key": "x/user/users", + "value": null + }, + { + "op": "put", + "key": "x/￿/😀", + "value": { + "xac0a": [ + null, + true, + -8681219311921926, + 1152921504606847576 + ], + "9x\\
z": {}, + "0z￿": { + "": false, + "z￿a": false, + "
1 ": -7503386500974739 + }, + "
y😀": 977924.572013817 + } + }, + { + "op": "put", + "key": "a", + "value": " 中 b " + }, + { + "op": "commit", + "root": "78b849293d0d9b53efa084c50526ded9c3124aad28af23e838a1d97d56f41bad" + }, + { + "op": "commit", + "root": "78b849293d0d9b53efa084c50526ded9c3124aad28af23e838a1d97d56f41bad" + }, + { + "op": "put", + "key": "x", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "23039eaff156d150ef59b315d19271beaf0de15fab121eb58944e13aefb32941" + }, + { + "op": "put", + "key": "￿", + "value": 1884570001370224 + }, + { + "op": "put", + "key": "u", + "value": {} + }, + { + "op": "delete", + "key": "ab/é", + "existed": true + }, + { + "op": "put", + "key": "user/é/a", + "value": true + }, + { + "op": "put", + "key": "x", + "value": "/abax0 " + }, + { + "op": "put", + "key": "a/a/a", + "value": true + }, + { + "op": "put", + "key": "x", + "value": { + "": null, + "x0": 0.4279955369099355, + "z": "￿cz" + } + }, + { + "op": "delete", + "key": "😀/users", + "existed": false + }, + { + "op": "delete", + "key": "abc/😀/u", + "existed": true + }, + { + "op": "delete", + "key": "a/user", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": -831238.3231132485 + }, + { + "op": "put", + "key": "é/a", + "value": 20162852062012.543 + }, + { + "op": "commit", + "root": "d008971bc6e884a126103a7763f7c230f754fbf6134b2da3fdbe2f99e20ee6d1" + }, + { + "op": "delete", + "key": "a/a/a", + "existed": true + }, + { + "op": "put", + "key": "a/￿/é", + "value": [] + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "é/a", + "value": 3429741327020023 + }, + { + "op": "put", + "key": "users", + "value": "y\\\n😀by
b" + }, + { + "op": "put", + "key": "ab/￿/user", + "value": 8975440110461416 + }, + { + "op": "put", + "key": "é", + "value": [ + -1480753958253816, + "a
0", + "", + 0.9184893929983005 + ] + }, + { + "op": "put", + "key": "￿/a/é", + "value": true + }, + { + "op": "put", + "key": "user/ab/x", + "value": 816313151259329 + }, + { + "op": "commit", + "root": "be03c17dd1d264530000e2aa05d6f8d23339c4c7e4dcd7154683b3a828c7af52" + }, + { + "op": "put", + "key": "users", + "value": "/￿￿😀
y" + }, + { + "op": "put", + "key": "abc/abc/users", + "value": 4.252731372064616e-12 + }, + { + "op": "put", + "key": "users/x/é", + "value": true + }, + { + "op": "put", + "key": "ab/abc", + "value": { + "0/中a￿a￿": true, + "\n￿\t😀9\n": null + } + }, + { + "op": "put", + "key": "user", + "value": 1152921504606847197 + }, + { + "op": "commit", + "root": "b1e2018a9643211c83d92cd3f625878e1c72a2276bb48b2e46cf10ce158d7570" + }, + { + "op": "put", + "key": "ab/abc", + "value": null + }, + { + "op": "put", + "key": "user/user", + "value": "\u0001é中中" + }, + { + "op": "put", + "key": "x/￿/😀", + "value": { + "\\": false, + "yx/\\\u0001😀c\n": { + "\u0001a中\u0001": "
\n0￿c", + "": false, + "a b\n": 3657686048913665 + }, + "\né9": { + "a9z ": null, + "a0z": -4006062950343854, + "/\\😀": true + } + } + }, + { + "op": "put", + "key": "é", + "value": "90" + }, + { + "op": "commit", + "root": "0be16cc038bcba7effeef843644b55d385ef897ba291aeab38d014828fc1891a" + }, + { + "op": "commit", + "root": "0be16cc038bcba7effeef843644b55d385ef897ba291aeab38d014828fc1891a" + }, + { + "op": "delete", + "key": "user/é/a", + "existed": true + }, + { + "op": "put", + "key": "￿", + "value": { + "9￿1\n\t1
": { + "\u0001\\a": -4500260163213891, + "/0😀
": -0.8120118552736082 + }, + "中1
0": true, + "é\t/é": -5601067476714478 + } + }, + { + "op": "put", + "key": "😀/😀", + "value": [ + "\t\t", + null + ] + }, + { + "op": "commit", + "root": "4c5fff1da7f1405c6d023119a343972d3aa35bfd1d27602457bd7fa952ea257b" + }, + { + "op": "delete", + "key": "abc/users", + "existed": true + }, + { + "op": "put", + "key": "😀/user", + "value": "
\nbz\"" + }, + { + "op": "delete", + "key": "x/user/users", + "existed": true + }, + { + "op": "commit", + "root": "4bd12ca069f5930348a2b1a68889e50470f095f42e0f3fa445199190ad79d5a0" + }, + { + "op": "put", + "key": "é/abc/￿", + "value": 79851996.01838501 + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "😀/users/￿", + "value": -0.8636252350646845 + }, + { + "op": "put", + "key": "é/abc", + "value": 7981756839298357 + }, + { + "op": "delete", + "key": "é/abc", + "existed": true + }, + { + "op": "delete", + "key": "user/user", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": 1152921504606847204 + }, + { + "op": "put", + "key": "é", + "value": false + }, + { + "op": "put", + "key": "é", + "value": 0.00029552700270127027 + }, + { + "op": "put", + "key": "a", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "😀", + "value": [ + [ + null, + null, + "y/中1111\n", + -18446744073709551616 + ], + -3422922777074208 + ] + }, + { + "op": "delete", + "key": "￿/u", + "existed": true + }, + { + "op": "delete", + "key": "￿/x", + "existed": false + }, + { + "op": "put", + "key": "￿", + "value": { + "b\n\u0001\\c": true, + "bz1\"\\/￿": -18446744073709551616, + "\t😀\n\nyé￿y": 811883.0582577325, + "c\\中\tz/": { + "": null + } + } + }, + { + "op": "put", + "key": "￿", + "value": "xx" + }, + { + "op": "put", + "key": "u", + "value": 1152921504606847016 + }, + { + "op": "put", + "key": "￿/user/abc", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": 975825.6783753687 + }, + { + "op": "put", + "key": "😀/￿/users", + "value": 1298350660019815 + }, + { + "op": "put", + "key": "x/users", + "value": {} + }, + { + "op": "put", + "key": "😀/u/a", + "value": {} + }, + { + "op": "commit", + "root": "9f8dbb0501b6f10fdafe85f61402e17d5203aa77a0ea74a2c395245e5d28f1c1" + }, + { + "op": "delete", + "key": "ab/abc", + "existed": true + }, + { + "op": "put", + "key": "x/😀", + "value": { + "y￿￿ ": { + "x": null, + "x/b a0": "\"
" + }, + "
yc9￿": false, + "😀1": null + } + }, + { + "op": "commit", + "root": "87b4b31ad31c2d68ce230291cc4f804b1a928a15e8fa6af6e9a970e7debe8d19" + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "ab", + "value": { + "😀b": -199200.11279452336, + "/c 0cc": -18446744073709551616, + "ay
\"\" \u0001é": null + } + }, + { + "op": "put", + "key": "￿/x", + "value": -4457290127836679 + }, + { + "op": "commit", + "root": "3a2aa51cd91e8eff7f3d8f660d41336b7a9d24fca7f5f785df433119fab06eab" + }, + { + "op": "put", + "key": "user/é", + "value": {} + }, + { + "op": "commit", + "root": "ab868f4ec359b346390f245c843b4880557547a72d67be1e457b086a7bcb4716" + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "commit", + "root": "281fb35af44134035bc1c37412a62149769357e6615c76920eeab82091cda8f1" + }, + { + "op": "put", + "key": "é/ab", + "value": true + }, + { + "op": "delete", + "key": "😀/😀", + "existed": true + }, + { + "op": "delete", + "key": "😀/￿/users", + "existed": true + }, + { + "op": "delete", + "key": "￿/é", + "existed": false + }, + { + "op": "delete", + "key": "😀/u/a", + "existed": true + }, + { + "op": "delete", + "key": "😀", + "existed": false + }, + { + "op": "delete", + "key": "u/ab/abc", + "existed": true + }, + { + "op": "delete", + "key": "😀/u/￿", + "existed": true + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "delete", + "key": "x/￿/😀", + "existed": true + }, + { + "op": "delete", + "key": "😀/users/￿", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": [ + 683501.1972635924, + {}, + 0.19109490539973928 + ] + }, + { + "op": "put", + "key": "users/abc/x", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x", + "value": [ + {} + ] + }, + { + "op": "delete", + "key": "￿/x", + "existed": true + }, + { + "op": "commit", + "root": "d5ca38a3c8b222e4ae704461b9a71839d7e29cd2880a7776eae66cc078e6b3f7" + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "put", + "key": "x/abc", + "value": "中中" + }, + { + "op": "put", + "key": "abc", + "value": [ + { + "zcy ": "\"中\u0001c￿9", + " x中😀9 x": "￿x￿\n

ba", + "
0/0\t中": 6430599794759789, + "/\"\\/a中é": -18446744073709551616 + }, + 0.6293664603917879, + 58024990733594.93, + { + "/a\\\n
 ": null + } + ] + }, + { + "op": "put", + "key": "ab/ab/users", + "value": null + }, + { + "op": "commit", + "root": "681ca1a32b1ecc7af0d441c83c912e89ccfe0ed8b0e177a1d2f53aee02062360" + }, + { + "op": "put", + "key": "é/é", + "value": 0.9885952857249212 + }, + { + "op": "put", + "key": "a", + "value": { + "中bxc\n \n/": "0
中\nc
x0", + "": {}, + "￿z￿😀c\"": { + "￿😀\\9a0a": 0.10172587710287129, + "
\n\"z0 \\": 27745454571002.375, + "中y\u0001中é": false, + "\"": "" + } + } + }, + { + "op": "commit", + "root": "8afb8c739e64842ab61fae8dcc1bbe9e79cda9fe2a0a37d56d6ce8db52bd6bee" + }, + { + "op": "commit", + "root": "8afb8c739e64842ab61fae8dcc1bbe9e79cda9fe2a0a37d56d6ce8db52bd6bee" + }, + { + "op": "put", + "key": "😀", + "value": "xx😀/a\u0001😀" + }, + { + "op": "put", + "key": "a", + "value": "z0\\9中😀￿" + }, + { + "op": "put", + "key": "ab/é", + "value": "\"90
😀\\z" + }, + { + "op": "put", + "key": "😀", + "value": { + "
中\u0001a0": "" + } + }, + { + "op": "commit", + "root": "73440078142925d8edfd0fc388338331da0149fa0d7424b1a3539066a19a51dc" + }, + { + "op": "put", + "key": "user", + "value": true + }, + { + "op": "put", + "key": "users", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a/é/ab", + "value": -0.8878078883743652 + }, + { + "op": "put", + "key": "user", + "value": { + "z\\\\1": 617485.7139944278, + "": false, + "z
1y\"
b": 1152921504606847838 + } + }, + { + "op": "put", + "key": "a", + "value": "a9" + }, + { + "op": "put", + "key": "😀/user/é", + "value": -8402171115934331 + }, + { + "op": "put", + "key": "abc", + "value": 5583860529011803 + }, + { + "op": "commit", + "root": "f38c29521e0b4bb6f5c88414e410823be309bb632e8febf9057c4fa8bc9f0d5e" + }, + { + "op": "put", + "key": "user/￿", + "value": [] + }, + { + "op": "delete", + "key": "￿/user/abc", + "existed": true + }, + { + "op": "commit", + "root": "0ad1ebfa3f9ec886ecf09faa412b8419f72b7636a7b9b9c9e6dc8156322ad38b" + }, + { + "op": "commit", + "root": "0ad1ebfa3f9ec886ecf09faa412b8419f72b7636a7b9b9c9e6dc8156322ad38b" + }, + { + "op": "put", + "key": "x/u", + "value": 3745147572404717 + }, + { + "op": "put", + "key": "a", + "value": { + "0\\": [ + null, + -206589143784449 + ], + "a\n ": "" + } + }, + { + "op": "delete", + "key": "u/abc", + "existed": true + }, + { + "op": "delete", + "key": "a/x/😀", + "existed": true + }, + { + "op": "commit", + "root": "63d924e85416e06acc5e71bfb8de837a8321623c62e6cb1b6e93bb4dbcffa2f4" + }, + { + "op": "delete", + "key": "user/￿/u", + "existed": true + }, + { + "op": "commit", + "root": "8d00d046545cfce23936346365de038bbcff3c0d3f458890e561836b38aa0149" + }, + { + "op": "delete", + "key": "u/ab", + "existed": false + }, + { + "op": "put", + "key": "abc", + "value": "yc😀" + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "delete", + "key": "users/x/é", + "existed": true + }, + { + "op": "commit", + "root": "d241adbbc6c763bcd49aae08e5e368a333c3a0ad4e40d9f1349234ba0eea2400" + }, + { + "op": "put", + "key": "é", + "value": 1152921504606847920 + }, + { + "op": "delete", + "key": "users/😀", + "existed": true + }, + { + "op": "put", + "key": "￿/😀", + "value": -0.3082166045842658 + }, + { + "op": "put", + "key": "x/ab", + "value": [ + -18446744073709551616 + ] + }, + { + "op": "put", + "key": "x/user/user", + "value": false + }, + { + "op": "commit", + "root": "8cbf0139f1e5c92c87dc40ee4c215c6b1d55324ed4fd86c9612b0f6130ee93f1" + }, + { + "op": "put", + "key": "😀", + "value": "中\"0😀bz/中" + }, + { + "op": "put", + "key": "abc/x", + "value": { + " \n\"": 92.14966227236646 + } + }, + { + "op": "commit", + "root": "c00cd9486e6f8396f1c3516627f629c7cd1af200f67752a723e7e790f0acb3ec" + }, + { + "op": "put", + "key": "x/abc", + "value": "中😀\"a\t" + }, + { + "op": "delete", + "key": "😀/user", + "existed": true + }, + { + "op": "delete", + "key": "abc/abc/users", + "existed": true + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "5e68509ee05276f63cc15c4bea15edf94832d0eb9fe175dcbcb70c53d8802ec4" + }, + { + "op": "delete", + "key": "a/ab/x", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": 426159.6239946245 + }, + { + "op": "commit", + "root": "6c0087bf5331af6466d030140434d31844cf4464a2f63ffca50f8079425ce5a9" + }, + { + "op": "delete", + "key": "a/a", + "existed": true + }, + { + "op": "delete", + "key": "a/￿/é", + "existed": true + }, + { + "op": "delete", + "key": "users/😀/ab", + "existed": true + }, + { + "op": "delete", + "key": "x/a", + "existed": false + }, + { + "op": "delete", + "key": "é/ab", + "existed": true + }, + { + "op": "put", + "key": "é/￿", + "value": 0.07561877714596606 + }, + { + "op": "put", + "key": "ab/a", + "value": { + "\ncc": true, + "\\\n\tza": { + "\nc\n中z 1": 1152921504606847697 + } + } + }, + { + "op": "delete", + "key": "u/users/a", + "existed": true + }, + { + "op": "put", + "key": "x/abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "u", + "value": {} + }, + { + "op": "put", + "key": "😀/user", + "value": -3472000912914411 + }, + { + "op": "put", + "key": "a/a", + "value": 0.06132866827788308 + }, + { + "op": "put", + "key": "a/user/users", + "value": -0.9172011080678681 + }, + { + "op": "delete", + "key": "😀/user", + "existed": true + }, + { + "op": "put", + "key": "￿/￿/abc", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "abc/user", + "value": {} + }, + { + "op": "commit", + "root": "43e727a27a0f4414f21bfb95a62e45bcf0e0ad476999ce670b87f6d2ce7eb07f" + }, + { + "op": "delete", + "key": "user/￿", + "existed": true + }, + { + "op": "commit", + "root": "4490add245a9c03b8b2fb7eee5064cb62be5cd3ac2464ee5205dfbebd789ae70" + }, + { + "op": "put", + "key": "u/user/abc", + "value": 3543116664759.4956 + }, + { + "op": "delete", + "key": "abc/￿/abc", + "existed": true + }, + { + "op": "delete", + "key": "user/é/ab", + "existed": true + }, + { + "op": "put", + "key": "u/a/abc", + "value": [ + false, + "c😀
x" + ] + }, + { + "op": "put", + "key": "u", + "value": { + "
x\"\u0001￿é9": false + } + }, + { + "op": "commit", + "root": "c8b2ca42e71a4c5adef17014af3fe3b9a7da9e58befe03dcb486dbfd2a7a0b0f" + }, + { + "op": "delete", + "key": "abc/user", + "existed": true + }, + { + "op": "put", + "key": "u/é", + "value": {} + }, + { + "op": "put", + "key": "a/users", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "users/x", + "value": [ + -18446744073709551616, + { + "\na1c": 1892193662427921, + "": -471541699076132, + "\u0001
\u0001x\"\"": "1\"xy
\\￿\"" + }, + {}, + [ + null, + true + ] + ] + }, + { + "op": "delete", + "key": "é/é/u", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": 0.0699746458055941 + }, + { + "op": "put", + "key": "a/é", + "value": false + }, + { + "op": "delete", + "key": "x/user/user", + "existed": true + }, + { + "op": "delete", + "key": "é/a", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": { + "": null + } + }, + { + "op": "put", + "key": "😀", + "value": { + "a1bc
\u0001": null, + "￿\u000191": {}, + "1中￿": true, + "": [ + "x中9￿", + "", + null, + -7682083297527074 + ] + } + }, + { + "op": "put", + "key": "ab/a/user", + "value": { + "": { + "1": -6778640052511576, + " yzz": null, + "y": 191967.5581819946, + "\n中0": true + }, + "\" c\\x": -7215896155523050, + "\\\u0001\t c": 1152921504606847341 + } + }, + { + "op": "commit", + "root": "f8ab1e054806c7506a7c426a7817280de384ec5a6dbaffa46c9d2fa2b004a560" + }, + { + "op": "delete", + "key": "x/é/a", + "existed": true + }, + { + "op": "put", + "key": "x/é", + "value": false + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "delete", + "key": "ab/a", + "existed": true + }, + { + "op": "put", + "key": "x/users", + "value": 1152921504606847806 + }, + { + "op": "commit", + "root": "fdc996c3995663ac0d5de35c94622a928152fab7bf1ffd3a451745ec3531087f" + }, + { + "op": "put", + "key": "users", + "value": { + "é": "\n ￿\"中/", + "\u0001\n\n\t": true, + "
": [], + "
b\tzy\"0c": true + } + }, + { + "op": "put", + "key": "￿", + "value": 26437716273043 + }, + { + "op": "delete", + "key": "x/abc", + "existed": true + }, + { + "op": "put", + "key": "a/😀/x", + "value": {} + }, + { + "op": "put", + "key": "😀", + "value": {} + }, + { + "op": "put", + "key": "u/abc/￿", + "value": { + " 
cy": null, + "c\n": -944359.3880934673 + } + }, + { + "op": "delete", + "key": "ab/é", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": { + "/": true, + "éz9y ": null + } + }, + { + "op": "commit", + "root": "df1c4076cd40cd99fb70812fa34a361d12026750db51908bf38394eb3be2aa03" + }, + { + "op": "commit", + "root": "df1c4076cd40cd99fb70812fa34a361d12026750db51908bf38394eb3be2aa03" + }, + { + "op": "put", + "key": "😀", + "value": false + }, + { + "op": "put", + "key": "😀/ab", + "value": 5798900.91171178 + }, + { + "op": "put", + "key": "a/u", + "value": false + }, + { + "op": "commit", + "root": "b294bdfb396177de4a811e652f1a7165a21fb7b2a4551bbd3a400a20964d6e54" + }, + { + "op": "commit", + "root": "b294bdfb396177de4a811e652f1a7165a21fb7b2a4551bbd3a400a20964d6e54" + }, + { + "op": "put", + "key": "a", + "value": [ + { + "": true, + "/😀": -5655752702209879, + "\u0001\u0001\"\"😀
": -460660283449905 + }, + null, + "a
90b
a
" + ] + }, + { + "op": "put", + "key": "x", + "value": -532135.6385488648 + }, + { + "op": "delete", + "key": "😀/user/é", + "existed": true + }, + { + "op": "put", + "key": "users/x/a", + "value": {} + }, + { + "op": "delete", + "key": "x/user/a", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": { + "/y\u0001\"": "9x￿z/é\"
" + } + }, + { + "op": "delete", + "key": "é/￿", + "existed": true + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "commit", + "root": "c393dbc1294c2f6004b870f6435592e62bc4f781ffe8b74f06284de7a7563edf" + }, + { + "op": "put", + "key": "ab", + "value": { + "0": true, + "11": "" + } + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "delete", + "key": "￿/user/ab", + "existed": false + }, + { + "op": "put", + "key": "😀/x", + "value": {} + }, + { + "op": "put", + "key": "😀", + "value": [] + }, + { + "op": "put", + "key": "x/￿/ab", + "value": [] + }, + { + "op": "put", + "key": "u", + "value": { + "\t9中c y": { + "1中z￿😀a1": 2.237744424848274, + "9
中/": -2847243301517036, + "\t": 1554006749238746 + }, + "\tcc￿": 3166238056711594 + } + }, + { + "op": "put", + "key": "abc", + "value": -336747.62071530614 + }, + { + "op": "put", + "key": "users/é/😀", + "value": [ + -18446744073709551616, + {} + ] + }, + { + "op": "delete", + "key": "😀/x", + "existed": true + }, + { + "op": "delete", + "key": "￿/￿/abc", + "existed": true + }, + { + "op": "delete", + "key": "é/😀/ab", + "existed": true + }, + { + "op": "delete", + "key": "😀/é", + "existed": true + }, + { + "op": "delete", + "key": "u/é", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": -6224794831260817 + }, + { + "op": "put", + "key": "😀/￿", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "user/ab/x", + "existed": true + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": "\\\tbc￿" + }, + { + "op": "put", + "key": "😀/😀/ab", + "value": { + "zy": -4365457848758776, + "
": "/0\t99", + "0\\9b😀zx": { + "\\": -251140734133170, + "￿": true, + "\u0001écb90￿😀": null, + "": null + }, + "": { + "😀c \n/b\\": -18446744073709551616, + "1x
xb
\u00011": -18446744073709551616, + "￿\"\t 9\\": "\t" + } + } + }, + { + "op": "put", + "key": "😀", + "value": [ + null, + { + "cc\t😀\"bx0": null + } + ] + }, + { + "op": "put", + "key": "users/😀", + "value": -1461530824469002 + }, + { + "op": "delete", + "key": "u/a/abc", + "existed": true + }, + { + "op": "delete", + "key": "ab/￿/user", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": 1152921504606847348 + }, + { + "op": "delete", + "key": "x/u", + "existed": true + }, + { + "op": "put", + "key": "é/users", + "value": { + "": null, + "0x/
\"z\n": "b0\t" + } + }, + { + "op": "put", + "key": "😀", + "value": [ + -18446744073709551616, + false, + true + ] + }, + { + "op": "put", + "key": "a", + "value": [ + 582857.2323208617, + 1020079857923796, + 1152921504606847959 + ] + }, + { + "op": "put", + "key": "😀", + "value": 1152921504606847269 + }, + { + "op": "commit", + "root": "f7eb37f8416d29fa4ef6ffde755ff3ef7654b67a42172e846c354a14a78a96b2" + }, + { + "op": "put", + "key": "é", + "value": 7.442987188463953 + }, + { + "op": "commit", + "root": "a82b6b95f3bf6932c14236eb3f9f9542e5f6ad4d6216cbab8aff42c0c2d6e58d" + } + ], + "items": [ + [ + "a", + [ + 582857.2323208617, + 1020079857923796, + 1152921504606847959 + ] + ], + [ + "a/a", + 0.06132866827788308 + ], + [ + "a/u", + false + ], + [ + "a/user/users", + -0.9172011080678681 + ], + [ + "a/users", + null + ], + [ + "a/é", + false + ], + [ + "a/é/ab", + -0.8878078883743652 + ], + [ + "a/é/x", + [ + [] + ] + ], + [ + "a/😀/x", + {} + ], + [ + "ab", + { + "0": true, + "11": "" + } + ], + [ + "ab/a/user", + { + "": { + "\n中0": true, + " yzz": null, + "1": -6778640052511576, + "y": 191967.5581819946 + }, + "\" c\\x": -7215896155523050, + "\\\u0001\t c": 1152921504606847341 + } + ], + [ + "ab/ab/users", + null + ], + [ + "abc", + -336747.62071530614 + ], + [ + "abc/ab", + { + "中": [ + -142772.81732244696, + "z ab1", + "\\c\"", + null + ], + "￿yab/\n
a": 1152921504606847664 + } + ], + [ + "abc/x", + { + " \n\"": 92.14966227236646 + } + ], + [ + "u", + { + "\t9中c y": { + "\t": 1554006749238746, + "1中z￿😀a1": 2.237744424848274, + "9
中/": -2847243301517036 + }, + "\tcc￿": 3166238056711594 + } + ], + [ + "u/abc/￿", + { + " 
cy": null, + "c\n": -944359.3880934673 + } + ], + [ + "u/user", + null + ], + [ + "u/user/abc", + 3543116664759.4956 + ], + [ + "u/users", + { + "😀\\\u0001é/y/9": "
a中yb9" + } + ], + [ + "user", + 426159.6239946245 + ], + [ + "user/u", + null + ], + [ + "user/é", + {} + ], + [ + "users", + { + "/y\u0001\"": "9x￿z/é\"
" + } + ], + [ + "users/abc/x", + -18446744073709551616 + ], + [ + "users/x", + [ + -18446744073709551616, + { + "": -471541699076132, + "\u0001
\u0001x\"\"": "1\"xy
\\￿\"", + "\na1c": 1892193662427921 + }, + {}, + [ + null, + true + ] + ] + ], + [ + "users/x/a", + {} + ], + [ + "users/é/😀", + [ + -18446744073709551616, + {} + ] + ], + [ + "users/😀", + -1461530824469002 + ], + [ + "x", + -532135.6385488648 + ], + [ + "x/ab", + [ + -18446744073709551616 + ] + ], + [ + "x/users", + 1152921504606847806 + ], + [ + "x/é", + false + ], + [ + "x/￿", + -0.742495377461639 + ], + [ + "x/￿/ab", + [] + ], + [ + "x/😀", + { + "y￿￿ ": { + "x": null, + "x/b a0": "\"
" + }, + "
yc9￿": false, + "😀1": null + } + ], + [ + "é", + 7.442987188463953 + ], + [ + "é/abc/￿", + 79851996.01838501 + ], + [ + "é/users", + { + "": null, + "0x/
\"z\n": "b0\t" + } + ], + [ + "é/é", + 0.9885952857249212 + ], + [ + "￿", + 26437716273043 + ], + [ + "￿/a/é", + true + ], + [ + "￿/users", + null + ], + [ + "￿/😀", + -0.3082166045842658 + ], + [ + "😀", + 1152921504606847269 + ], + [ + "😀/ab", + 5798900.91171178 + ], + [ + "😀/￿", + -18446744073709551616 + ], + [ + "😀/😀/ab", + { + "": { + "1x
xb
\u00011": -18446744073709551616, + "￿\"\t 9\\": "\t", + "😀c \n/b\\": -18446744073709551616 + }, + "0\\9b😀zx": { + "": null, + "\u0001écb90￿😀": null, + "\\": -251140734133170, + "￿": true + }, + "zy": -4365457848758776, + "
": "/0\t99" + } + ] + ] + }, + { + "name": "random-2/swarm", + "addressing": "swarm", + "steps": [ + { + "op": "commit", + "root": null + }, + { + "op": "commit", + "root": null + }, + { + "op": "put", + "key": "user", + "value": "￿0￿b" + }, + { + "op": "put", + "key": "é", + "value": { + "中\té9b": null, + "\t/\\\né": -5810787696403591, + "1az": 1152921504606847153, + "é\"é中z\t\né": -18446744073709551616 + } + }, + { + "op": "put", + "key": "user/é", + "value": { + "\u00019\u0001": [ + 6896831261063722, + 0.09465368642821947, + 7410724576053357, + 62915024.06150362 + ], + "\u0001é\"": false, + "a0xb😀": null, + "😀1xé": -4218229742632357 + } + }, + { + "op": "put", + "key": "é", + "value": null + }, + { + "op": "put", + "key": "users", + "value": true + }, + { + "op": "put", + "key": "users", + "value": 1152921504606847237 + }, + { + "op": "delete", + "key": "user/é", + "existed": true + }, + { + "op": "put", + "key": "x", + "value": null + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "u/ab/abc", + "value": {} + }, + { + "op": "put", + "key": "users/ab/é", + "value": 8264717403952100 + }, + { + "op": "put", + "key": "u/users", + "value": { + "😀\\\u0001é/y/9": "
a中yb9" + } + }, + { + "op": "put", + "key": "u", + "value": { + "b11\tc9c😀": 73854.84734464146 + } + }, + { + "op": "put", + "key": "user", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": true + }, + { + "op": "put", + "key": "a", + "value": true + }, + { + "op": "put", + "key": "￿/é", + "value": 0.5210497969853378 + }, + { + "op": "put", + "key": "é", + "value": [ + -7317684015496443, + { + "": "￿", + " \" a\n": null + }, + true, + null + ] + }, + { + "op": "delete", + "key": "￿/é", + "existed": true + }, + { + "op": "put", + "key": "x/u/users", + "value": "" + }, + { + "op": "put", + "key": "a", + "value": { + "😀": -18446744073709551616 + } + }, + { + "op": "put", + "key": "ab", + "value": true + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "commit", + "root": "12c00ebfee3787a53c6969da41f64cd0d11624f087f61b8f758f4931911e3f8d" + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "put", + "key": "😀", + "value": "\ty\"9\u0001" + }, + { + "op": "put", + "key": "é/😀/ab", + "value": { + "\u0001￿": "\nc😀😀xc\"z" + } + }, + { + "op": "commit", + "root": "5b205214f4d96c174d9b7696efa86415149642fcad06a63967265a861f4b0337" + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "put", + "key": "abc", + "value": { + " xy中\nx/é": 1264464186784.523 + } + }, + { + "op": "put", + "key": "ab/é", + "value": "
\u0001\"中/c" + }, + { + "op": "delete", + "key": "a/é/é", + "existed": false + }, + { + "op": "commit", + "root": "b7e263d9c436d371f7499b21a04af00863adfa5a9505353538b5d43022e4464c" + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "user/u", + "value": null + }, + { + "op": "put", + "key": "x/é/a", + "value": true + }, + { + "op": "put", + "key": "x/x", + "value": 0.22385174241472972 + }, + { + "op": "put", + "key": "a/u", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x", + "value": { + "9é\tac￿\"": 4508207094872803, + "9
ybz\u0001": { + "ya 中": 5841994782166.965, + "0 \u0001
y\u0001中": -18446744073709551616, + "9": -2995669663086030 + } + } + }, + { + "op": "delete", + "key": "x/x", + "existed": true + }, + { + "op": "put", + "key": "a/é/x", + "value": [ + [] + ] + }, + { + "op": "put", + "key": "￿/a", + "value": [ + null, + true + ] + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "😀/abc", + "value": -1044675643202406 + }, + { + "op": "commit", + "root": "ed1bf30ac319a09143af82c887700c98328471721f8a544cd4210e58b5ffd7c1" + }, + { + "op": "put", + "key": "😀", + "value": -0.3475467091950055 + }, + { + "op": "put", + "key": "user/abc", + "value": -6568388855102268 + }, + { + "op": "put", + "key": "é/é/abc", + "value": "😀/\\
 中￿
" + }, + { + "op": "delete", + "key": "x/u/users", + "existed": true + }, + { + "op": "put", + "key": "😀/😀", + "value": false + }, + { + "op": "put", + "key": "users", + "value": true + }, + { + "op": "put", + "key": "é/users/￿", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a", + "value": 1152921504606847233 + }, + { + "op": "put", + "key": "a/a", + "value": 456073.1144339554 + }, + { + "op": "put", + "key": "😀", + "value": "/é" + }, + { + "op": "put", + "key": "x", + "value": { + "": { + "￿/中￿￿c😀": "\u0001\\\tz\n\\é\t", + "": null, + "😀yxézc\\": 5366988803890382 + } + } + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": {} + }, + { + "op": "put", + "key": "users/😀", + "value": -0.6007859178460306 + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": { + "": 68284553343612.805 + } + }, + { + "op": "put", + "key": "user/￿/u", + "value": 8235722742674778 + }, + { + "op": "put", + "key": "abc/é", + "value": -6658090824912385 + }, + { + "op": "put", + "key": "￿", + "value": true + }, + { + "op": "put", + "key": "a/é", + "value": 847266905937133 + }, + { + "op": "delete", + "key": "users/ab/é", + "existed": true + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "put", + "key": "users/ab/😀", + "value": [] + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": [ + false + ] + }, + { + "op": "commit", + "root": "106adbd2a72de612ff4465f915a94c34053824dc0375d6cb5bee5cac24dcdc97" + }, + { + "op": "put", + "key": "user", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "a/u", + "existed": true + }, + { + "op": "put", + "key": "users/😀/ab", + "value": false + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "x/￿", + "value": -0.742495377461639 + }, + { + "op": "commit", + "root": "73b93c6687a11ee8c267b72673b89e8c0a8a93e61cf442a10d5764c172a5ce99" + }, + { + "op": "put", + "key": "a/x/😀", + "value": { + "😀/ cz\"￿": { + " 0": "xaa0" + } + } + }, + { + "op": "commit", + "root": "bf2b5b7eb5f8132c7c410d41039c6ae6970e21aed2ba01400498ba4ad2dcb206" + }, + { + "op": "put", + "key": "abc/😀/u", + "value": { + "": "0y", + "￿\\": false, + "9
c\u0001\u00011y": "0￿/" + } + }, + { + "op": "put", + "key": "é/a/a", + "value": [] + }, + { + "op": "put", + "key": "é/é/u", + "value": { + "\u0001\\ 011": null, + "éc￿中ab\\\n": { + "9x\"é\"é\u0001😀": null + }, + "19aa\u0001by": -3212338297784968 + } + }, + { + "op": "put", + "key": "user", + "value": "
" + }, + { + "op": "put", + "key": "user", + "value": { + "": "9\u0001b中\"/x￿" + } + }, + { + "op": "put", + "key": "abc/ab", + "value": { + "￿yab/\n
a": 1152921504606847664, + "中": [ + -142772.81732244696, + "z ab1", + "\\c\"", + null + ] + } + }, + { + "op": "commit", + "root": "1f5988178f117b92e4b7edfeafd954074094ec7f2ba08e98302ebbbe41668461" + }, + { + "op": "commit", + "root": "1f5988178f117b92e4b7edfeafd954074094ec7f2ba08e98302ebbbe41668461" + }, + { + "op": "put", + "key": "users", + "value": {} + }, + { + "op": "put", + "key": "a/users/￿", + "value": [ + { + "y\n": true, + "b😀zé\t": 198487.23636563774, + "￿y\"ya9z": false + }, + "\u0001\t0\n\n91", + 1152921504606847744, + { + "": 0.6799907687918971, + "91\t\t": 6252070170451755, + "1中中z\t \"": -780526.7944130792, + "￿\t￿é\tc": -18446744073709551616 + } + ] + }, + { + "op": "put", + "key": "abc/￿/abc", + "value": -4802163647997278 + }, + { + "op": "delete", + "key": "é/a/a", + "existed": true + }, + { + "op": "put", + "key": "user/é/ab", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "abc/users", + "value": { + "": -0.7713840304213011, + "\\z/cé": { + "\tx￿": null, + "a \u0001yb": null, + "\\\u0001
￿a": 277760.69799884036 + }, + " \\😀￿\u0001": { + "é": false, + "\"\"z": false + }, + "0😀\t": -204132.43975000083 + } + }, + { + "op": "put", + "key": "user/a", + "value": -279452717319622 + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "commit", + "root": "d69e270b4802dad689d81cd0201f90cc9428db0228cfb2565b2c0e4520b584e2" + }, + { + "op": "commit", + "root": "d69e270b4802dad689d81cd0201f90cc9428db0228cfb2565b2c0e4520b584e2" + }, + { + "op": "put", + "key": "a/user", + "value": "￿aa\"x " + }, + { + "op": "put", + "key": "u/u", + "value": { + "\t
": true + } + }, + { + "op": "commit", + "root": "7c41938bf4a87b63f74045fda2d18ed5b6562d4a2524ef020a610d084a22676e" + }, + { + "op": "put", + "key": "ab/ab/users", + "value": [ + 0.8224996794113091, + 714451.1198869103, + 706927.5322352627, + 7602748445201485 + ] + }, + { + "op": "put", + "key": "u/abc", + "value": false + }, + { + "op": "commit", + "root": "8cb12c0bae40eb92694307869f7b9842a0701d8f50910da9c9ef40d0cbfcd56d" + }, + { + "op": "commit", + "root": "8cb12c0bae40eb92694307869f7b9842a0701d8f50910da9c9ef40d0cbfcd56d" + }, + { + "op": "commit", + "root": "8cb12c0bae40eb92694307869f7b9842a0701d8f50910da9c9ef40d0cbfcd56d" + }, + { + "op": "delete", + "key": "users/ab/😀", + "existed": true + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "delete", + "key": "user/a", + "existed": true + }, + { + "op": "delete", + "key": "￿/a", + "existed": true + }, + { + "op": "delete", + "key": "é/é/abc", + "existed": true + }, + { + "op": "commit", + "root": "b59a34cef0c2f9bf6063104a3e5d37794f8167633b01928578d5e364977f7c8c" + }, + { + "op": "delete", + "key": "u/u", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": null + }, + { + "op": "delete", + "key": "user/ab/x", + "existed": false + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": false + }, + { + "op": "delete", + "key": "a/users/￿", + "existed": true + }, + { + "op": "delete", + "key": "😀/abc", + "existed": true + }, + { + "op": "delete", + "key": "abc/é", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": 8136182827453579 + }, + { + "op": "put", + "key": "a/ab/x", + "value": [] + }, + { + "op": "put", + "key": "x/user/a", + "value": false + }, + { + "op": "put", + "key": "😀", + "value": { + "/": 1.1641864334751701e-05, + "\"中1/": { + "": true + }, + "z\\": -7127687235979652 + } + }, + { + "op": "delete", + "key": "é/users/￿", + "existed": true + }, + { + "op": "put", + "key": "u/users/a", + "value": {} + }, + { + "op": "put", + "key": "user/user", + "value": "9😀1é" + }, + { + "op": "commit", + "root": "24f4c02943ef913b4af15b0ea95753e2eb2c7d64471498aa3641b938cb647f48" + }, + { + "op": "put", + "key": "😀/é", + "value": "z
\n 😀\t" + }, + { + "op": "put", + "key": "￿/u", + "value": null + }, + { + "op": "put", + "key": "😀/u/￿", + "value": [ + [ + false, + -7095750625121347 + ], + 9.745363582635186, + { + " ": -717185.8436805888 + } + ] + }, + { + "op": "commit", + "root": "75b0da6064b3f7c863bff9d2ab72be7272f222b42f33d06593de99ebf80f9096" + }, + { + "op": "put", + "key": "é", + "value": [ + 8.123583920255413, + 3788.350462644594 + ] + }, + { + "op": "put", + "key": "￿/users", + "value": null + }, + { + "op": "delete", + "key": "user/abc", + "existed": true + }, + { + "op": "commit", + "root": "1123b22bcea29d92b3f228adfa38de37ecccee820af3da1ac51fbc1337c4b1d7" + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "put", + "key": "abc", + "value": "9é9b/a\n" + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "u/user", + "value": null + }, + { + "op": "put", + "key": "x/user/users", + "value": null + }, + { + "op": "put", + "key": "x/￿/😀", + "value": { + "xac0a": [ + null, + true, + -8681219311921926, + 1152921504606847576 + ], + "9x\\
z": {}, + "0z￿": { + "": false, + "z￿a": false, + "
1 ": -7503386500974739 + }, + "
y😀": 977924.572013817 + } + }, + { + "op": "put", + "key": "a", + "value": " 中 b " + }, + { + "op": "commit", + "root": "51d696a703e47b179b6a15033a9f21f8911fc8176ee532d4b709a0b45c516a2b" + }, + { + "op": "commit", + "root": "51d696a703e47b179b6a15033a9f21f8911fc8176ee532d4b709a0b45c516a2b" + }, + { + "op": "put", + "key": "x", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "d04526e6411d5528d87e1b26e611b1778db3758fc644d07c278042778df92369" + }, + { + "op": "put", + "key": "￿", + "value": 1884570001370224 + }, + { + "op": "put", + "key": "u", + "value": {} + }, + { + "op": "delete", + "key": "ab/é", + "existed": true + }, + { + "op": "put", + "key": "user/é/a", + "value": true + }, + { + "op": "put", + "key": "x", + "value": "/abax0 " + }, + { + "op": "put", + "key": "a/a/a", + "value": true + }, + { + "op": "put", + "key": "x", + "value": { + "": null, + "x0": 0.4279955369099355, + "z": "￿cz" + } + }, + { + "op": "delete", + "key": "😀/users", + "existed": false + }, + { + "op": "delete", + "key": "abc/😀/u", + "existed": true + }, + { + "op": "delete", + "key": "a/user", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": -831238.3231132485 + }, + { + "op": "put", + "key": "é/a", + "value": 20162852062012.543 + }, + { + "op": "commit", + "root": "584cb3072b8026c481bc13ed4f7b2f320aa26791cb0568906bd5c9759e848f45" + }, + { + "op": "delete", + "key": "a/a/a", + "existed": true + }, + { + "op": "put", + "key": "a/￿/é", + "value": [] + }, + { + "op": "delete", + "key": "users", + "existed": true + }, + { + "op": "put", + "key": "é/a", + "value": 3429741327020023 + }, + { + "op": "put", + "key": "users", + "value": "y\\\n😀by
b" + }, + { + "op": "put", + "key": "ab/￿/user", + "value": 8975440110461416 + }, + { + "op": "put", + "key": "é", + "value": [ + -1480753958253816, + "a
0", + "", + 0.9184893929983005 + ] + }, + { + "op": "put", + "key": "￿/a/é", + "value": true + }, + { + "op": "put", + "key": "user/ab/x", + "value": 816313151259329 + }, + { + "op": "commit", + "root": "f484a40e435afdd52b935ee66137dd9c615655ca910f809f24f659bf6471387e" + }, + { + "op": "put", + "key": "users", + "value": "/￿￿😀
y" + }, + { + "op": "put", + "key": "abc/abc/users", + "value": 4.252731372064616e-12 + }, + { + "op": "put", + "key": "users/x/é", + "value": true + }, + { + "op": "put", + "key": "ab/abc", + "value": { + "0/中a￿a￿": true, + "\n￿\t😀9\n": null + } + }, + { + "op": "put", + "key": "user", + "value": 1152921504606847197 + }, + { + "op": "commit", + "root": "7ca07d72ebdfea07b0a1d068cd3324efa9783d07299437e08a217eacff819867" + }, + { + "op": "put", + "key": "ab/abc", + "value": null + }, + { + "op": "put", + "key": "user/user", + "value": "\u0001é中中" + }, + { + "op": "put", + "key": "x/￿/😀", + "value": { + "\\": false, + "yx/\\\u0001😀c\n": { + "\u0001a中\u0001": "
\n0￿c", + "": false, + "a b\n": 3657686048913665 + }, + "\né9": { + "a9z ": null, + "a0z": -4006062950343854, + "/\\😀": true + } + } + }, + { + "op": "put", + "key": "é", + "value": "90" + }, + { + "op": "commit", + "root": "abcc25e326cb75abcf7e348059dc95a14f990356bcd59a878e79b9d0e51bbc7c" + }, + { + "op": "commit", + "root": "abcc25e326cb75abcf7e348059dc95a14f990356bcd59a878e79b9d0e51bbc7c" + }, + { + "op": "delete", + "key": "user/é/a", + "existed": true + }, + { + "op": "put", + "key": "￿", + "value": { + "9￿1\n\t1
": { + "\u0001\\a": -4500260163213891, + "/0😀
": -0.8120118552736082 + }, + "中1
0": true, + "é\t/é": -5601067476714478 + } + }, + { + "op": "put", + "key": "😀/😀", + "value": [ + "\t\t", + null + ] + }, + { + "op": "commit", + "root": "fe7d422addf21da9771ea36bcedc5e8e87a38f44e0a7c39c3130971f2e6709e3" + }, + { + "op": "delete", + "key": "abc/users", + "existed": true + }, + { + "op": "put", + "key": "😀/user", + "value": "
\nbz\"" + }, + { + "op": "delete", + "key": "x/user/users", + "existed": true + }, + { + "op": "commit", + "root": "89a1a4757156bd56dc6f450446cfdf41fc82509d0d1bf990c02dcd756e20c426" + }, + { + "op": "put", + "key": "é/abc/￿", + "value": 79851996.01838501 + }, + { + "op": "delete", + "key": "é", + "existed": true + }, + { + "op": "put", + "key": "😀/users/￿", + "value": -0.8636252350646845 + }, + { + "op": "put", + "key": "é/abc", + "value": 7981756839298357 + }, + { + "op": "delete", + "key": "é/abc", + "existed": true + }, + { + "op": "delete", + "key": "user/user", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": 1152921504606847204 + }, + { + "op": "put", + "key": "é", + "value": false + }, + { + "op": "put", + "key": "é", + "value": 0.00029552700270127027 + }, + { + "op": "put", + "key": "a", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "😀", + "value": [ + [ + null, + null, + "y/中1111\n", + -18446744073709551616 + ], + -3422922777074208 + ] + }, + { + "op": "delete", + "key": "￿/u", + "existed": true + }, + { + "op": "delete", + "key": "￿/x", + "existed": false + }, + { + "op": "put", + "key": "￿", + "value": { + "b\n\u0001\\c": true, + "bz1\"\\/￿": -18446744073709551616, + "\t😀\n\nyé￿y": 811883.0582577325, + "c\\中\tz/": { + "": null + } + } + }, + { + "op": "put", + "key": "￿", + "value": "xx" + }, + { + "op": "put", + "key": "u", + "value": 1152921504606847016 + }, + { + "op": "put", + "key": "￿/user/abc", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": 975825.6783753687 + }, + { + "op": "put", + "key": "😀/￿/users", + "value": 1298350660019815 + }, + { + "op": "put", + "key": "x/users", + "value": {} + }, + { + "op": "put", + "key": "😀/u/a", + "value": {} + }, + { + "op": "commit", + "root": "c6349ca1babc082e1ce8de16011dd0fa13d1e872699366b73fcbfbb494bbbe8f" + }, + { + "op": "delete", + "key": "ab/abc", + "existed": true + }, + { + "op": "put", + "key": "x/😀", + "value": { + "y￿￿ ": { + "x": null, + "x/b a0": "\"
" + }, + "
yc9￿": false, + "😀1": null + } + }, + { + "op": "commit", + "root": "c825ad5c2a49c6eaf7007128d097a7b301945fe1c318b72a3db5bc8c1830aba2" + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "ab", + "value": { + "😀b": -199200.11279452336, + "/c 0cc": -18446744073709551616, + "ay
\"\" \u0001é": null + } + }, + { + "op": "put", + "key": "￿/x", + "value": -4457290127836679 + }, + { + "op": "commit", + "root": "3c59bd869ffaab129ca04b84afef708305a596533c72db4142dc7bbbd292ae85" + }, + { + "op": "put", + "key": "user/é", + "value": {} + }, + { + "op": "commit", + "root": "fd32d1c9a1d1d7a6e45e3186aeff3ff29306f5123d096729ce62a5647fd1013d" + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "commit", + "root": "9ed7bbbbe5778e14e41c2029408c3792517ba848574b752f4ba2fb433b87053c" + }, + { + "op": "put", + "key": "é/ab", + "value": true + }, + { + "op": "delete", + "key": "😀/😀", + "existed": true + }, + { + "op": "delete", + "key": "😀/￿/users", + "existed": true + }, + { + "op": "delete", + "key": "￿/é", + "existed": false + }, + { + "op": "delete", + "key": "😀/u/a", + "existed": true + }, + { + "op": "delete", + "key": "😀", + "existed": false + }, + { + "op": "delete", + "key": "u/ab/abc", + "existed": true + }, + { + "op": "delete", + "key": "😀/u/￿", + "existed": true + }, + { + "op": "delete", + "key": "￿", + "existed": true + }, + { + "op": "delete", + "key": "x/￿/😀", + "existed": true + }, + { + "op": "delete", + "key": "😀/users/￿", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": [ + 683501.1972635924, + {}, + 0.19109490539973928 + ] + }, + { + "op": "put", + "key": "users/abc/x", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "x", + "value": [ + {} + ] + }, + { + "op": "delete", + "key": "￿/x", + "existed": true + }, + { + "op": "commit", + "root": "bfc7ea0697759fa7ce8dd7f88b41788ce610ea578b7115726f99e32a932288f2" + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "put", + "key": "x/abc", + "value": "中中" + }, + { + "op": "put", + "key": "abc", + "value": [ + { + "zcy ": "\"中\u0001c￿9", + " x中😀9 x": "￿x￿\n

ba", + "
0/0\t中": 6430599794759789, + "/\"\\/a中é": -18446744073709551616 + }, + 0.6293664603917879, + 58024990733594.93, + { + "/a\\\n
 ": null + } + ] + }, + { + "op": "put", + "key": "ab/ab/users", + "value": null + }, + { + "op": "commit", + "root": "113f2e24df95fdd1764a1e7cc94f838088dcf573bcf71e37b7d179cd2d50f3e1" + }, + { + "op": "put", + "key": "é/é", + "value": 0.9885952857249212 + }, + { + "op": "put", + "key": "a", + "value": { + "中bxc\n \n/": "0
中\nc
x0", + "": {}, + "￿z￿😀c\"": { + "￿😀\\9a0a": 0.10172587710287129, + "
\n\"z0 \\": 27745454571002.375, + "中y\u0001中é": false, + "\"": "" + } + } + }, + { + "op": "commit", + "root": "52fef9d76c7ffd420b4f1a29d4f3dfa83a557c3b0fa689db3f9ee4a4fa7277e6" + }, + { + "op": "commit", + "root": "52fef9d76c7ffd420b4f1a29d4f3dfa83a557c3b0fa689db3f9ee4a4fa7277e6" + }, + { + "op": "put", + "key": "😀", + "value": "xx😀/a\u0001😀" + }, + { + "op": "put", + "key": "a", + "value": "z0\\9中😀￿" + }, + { + "op": "put", + "key": "ab/é", + "value": "\"90
😀\\z" + }, + { + "op": "put", + "key": "😀", + "value": { + "
中\u0001a0": "" + } + }, + { + "op": "commit", + "root": "1f91b71c99f2be8f8286a6c37afadb4e42038dc13b0396d120032d3d27a18381" + }, + { + "op": "put", + "key": "user", + "value": true + }, + { + "op": "put", + "key": "users", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "a/é/ab", + "value": -0.8878078883743652 + }, + { + "op": "put", + "key": "user", + "value": { + "z\\\\1": 617485.7139944278, + "": false, + "z
1y\"
b": 1152921504606847838 + } + }, + { + "op": "put", + "key": "a", + "value": "a9" + }, + { + "op": "put", + "key": "😀/user/é", + "value": -8402171115934331 + }, + { + "op": "put", + "key": "abc", + "value": 5583860529011803 + }, + { + "op": "commit", + "root": "2f8bc65ad9bb12a2ffd1b20c4847ecfabf0117a147cf7309841bf562581def58" + }, + { + "op": "put", + "key": "user/￿", + "value": [] + }, + { + "op": "delete", + "key": "￿/user/abc", + "existed": true + }, + { + "op": "commit", + "root": "c9bf2aaa5d36afe7157cd4a1c5f547a73cba82279ba5d0c18693ee110823bc84" + }, + { + "op": "commit", + "root": "c9bf2aaa5d36afe7157cd4a1c5f547a73cba82279ba5d0c18693ee110823bc84" + }, + { + "op": "put", + "key": "x/u", + "value": 3745147572404717 + }, + { + "op": "put", + "key": "a", + "value": { + "0\\": [ + null, + -206589143784449 + ], + "a\n ": "" + } + }, + { + "op": "delete", + "key": "u/abc", + "existed": true + }, + { + "op": "delete", + "key": "a/x/😀", + "existed": true + }, + { + "op": "commit", + "root": "c41a913bf40207cc9edd3af6bc2d219208744d8b7a2eeeb48bc089bd77743f3b" + }, + { + "op": "delete", + "key": "user/￿/u", + "existed": true + }, + { + "op": "commit", + "root": "3aa0dd79a9b880f07f67148f623811a10b1189284357568156625945da1accad" + }, + { + "op": "delete", + "key": "u/ab", + "existed": false + }, + { + "op": "put", + "key": "abc", + "value": "yc😀" + }, + { + "op": "delete", + "key": "user", + "existed": true + }, + { + "op": "delete", + "key": "users/x/é", + "existed": true + }, + { + "op": "commit", + "root": "55af260959ecbcf2c8287029aece71721a9e031640271e320f13bebd06ee80f3" + }, + { + "op": "put", + "key": "é", + "value": 1152921504606847920 + }, + { + "op": "delete", + "key": "users/😀", + "existed": true + }, + { + "op": "put", + "key": "￿/😀", + "value": -0.3082166045842658 + }, + { + "op": "put", + "key": "x/ab", + "value": [ + -18446744073709551616 + ] + }, + { + "op": "put", + "key": "x/user/user", + "value": false + }, + { + "op": "commit", + "root": "592bc070abcd7c5d3da2be0d4b9360059eba31edcb5f3ecde65e9e87a94b3ec4" + }, + { + "op": "put", + "key": "😀", + "value": "中\"0😀bz/中" + }, + { + "op": "put", + "key": "abc/x", + "value": { + " \n\"": 92.14966227236646 + } + }, + { + "op": "commit", + "root": "af889f82cdb37d2ba4b662aacb4935c02d37979b310e17cb66a8f69f0a126ff0" + }, + { + "op": "put", + "key": "x/abc", + "value": "中😀\"a\t" + }, + { + "op": "delete", + "key": "😀/user", + "existed": true + }, + { + "op": "delete", + "key": "abc/abc/users", + "existed": true + }, + { + "op": "delete", + "key": "a", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": -18446744073709551616 + }, + { + "op": "commit", + "root": "2fe6c4f24d9e32306fba454f2413b0eac462e17df7d56fdb5fbdbcb627a93e9a" + }, + { + "op": "delete", + "key": "a/ab/x", + "existed": true + }, + { + "op": "put", + "key": "user", + "value": 426159.6239946245 + }, + { + "op": "commit", + "root": "4bb32d774848ece97cb7213c4fe0260b2f4be1ced8b49ff889736eca3f7903fb" + }, + { + "op": "delete", + "key": "a/a", + "existed": true + }, + { + "op": "delete", + "key": "a/￿/é", + "existed": true + }, + { + "op": "delete", + "key": "users/😀/ab", + "existed": true + }, + { + "op": "delete", + "key": "x/a", + "existed": false + }, + { + "op": "delete", + "key": "é/ab", + "existed": true + }, + { + "op": "put", + "key": "é/￿", + "value": 0.07561877714596606 + }, + { + "op": "put", + "key": "ab/a", + "value": { + "\ncc": true, + "\\\n\tza": { + "\nc\n中z 1": 1152921504606847697 + } + } + }, + { + "op": "delete", + "key": "u/users/a", + "existed": true + }, + { + "op": "put", + "key": "x/abc", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "u", + "value": {} + }, + { + "op": "put", + "key": "😀/user", + "value": -3472000912914411 + }, + { + "op": "put", + "key": "a/a", + "value": 0.06132866827788308 + }, + { + "op": "put", + "key": "a/user/users", + "value": -0.9172011080678681 + }, + { + "op": "delete", + "key": "😀/user", + "existed": true + }, + { + "op": "put", + "key": "￿/￿/abc", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "x", + "existed": true + }, + { + "op": "put", + "key": "abc/user", + "value": {} + }, + { + "op": "commit", + "root": "4f6432d85250fdf263d9af81add321ebb14a54d26a45355dfd8b7c09a6415fce" + }, + { + "op": "delete", + "key": "user/￿", + "existed": true + }, + { + "op": "commit", + "root": "3e84385523803958e33096cb156795165659052d7a9e34ab5bfd23d3c47efb13" + }, + { + "op": "put", + "key": "u/user/abc", + "value": 3543116664759.4956 + }, + { + "op": "delete", + "key": "abc/￿/abc", + "existed": true + }, + { + "op": "delete", + "key": "user/é/ab", + "existed": true + }, + { + "op": "put", + "key": "u/a/abc", + "value": [ + false, + "c😀
x" + ] + }, + { + "op": "put", + "key": "u", + "value": { + "
x\"\u0001￿é9": false + } + }, + { + "op": "commit", + "root": "c6215918180b5fd6d91fb4399227042d70dd7b2d4f55e9e4a3bdf9fd8c7aae64" + }, + { + "op": "delete", + "key": "abc/user", + "existed": true + }, + { + "op": "put", + "key": "u/é", + "value": {} + }, + { + "op": "put", + "key": "a/users", + "value": null + }, + { + "op": "put", + "key": "￿", + "value": -18446744073709551616 + }, + { + "op": "put", + "key": "users/x", + "value": [ + -18446744073709551616, + { + "\na1c": 1892193662427921, + "": -471541699076132, + "\u0001
\u0001x\"\"": "1\"xy
\\￿\"" + }, + {}, + [ + null, + true + ] + ] + }, + { + "op": "delete", + "key": "é/é/u", + "existed": true + }, + { + "op": "put", + "key": "u", + "value": 0.0699746458055941 + }, + { + "op": "put", + "key": "a/é", + "value": false + }, + { + "op": "delete", + "key": "x/user/user", + "existed": true + }, + { + "op": "delete", + "key": "é/a", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": { + "": null + } + }, + { + "op": "put", + "key": "😀", + "value": { + "a1bc
\u0001": null, + "￿\u000191": {}, + "1中￿": true, + "": [ + "x中9￿", + "", + null, + -7682083297527074 + ] + } + }, + { + "op": "put", + "key": "ab/a/user", + "value": { + "": { + "1": -6778640052511576, + " yzz": null, + "y": 191967.5581819946, + "\n中0": true + }, + "\" c\\x": -7215896155523050, + "\\\u0001\t c": 1152921504606847341 + } + }, + { + "op": "commit", + "root": "e0f7d29f83e627e782799b7639e430ad4d5a33210d88a3f4dcfb42395c52f7e8" + }, + { + "op": "delete", + "key": "x/é/a", + "existed": true + }, + { + "op": "put", + "key": "x/é", + "value": false + }, + { + "op": "delete", + "key": "abc", + "existed": true + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "delete", + "key": "ab/a", + "existed": true + }, + { + "op": "put", + "key": "x/users", + "value": 1152921504606847806 + }, + { + "op": "commit", + "root": "3c8e4e2b0dd5f8cbc7eaaa23957ac8dd7084eda5abd9d693dfb8a45ec683129c" + }, + { + "op": "put", + "key": "users", + "value": { + "é": "\n ￿\"中/", + "\u0001\n\n\t": true, + "
": [], + "
b\tzy\"0c": true + } + }, + { + "op": "put", + "key": "￿", + "value": 26437716273043 + }, + { + "op": "delete", + "key": "x/abc", + "existed": true + }, + { + "op": "put", + "key": "a/😀/x", + "value": {} + }, + { + "op": "put", + "key": "😀", + "value": {} + }, + { + "op": "put", + "key": "u/abc/￿", + "value": { + " 
cy": null, + "c\n": -944359.3880934673 + } + }, + { + "op": "delete", + "key": "ab/é", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": { + "/": true, + "éz9y ": null + } + }, + { + "op": "commit", + "root": "7b0b2ed327216e9aa9d6d085f42844a49d63f151f95e82c888ea7336a11f12d8" + }, + { + "op": "commit", + "root": "7b0b2ed327216e9aa9d6d085f42844a49d63f151f95e82c888ea7336a11f12d8" + }, + { + "op": "put", + "key": "😀", + "value": false + }, + { + "op": "put", + "key": "😀/ab", + "value": 5798900.91171178 + }, + { + "op": "put", + "key": "a/u", + "value": false + }, + { + "op": "commit", + "root": "1ba7e3ce1486e4a595999a02ebac3eabff9c7e9bdf13ba93e634468e3cc6cd3b" + }, + { + "op": "commit", + "root": "1ba7e3ce1486e4a595999a02ebac3eabff9c7e9bdf13ba93e634468e3cc6cd3b" + }, + { + "op": "put", + "key": "a", + "value": [ + { + "": true, + "/😀": -5655752702209879, + "\u0001\u0001\"\"😀
": -460660283449905 + }, + null, + "a
90b
a
" + ] + }, + { + "op": "put", + "key": "x", + "value": -532135.6385488648 + }, + { + "op": "delete", + "key": "😀/user/é", + "existed": true + }, + { + "op": "put", + "key": "users/x/a", + "value": {} + }, + { + "op": "delete", + "key": "x/user/a", + "existed": true + }, + { + "op": "put", + "key": "users", + "value": { + "/y\u0001\"": "9x￿z/é\"
" + } + }, + { + "op": "delete", + "key": "é/￿", + "existed": true + }, + { + "op": "delete", + "key": "u", + "existed": true + }, + { + "op": "commit", + "root": "c41adbec64523fe3f106c910ffbf15fc9cce842559c09f4d30ff3fff1b58bb95" + }, + { + "op": "put", + "key": "ab", + "value": { + "0": true, + "11": "" + } + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "delete", + "key": "￿/user/ab", + "existed": false + }, + { + "op": "put", + "key": "😀/x", + "value": {} + }, + { + "op": "put", + "key": "😀", + "value": [] + }, + { + "op": "put", + "key": "x/￿/ab", + "value": [] + }, + { + "op": "put", + "key": "u", + "value": { + "\t9中c y": { + "1中z￿😀a1": 2.237744424848274, + "9
中/": -2847243301517036, + "\t": 1554006749238746 + }, + "\tcc￿": 3166238056711594 + } + }, + { + "op": "put", + "key": "abc", + "value": -336747.62071530614 + }, + { + "op": "put", + "key": "users/é/😀", + "value": [ + -18446744073709551616, + {} + ] + }, + { + "op": "delete", + "key": "😀/x", + "existed": true + }, + { + "op": "delete", + "key": "￿/￿/abc", + "existed": true + }, + { + "op": "delete", + "key": "é/😀/ab", + "existed": true + }, + { + "op": "delete", + "key": "😀/é", + "existed": true + }, + { + "op": "delete", + "key": "u/é", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": -6224794831260817 + }, + { + "op": "put", + "key": "😀/￿", + "value": -18446744073709551616 + }, + { + "op": "delete", + "key": "user/ab/x", + "existed": true + }, + { + "op": "delete", + "key": "😀", + "existed": true + }, + { + "op": "put", + "key": "a", + "value": "\\\tbc￿" + }, + { + "op": "put", + "key": "😀/😀/ab", + "value": { + "zy": -4365457848758776, + "
": "/0\t99", + "0\\9b😀zx": { + "\\": -251140734133170, + "￿": true, + "\u0001écb90￿😀": null, + "": null + }, + "": { + "😀c \n/b\\": -18446744073709551616, + "1x
xb
\u00011": -18446744073709551616, + "￿\"\t 9\\": "\t" + } + } + }, + { + "op": "put", + "key": "😀", + "value": [ + null, + { + "cc\t😀\"bx0": null + } + ] + }, + { + "op": "put", + "key": "users/😀", + "value": -1461530824469002 + }, + { + "op": "delete", + "key": "u/a/abc", + "existed": true + }, + { + "op": "delete", + "key": "ab/￿/user", + "existed": true + }, + { + "op": "put", + "key": "é", + "value": 1152921504606847348 + }, + { + "op": "delete", + "key": "x/u", + "existed": true + }, + { + "op": "put", + "key": "é/users", + "value": { + "": null, + "0x/
\"z\n": "b0\t" + } + }, + { + "op": "put", + "key": "😀", + "value": [ + -18446744073709551616, + false, + true + ] + }, + { + "op": "put", + "key": "a", + "value": [ + 582857.2323208617, + 1020079857923796, + 1152921504606847959 + ] + }, + { + "op": "put", + "key": "😀", + "value": 1152921504606847269 + }, + { + "op": "commit", + "root": "688722546d599e1a6925095422d375baa4d133e5f6670cf75969f7215971d3fe" + }, + { + "op": "put", + "key": "é", + "value": 7.442987188463953 + }, + { + "op": "commit", + "root": "2ab87f68d034916fa3d96b5b1bcd7594aa0b51fed3ca6367fad21a61720a61be" + } + ], + "items": [ + [ + "a", + [ + 582857.2323208617, + 1020079857923796, + 1152921504606847959 + ] + ], + [ + "a/a", + 0.06132866827788308 + ], + [ + "a/u", + false + ], + [ + "a/user/users", + -0.9172011080678681 + ], + [ + "a/users", + null + ], + [ + "a/é", + false + ], + [ + "a/é/ab", + -0.8878078883743652 + ], + [ + "a/é/x", + [ + [] + ] + ], + [ + "a/😀/x", + {} + ], + [ + "ab", + { + "0": true, + "11": "" + } + ], + [ + "ab/a/user", + { + "": { + "\n中0": true, + " yzz": null, + "1": -6778640052511576, + "y": 191967.5581819946 + }, + "\" c\\x": -7215896155523050, + "\\\u0001\t c": 1152921504606847341 + } + ], + [ + "ab/ab/users", + null + ], + [ + "abc", + -336747.62071530614 + ], + [ + "abc/ab", + { + "中": [ + -142772.81732244696, + "z ab1", + "\\c\"", + null + ], + "￿yab/\n
a": 1152921504606847664 + } + ], + [ + "abc/x", + { + " \n\"": 92.14966227236646 + } + ], + [ + "u", + { + "\t9中c y": { + "\t": 1554006749238746, + "1中z￿😀a1": 2.237744424848274, + "9
中/": -2847243301517036 + }, + "\tcc￿": 3166238056711594 + } + ], + [ + "u/abc/￿", + { + " 
cy": null, + "c\n": -944359.3880934673 + } + ], + [ + "u/user", + null + ], + [ + "u/user/abc", + 3543116664759.4956 + ], + [ + "u/users", + { + "😀\\\u0001é/y/9": "
a中yb9" + } + ], + [ + "user", + 426159.6239946245 + ], + [ + "user/u", + null + ], + [ + "user/é", + {} + ], + [ + "users", + { + "/y\u0001\"": "9x￿z/é\"
" + } + ], + [ + "users/abc/x", + -18446744073709551616 + ], + [ + "users/x", + [ + -18446744073709551616, + { + "": -471541699076132, + "\u0001
\u0001x\"\"": "1\"xy
\\￿\"", + "\na1c": 1892193662427921 + }, + {}, + [ + null, + true + ] + ] + ], + [ + "users/x/a", + {} + ], + [ + "users/é/😀", + [ + -18446744073709551616, + {} + ] + ], + [ + "users/😀", + -1461530824469002 + ], + [ + "x", + -532135.6385488648 + ], + [ + "x/ab", + [ + -18446744073709551616 + ] + ], + [ + "x/users", + 1152921504606847806 + ], + [ + "x/é", + false + ], + [ + "x/￿", + -0.742495377461639 + ], + [ + "x/￿/ab", + [] + ], + [ + "x/😀", + { + "y￿￿ ": { + "x": null, + "x/b a0": "\"
" + }, + "
yc9￿": false, + "😀1": null + } + ], + [ + "é", + 7.442987188463953 + ], + [ + "é/abc/￿", + 79851996.01838501 + ], + [ + "é/users", + { + "": null, + "0x/
\"z\n": "b0\t" + } + ], + [ + "é/é", + 0.9885952857249212 + ], + [ + "￿", + 26437716273043 + ], + [ + "￿/a/é", + true + ], + [ + "￿/users", + null + ], + [ + "￿/😀", + -0.3082166045842658 + ], + [ + "😀", + 1152921504606847269 + ], + [ + "😀/ab", + 5798900.91171178 + ], + [ + "😀/￿", + -18446744073709551616 + ], + [ + "😀/😀/ab", + { + "": { + "1x
xb
\u00011": -18446744073709551616, + "￿\"\t 9\\": "\t", + "😀c \n/b\\": -18446744073709551616 + }, + "0\\9b😀zx": { + "": null, + "\u0001écb90￿😀": null, + "\\": -251140734133170, + "￿": true + }, + "zy": -4365457848758776, + "
": "/0\t99" + } + ] + ] + } + ] +} diff --git a/js/test/fixtures/swarm-addresses.json b/js/test/fixtures/swarm-addresses.json new file mode 100644 index 0000000..3e74f2b --- /dev/null +++ b/js/test/fixtures/swarm-addresses.json @@ -0,0 +1,66 @@ +{ + "note": "Generated by js/tools/make_fixtures.py from the Python reference implementation; do not edit by hand.", + "data": "byte i = (i * 31 + 7) & 0xff", + "cases": [ + { + "size": 0, + "ref": "b34ca8c22b9e982354f9c7f50b470d66db428d880c8a904d5fe4ec9713171526" + }, + { + "size": 1, + "ref": "b073f3782b5a9db35d5642aacb734064fc421fd277a882b6fd177c744d0ad10d" + }, + { + "size": 31, + "ref": "3026a29b50c0254cab38b3800171f8968e524610c5f1aa69464c49a0b8377182" + }, + { + "size": 32, + "ref": "cfd2d9919025db2e91299b9d0f2e1c253665363c3f3b5acbd8329b7db5db96b1" + }, + { + "size": 33, + "ref": "fb8c581d61795b75d51208020f9c8261f8149aa729d005e54301710cd694b191" + }, + { + "size": 64, + "ref": "f90150e6470e6a22b0ee50d40f87736ba9e38f966b187b67dc137bdbaa9bc121" + }, + { + "size": 4095, + "ref": "f137b04ca81dc566928fcec4f5e37c10a0c27b30ddb1d535ea4c2d4aebdcaa03" + }, + { + "size": 4096, + "ref": "ee1ccdcda5351ae064dcaf614b583e2e7a8a42ca4df5f5f5eb3b7559ac24b0ec" + }, + { + "size": 4097, + "ref": "bd3357b006abe623abfef563dd07f5cd9d496e301f404ad3d7767e20abc24ef6" + }, + { + "size": 8192, + "ref": "ed190bdd51f9ca81eefd1817a605caf3136bf9f24d32e52e97cb3f521ad7434b" + }, + { + "size": 8193, + "ref": "0849b8d414cb0204e005368bf4feb95a8da39ee4d5e7e400e251d1520e68680e" + }, + { + "size": 524288, + "ref": "e3a273803ee6200d328d15f8a838eb5cc9124e4bb15114b517ae0a7b406cbcdf" + }, + { + "size": 524289, + "ref": "d02da6e441f6fc652c6724357fe23faff6c454a6dae152f3c7c88d894be98fe1" + }, + { + "size": 528384, + "ref": "21165bba025f93fb98a2f1feef21396960af463d5c48c70187758ec98828898a" + }, + { + "size": 1048581, + "ref": "8cc2eab297e99267865d7c3c273531acc6296b8ce715f7dec5b4fdfe0a9dc772" + } + ] +} diff --git a/js/test/helpers.js b/js/test/helpers.js new file mode 100644 index 0000000..1e547a2 --- /dev/null +++ b/js/test/helpers.js @@ -0,0 +1,34 @@ +import { readFileSync } from 'node:fs'; +import { parseJson } from '../src/canonical.js'; +import { MemoryBytesStore } from '../src/stores.js'; + +/** A fixture written by tools/make_fixtures.py (big integers come back as BigInt). */ +export function fixture(name) { + return parseJson(readFileSync(new URL(`./fixtures/${name}`, import.meta.url), 'utf8')); +} + +/** The deterministic test data the fixtures' Swarm addresses were computed over. */ +export function pattern(size) { + const out = new Uint8Array(size); + for (let i = 0; i < size; i++) out[i] = (i * 31 + 7) & 0xff; + return out; +} + +export function storeFor(addressing) { + return new MemoryBytesStore({ addressing }); +} + +/** Apply fixture steps (put / delete / commit) to a store. */ +export async function apply(store, steps) { + for (const step of steps) { + if (step.op === 'put') store.put(step.key, step.value); + else if (step.op === 'delete') await store.delete(step.key); + else await store.commit(); + } +} + +export async function collect(asyncIterable) { + const out = []; + for await (const x of asyncIterable) out.push(x); + return out; +} diff --git a/js/test/live.test.js b/js/test/live.test.js new file mode 100644 index 0000000..5b057f2 --- /dev/null +++ b/js/test/live.test.js @@ -0,0 +1,29 @@ +// Live: read a store that Python published on Swarm, through a node with +// Bee's /bytes API (Bee, or Ant inside Freedom Browser). Skipped unless +// +// RECORDSTORE_BEE_URL=http://localhost:1633 npm test +// +// RECORDSTORE_LIVE_ROOT picks the root (default: OntoDAG's `space` pack, +// core v14, kept on Swarm by swarm-keep). Every blob is checked against its +// reference as it is read. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { BeeBytesStore, RecordStore } from '../src/index.js'; + +const url = process.env.RECORDSTORE_BEE_URL; +const root = process.env.RECORDSTORE_LIVE_ROOT + || '67f6be2688e5fb7ca85a463c3d2929dad2b177bb3edb6bb910e8fa110598cff3'; + +test('reads a store Python published on Swarm', { skip: !url && 'set RECORDSTORE_BEE_URL to run' }, async () => { + const store = RecordStore.at(root, new BeeBytesStore(url)); + const started = Date.now(); + const keys = []; + for await (const key of store.keys()) keys.push(key); + const sorted = [...keys].sort((a, b) => (Buffer.from(a) < Buffer.from(b) ? -1 : 1)); + assert.ok(keys.length > 0, 'the store has records'); + assert.deepEqual(keys, sorted, 'keys arrive in UTF-8 byte order'); + const sample = await store.get(keys[Math.floor(keys.length / 2)]); + assert.notEqual(sample, undefined); + console.log(`# ${keys.length} keys in ${Date.now() - started} ms; e.g. ${JSON.stringify(keys.slice(0, 3))}`); +}); diff --git a/js/test/merge.test.js b/js/test/merge.test.js new file mode 100644 index 0000000..6d6f2e9 --- /dev/null +++ b/js/test/merge.test.js @@ -0,0 +1,42 @@ +// Cross-language: three-way merges reach Python's merged roots, and +// conflicts are the same keys. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { ABSENT, DELETE, MergeConflict, RecordStore } from '../src/recordstore.js'; +import { apply, fixture, storeFor } from './helpers.js'; + +const { cases } = fixture('merges.json'); + +const oursWins = (_key, _base, ours) => (ours === ABSENT ? DELETE : ours); + +for (const c of cases) { + test(`merge ${c.name}`, async () => { + const blobs = storeFor(c.addressing); + const baseStore = new RecordStore(blobs); + await apply(baseStore, c.base); + assert.equal(baseStore.root, c.roots.base); + const branch = async (steps) => { + const s = new RecordStore(blobs, { root: c.roots.base }); + await apply(s, steps); + return s.commit(); + }; + const ours = await branch(c.ours); + const theirs = await branch(c.theirs); + assert.equal(ours, c.roots.ours); + assert.equal(theirs, c.roots.theirs); + + if (c.merged !== undefined) { + assert.equal(await RecordStore.merge(blobs, c.roots.base, ours, theirs), c.merged); + // Commutative without a resolver. + assert.equal(await RecordStore.merge(blobs, c.roots.base, theirs, ours), c.merged); + } else { + await assert.rejects(RecordStore.merge(blobs, c.roots.base, ours, theirs), (e) => { + assert.ok(e instanceof MergeConflict); + assert.deepEqual(e.conflicts.slice().sort(), c.conflicts); + return true; + }); + assert.equal(await RecordStore.merge(blobs, c.roots.base, ours, theirs, oursWins), c.merged_ours_wins); + } + }); +} diff --git a/js/test/store.test.js b/js/test/store.test.js new file mode 100644 index 0000000..8240f3e --- /dev/null +++ b/js/test/store.test.js @@ -0,0 +1,48 @@ +// Cross-language: replay the operation sequences Python ran and reach the +// same root at every commit, then read stores Python wrote. + +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { RecordStore } from '../src/recordstore.js'; +import { fromHex } from '../src/bytes.js'; +import { collect, fixture, storeFor } from './helpers.js'; + +const { cases } = fixture('stores.json'); + +for (const c of cases) { + test(`${c.name}: every commit reaches Python's root`, async () => { + const store = new RecordStore(storeFor(c.addressing)); + let commit = 0; + for (const step of c.steps) { + if (step.op === 'put') { + store.put(step.key, step.value); + } else if (step.op === 'delete') { + const present = await store.has(step.key); + assert.equal(await store.delete(step.key), present); + } else { + commit++; + assert.equal(await store.commit(), step.root, `commit ${commit}`); + } + } + assert.deepEqual(await collect(store.items()), c.items); + }); +} + +test('reads a store Python wrote, from its blobs alone', async () => { + for (const c of cases.filter((x) => x.blobs)) { + const blobs = storeFor(c.addressing); + for (const [ref, hex] of Object.entries(c.blobs)) { + assert.equal(await blobs.put(fromHex(hex)), ref, 'the blob hashes to its reference'); + } + const commits = c.steps.filter((s) => s.op === 'commit'); + // The fourth commit holds the unicode keys; read it as a snapshot. + const snapshot = RecordStore.at(commits[3].root, blobs); + const items = await collect(snapshot.items()); + assert.deepEqual(items.map(([k]) => k), ['a/b/c', 'users/alice', 'users/bob', 'userz', 'é', '￿', '😀']); + assert.deepEqual(await snapshot.get('users/alice'), { name: 'Alice', role: 'admin' }); + assert.equal(await snapshot.get('a/b/c'), null); + assert.equal(await snapshot.get('u'), undefined); + const last = RecordStore.at(commits.at(-1).root, blobs); + assert.deepEqual(await collect(last.items()), c.items); + } +}); diff --git a/js/tools/make_fixtures.py b/js/tools/make_fixtures.py new file mode 100644 index 0000000..de0d116 --- /dev/null +++ b/js/tools/make_fixtures.py @@ -0,0 +1,383 @@ +#!/usr/bin/env python3 +"""Generate the cross-language fixtures the JavaScript port must reproduce. + +The Python package is the reference implementation of recordstore's wire +format. This script runs it (the source in ``src/``, not an installed copy) +and writes what it produces to ``js/test/fixtures/``: canonical encodings, +Swarm content addresses, the root after every commit of scripted and random +stores, and three-way merges. The JavaScript tests replay the same operations +and must reach byte-identical bytes and roots. + + python3 js/tools/make_fixtures.py # rewrite the fixtures + python3 js/tools/make_fixtures.py --check # exit 1 if they are stale + +Needs swarmfs (with keccak) for the Swarm-addressed cases, as +``addressing="swarm"`` does. +""" + +from __future__ import annotations + +import json +import os +import random +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(os.path.dirname(HERE)) +OUT = os.path.join(REPO, "js", "test", "fixtures") +sys.path.insert(0, os.path.join(REPO, "src")) + +from recordstore import ( # noqa: E402 (the path insert must come first) + ABSENT, DELETE, MemoryBytesStore, MergeConflict, RecordStore, + canonical_bytes, +) +from recordstore.recordstore import _encode_value, _swarm_ref # noqa: E402 + + +class SwarmMemoryStore: + """MemoryBytesStore under Swarm (BMT) addressing: the references a Bee + node gives these blobs, computed offline.""" + + def __init__(self): + self.blobs = {} + + def put(self, data: bytes) -> str: + ref = _swarm_ref(data) + self.blobs[ref] = data + return ref + + def get(self, ref: str) -> bytes: + try: + return self.blobs[ref] + except KeyError: + raise KeyError(f"reference not found: {ref}") from None + + +def new_store(addressing: str): + return SwarmMemoryStore() if addressing == "swarm" else MemoryBytesStore() + + +# --------------------------------------------------------------------------- +# Canonical encoding +# --------------------------------------------------------------------------- + +def canonical_cases(): + """Values whose canonical bytes the JavaScript encoder must match. + + Integral floats below 2**53 (``1.0``) are left out on purpose: JavaScript + has one number type and writes them as integers (see the JS README). + """ + strings = [ + "", "plain", 'quote " and backslash \\', "tab\tnewline\ncr\r", + "controls \x00\x01\x1f\x7f", "\b\f", "é 中文 😀", "

", + "￿", "/", "", + ] + ints = [0, 1, -1, 42, 2**31, -(2**31) - 1, 2**53 - 1, -(2**53 - 1), + 2**53, 2**63, -(2**70), 10**30] + floats = [0.1, -2.5, 0.30000000000000004, 1.0000000000000002, 123.456, + 1e-4, 0.0001234, 1e-5, 1e-7, 2.5e-5, 1.5e-300, 5e-324, + 1e15 + 0.5, 1e16, 1e21, 1e22, 1.7976931348623157e308, + 9007199254740992.0, -1e100, 3.14159265358979, 100.5] + objects = [ + {}, + [], + {"b": 1, "a": 2}, + {"z": 1, "é": 2, "😀": 3, "￿": 4, "A": 5, "a": 6, "": 7, + "aa": 8}, + {"nested": {"list": [1, "two", None, True, False, {"x": []}], + "empty": {}}}, + [None, True, False, 0, "", [], {}], + {"users": [{"name": "Alice", "role": "admin"}, + {"name": "Bob", "tags": ["x", "y"]}]}, + ] + values = ([None, True, False] + strings + ints + floats + objects) + return [{"value": v, "hex": canonical_bytes(v).hex()} for v in values] + + +def value_blob_cases(): + """The record envelope ``{"rsv":1,"val":...}`` around a few values.""" + return [{"value": v, "hex": _encode_value(v).hex()} + for v in [None, "x", 1, 0.5, {"a": [1, 2]}]] + + +# --------------------------------------------------------------------------- +# Swarm content addresses +# --------------------------------------------------------------------------- + +def pattern(size: int) -> bytes: + """Deterministic test data; the JS tests rebuild it the same way.""" + return bytes(((i * 31) + 7) & 0xFF for i in range(size)) + + +def swarm_cases(): + sizes = [0, 1, 31, 32, 33, 64, 4095, 4096, 4097, 8192, 8193, + 4096 * 128, 4096 * 128 + 1, 4096 * 129, 4096 * 256 + 5] + return [{"size": n, "ref": _swarm_ref(pattern(n))} for n in sizes] + + +# --------------------------------------------------------------------------- +# Stores: scripted and random operation sequences +# --------------------------------------------------------------------------- + +def scripted_ops(): + """Shared prefixes, splits and the merges a delete forces.""" + return [ + ("put", "users/alice", {"name": "Alice", "role": "admin"}), + ("put", "users/bob", {"name": "Bob"}), + ("put", "users/al", "a proper prefix of alice"), + ("put", "u", 1), + ("put", "userz", [1, 2, 3]), + ("commit",), + ("delete", "users/al"), + ("commit",), + ("put", "users/alice", {"name": "Alice", "role": "admin"}), # same value + ("commit",), + ("put", "é", "two bytes"), + ("put", "😀", "four bytes"), + ("put", "￿", "three bytes, sorts before the emoji"), + ("put", "a/b/c", None), + ("delete", "u"), + ("delete", "never-was"), + ("commit",), + ("put", "tmp", "staged then deleted"), + ("delete", "tmp"), + ("put", "users/bob", {"name": "Bob", "age": 0.5}), + ("commit",), + ("delete", "users/alice"), + ("delete", "users/bob"), + ("delete", "userz"), + ("commit",), + ] + + +SEGMENTS = ["users", "u", "user", "a", "ab", "abc", "é", "😀", "￿", "x"] +ALPHABET = list("abcxyz019 /\"\\") + ["\n", "\t", "\x01", "é", "中", "😀", + "￿", "
"] + + +def random_key(rng): + if rng.random() < 0.2: + return rng.choice(SEGMENTS) + return "/".join(rng.choice(SEGMENTS) for _ in range(rng.randint(1, 3))) + + +def random_string(rng): + return "".join(rng.choice(ALPHABET) for _ in range(rng.randint(0, 8))) + + +def random_float(rng): + while True: + f = rng.choice([rng.uniform(-1e6, 1e6), rng.uniform(-1, 1), + rng.uniform(1, 10) * 10 ** rng.randint(-12, 25)]) + if not f.is_integer(): + return f + + +def random_value(rng, depth=0): + kind = rng.randint(0, 8 if depth < 2 else 5) + if kind == 0: + return None + if kind == 1: + return rng.random() < 0.5 + if kind == 2: + return rng.randint(-(2**53 - 1), 2**53 - 1) + if kind == 3: + return random_float(rng) + if kind == 4: + return random_string(rng) + if kind == 5: + return rng.choice([2**60 + rng.randint(0, 999), -(2**64)]) + if kind in (6, 7): + return {random_string(rng): random_value(rng, depth + 1) + for _ in range(rng.randint(0, 4))} + return [random_value(rng, depth + 1) for _ in range(rng.randint(0, 4))] + + +def random_ops(seed, count): + rng = random.Random(seed) + ops, live = [], set() + for _ in range(count): + roll = rng.random() + if roll < 0.6: + key = random_key(rng) + ops.append(("put", key, random_value(rng))) + live.add(key) + elif roll < 0.85: + if live and rng.random() < 0.85: + key = rng.choice(sorted(live)) + live.discard(key) + else: + key = random_key(rng) + live.discard(key) + ops.append(("delete", key)) + else: + ops.append(("commit",)) + ops.append(("commit",)) + return ops + + +def run(ops, addressing): + """Replay `ops` on the Python store; return the steps as fixture data + (each commit carrying the root it must produce, each delete whether the + key existed) and the final store.""" + blobs = new_store(addressing) + store = RecordStore(blobs) + steps = [] + for op in ops: + if op[0] == "put": + store.put(op[1], op[2]) + steps.append({"op": "put", "key": op[1], "value": op[2]}) + elif op[0] == "delete": + try: + store.delete(op[1]) + existed = True + except KeyError: + existed = False + steps.append({"op": "delete", "key": op[1], "existed": existed}) + else: + steps.append({"op": "commit", "root": store.commit()}) + return steps, store, blobs + + +def store_cases(): + cases = [] + for name, ops in [("scripted", scripted_ops()), + ("random-1", random_ops(1, 400)), + ("random-2", random_ops(2, 400))]: + for addressing in ("sha256", "swarm"): + steps, store, blobs = run(ops, addressing) + case = { + "name": f"{name}/{addressing}", + "addressing": addressing, + "steps": steps, + "items": [[k, v] for k, v in store.items()], + } + if name == "scripted": + # Every blob, so the JS reader is tested on bytes Python wrote. + case["blobs"] = {ref: data.hex() + for ref, data in sorted(blobs.blobs.items())} + cases.append(case) + return cases + + +# --------------------------------------------------------------------------- +# Three-way merges +# --------------------------------------------------------------------------- + +def ours_wins(_key, _base, ours, _theirs): + """The resolver the JS tests implement identically.""" + return DELETE if ours is ABSENT else ours + + +def merge_cases(): + base_ops = [("put", f"k{i}", {"n": i}) for i in range(20)] + [ + ("put", "shared/a", 1), ("put", "shared/b", 2), ("commit",)] + scenarios = [ + ("disjoint", + [("put", "k1", "ours"), ("delete", "k2"), ("put", "new/ours", 1)], + [("put", "k3", "theirs"), ("delete", "k4"), ("put", "new/theirs", 2)]), + ("same-change", + [("put", "k5", "both"), ("delete", "k6")], + [("put", "k5", "both"), ("delete", "k6"), ("put", "k7", "theirs")]), + ("conflict", + [("put", "k8", "ours"), ("delete", "shared/a"), ("put", "x", 1)], + [("put", "k8", "theirs"), ("put", "shared/a", 99), ("put", "x", 2)]), + ] + cases = [] + for addressing in ("sha256", "swarm"): + for name, ours_ops, theirs_ops in scenarios: + blobs = new_store(addressing) + base_store = RecordStore(blobs) + for op in base_ops: + _apply(base_store, op) + base = base_store.root + + def branch(ops): + s = RecordStore(blobs, root=base) + for op in ops: + _apply(s, op) + return s.commit() + + ours, theirs = branch(ours_ops), branch(theirs_ops) + case = {"name": f"{name}/{addressing}", "addressing": addressing, + "base": [_op_json(op) for op in base_ops], + "ours": [_op_json(op) for op in ours_ops], + "theirs": [_op_json(op) for op in theirs_ops], + "roots": {"base": base, "ours": ours, "theirs": theirs}} + try: + case["merged"] = RecordStore.merge(blobs, base, ours, theirs) + except MergeConflict as e: + case["conflicts"] = sorted(e.conflicts) + case["merged_ours_wins"] = RecordStore.merge( + blobs, base, ours, theirs, resolver=ours_wins) + cases.append(case) + return cases + + +def _apply(store, op): + if op[0] == "put": + store.put(op[1], op[2]) + elif op[0] == "delete": + store.delete(op[1]) + else: + store.commit() + + +def _op_json(op): + if op[0] == "put": + return {"op": "put", "key": op[1], "value": op[2]} + if op[0] == "delete": + return {"op": "delete", "key": op[1]} + return {"op": "commit"} + + +# --------------------------------------------------------------------------- + +def fixtures(): + note = ("Generated by js/tools/make_fixtures.py from the Python " + "reference implementation; do not edit by hand.") + return { + "canonical.json": {"note": note, "values": canonical_cases(), + "records": value_blob_cases()}, + "swarm-addresses.json": {"note": note, + "data": "byte i = (i * 31 + 7) & 0xff", + "cases": swarm_cases()}, + "stores.json": {"note": note, "cases": store_cases()}, + "merges.json": {"note": note, "resolver": "ours wins: DELETE if ours " + "is ABSENT, else ours", "cases": merge_cases()}, + } + + +def render(obj) -> str: + return json.dumps(obj, ensure_ascii=False, indent=1, allow_nan=False) + "\n" + + +def main(argv): + check = "--check" in argv + stale = [] + os.makedirs(OUT, exist_ok=True) + for name, obj in fixtures().items(): + path = os.path.join(OUT, name) + text = render(obj) + if check: + try: + with open(path, encoding="utf-8") as f: + if f.read() != text: + stale.append(name) + except FileNotFoundError: + stale.append(name) + else: + with open(path, "w", encoding="utf-8") as f: + f.write(text) + print(f"wrote {os.path.relpath(path, REPO)}") + if stale: + print("stale fixtures (rerun js/tools/make_fixtures.py): " + + ", ".join(stale), file=sys.stderr) + return 1 + if check: + print("fixtures are current") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/js/vendor/js-sha3/LICENSE b/js/vendor/js-sha3/LICENSE new file mode 100644 index 0000000..c1ee2fb --- /dev/null +++ b/js/vendor/js-sha3/LICENSE @@ -0,0 +1,20 @@ +Copyright 2015-2023 Chen, Yi-Cyuan + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/js/vendor/js-sha3/PROVENANCE.md b/js/vendor/js-sha3/PROVENANCE.md new file mode 100644 index 0000000..7e90812 --- /dev/null +++ b/js/vendor/js-sha3/PROVENANCE.md @@ -0,0 +1,23 @@ +# Vendored: js-sha3 0.9.3 + +Source: the `js-sha3` npm package, version 0.9.3 +(https://github.com/emn178/js-sha3), MIT license (see LICENSE). + +Vendored subset: `src/sha3.js` only (unmodified), plus the `index.js` ESM +facade. Copied from swarmlite's `js/vendor/js-sha3/`, which vendored it +first; same file, same version. + +SHA-256 of `sha3.js`: +`6378cc474ad0a0db3e88c0c67b7bd4a154e1b318f44dbbd13927f6e220efaa38` + +Used for keccak256 in Swarm (BMT) content addressing (`src/address.js`). + +Vendored (rather than npm-installed) so that a page published on Swarm can +import recordstore with relative URLs only: no registry, no CDN, no build. + +Sanity check: `keccak256('hello')` = +`1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8` +(the well-known Ethereum keccak vector; asserted in `test/address.test.js`). + +To upgrade: `npm pack js-sha3`, extract, copy `src/sha3.js` and LICENSE, +update the version and hash here, run `npm test`. diff --git a/js/vendor/js-sha3/index.js b/js/vendor/js-sha3/index.js new file mode 100644 index 0000000..483fe8e --- /dev/null +++ b/js/vendor/js-sha3/index.js @@ -0,0 +1,12 @@ +// ESM facade over the unmodified UMD build: in ES-module scope neither +// `module` nor `define` exists, so sha3.js falls through to attaching +// its methods to the global object (window / self / global); re-export +// the one method swarmlite uses. +import './sha3.js'; + +/** keccak256 over a string or byte array -> lowercase hex (no 0x). */ +export const keccak256 = globalThis.keccak_256; + +/** keccak256 over a byte array -> Uint8Array (for BMT/SOC hashing). */ +export const keccak256Bytes = (data) => + Uint8Array.from(globalThis.keccak_256.array(data)); diff --git a/js/vendor/js-sha3/sha3.js b/js/vendor/js-sha3/sha3.js new file mode 100644 index 0000000..52c9334 --- /dev/null +++ b/js/vendor/js-sha3/sha3.js @@ -0,0 +1,662 @@ +/** + * [js-sha3]{@link https://github.com/emn178/js-sha3} + * + * @version 0.9.3 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2015-2023 + * @license MIT + */ +/*jslint bitwise: true */ +(function () { + 'use strict'; + + var INPUT_ERROR = 'input is invalid type'; + var FINALIZE_ERROR = 'finalize already called'; + var WINDOW = typeof window === 'object'; + var root = WINDOW ? window : {}; + if (root.JS_SHA3_NO_WINDOW) { + WINDOW = false; + } + var WEB_WORKER = !WINDOW && typeof self === 'object'; + var NODE_JS = !root.JS_SHA3_NO_NODE_JS && typeof process === 'object' && process.versions && process.versions.node; + if (NODE_JS) { + root = global; + } else if (WEB_WORKER) { + root = self; + } + var COMMON_JS = !root.JS_SHA3_NO_COMMON_JS && typeof module === 'object' && module.exports; + var AMD = typeof define === 'function' && define.amd; + var ARRAY_BUFFER = !root.JS_SHA3_NO_ARRAY_BUFFER && typeof ArrayBuffer !== 'undefined'; + var HEX_CHARS = '0123456789abcdef'.split(''); + var SHAKE_PADDING = [31, 7936, 2031616, 520093696]; + var CSHAKE_PADDING = [4, 1024, 262144, 67108864]; + var KECCAK_PADDING = [1, 256, 65536, 16777216]; + var PADDING = [6, 1536, 393216, 100663296]; + var SHIFT = [0, 8, 16, 24]; + var RC = [1, 0, 32898, 0, 32906, 2147483648, 2147516416, 2147483648, 32907, 0, 2147483649, + 0, 2147516545, 2147483648, 32777, 2147483648, 138, 0, 136, 0, 2147516425, 0, + 2147483658, 0, 2147516555, 0, 139, 2147483648, 32905, 2147483648, 32771, + 2147483648, 32770, 2147483648, 128, 2147483648, 32778, 0, 2147483658, 2147483648, + 2147516545, 2147483648, 32896, 2147483648, 2147483649, 0, 2147516424, 2147483648]; + var BITS = [224, 256, 384, 512]; + var SHAKE_BITS = [128, 256]; + var OUTPUT_TYPES = ['hex', 'buffer', 'arrayBuffer', 'array', 'digest']; + var CSHAKE_BYTEPAD = { + '128': 168, + '256': 136 + }; + + + var isArray = root.JS_SHA3_NO_NODE_JS || !Array.isArray + ? function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + } + : Array.isArray; + + var isView = (ARRAY_BUFFER && (root.JS_SHA3_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView)) + ? function (obj) { + return typeof obj === 'object' && obj.buffer && obj.buffer.constructor === ArrayBuffer; + } + : ArrayBuffer.isView; + + // [message: string, isString: bool] + var formatMessage = function (message) { + var type = typeof message; + if (type === 'string') { + return [message, true]; + } + if (type !== 'object' || message === null) { + throw new Error(INPUT_ERROR); + } + if (ARRAY_BUFFER && message.constructor === ArrayBuffer) { + return [new Uint8Array(message), false]; + } + if (!isArray(message) && !isView(message)) { + throw new Error(INPUT_ERROR); + } + return [message, false]; + } + + var empty = function (message) { + return formatMessage(message)[0].length === 0; + }; + + var cloneArray = function (array) { + var newArray = []; + for (var i = 0; i < array.length; ++i) { + newArray[i] = array[i]; + } + return newArray; + } + + var createOutputMethod = function (bits, padding, outputType) { + return function (message) { + return new Keccak(bits, padding, bits).update(message)[outputType](); + }; + }; + + var createShakeOutputMethod = function (bits, padding, outputType) { + return function (message, outputBits) { + return new Keccak(bits, padding, outputBits).update(message)[outputType](); + }; + }; + + var createCshakeOutputMethod = function (bits, padding, outputType) { + return function (message, outputBits, n, s) { + return methods['cshake' + bits].update(message, outputBits, n, s)[outputType](); + }; + }; + + var createKmacOutputMethod = function (bits, padding, outputType) { + return function (key, message, outputBits, s) { + return methods['kmac' + bits].update(key, message, outputBits, s)[outputType](); + }; + }; + + var createOutputMethods = function (method, createMethod, bits, padding) { + for (var i = 0; i < OUTPUT_TYPES.length; ++i) { + var type = OUTPUT_TYPES[i]; + method[type] = createMethod(bits, padding, type); + } + return method; + }; + + var createMethod = function (bits, padding) { + var method = createOutputMethod(bits, padding, 'hex'); + method.create = function () { + return new Keccak(bits, padding, bits); + }; + method.update = function (message) { + return method.create().update(message); + }; + return createOutputMethods(method, createOutputMethod, bits, padding); + }; + + var createShakeMethod = function (bits, padding) { + var method = createShakeOutputMethod(bits, padding, 'hex'); + method.create = function (outputBits) { + return new Keccak(bits, padding, outputBits); + }; + method.update = function (message, outputBits) { + return method.create(outputBits).update(message); + }; + return createOutputMethods(method, createShakeOutputMethod, bits, padding); + }; + + var createCshakeMethod = function (bits, padding) { + var w = CSHAKE_BYTEPAD[bits]; + var method = createCshakeOutputMethod(bits, padding, 'hex'); + method.create = function (outputBits, n, s) { + if (empty(n) && empty(s)) { + return methods['shake' + bits].create(outputBits); + } else { + return new Keccak(bits, padding, outputBits).bytepad([n, s], w); + } + }; + method.update = function (message, outputBits, n, s) { + return method.create(outputBits, n, s).update(message); + }; + return createOutputMethods(method, createCshakeOutputMethod, bits, padding); + }; + + var createKmacMethod = function (bits, padding) { + var w = CSHAKE_BYTEPAD[bits]; + var method = createKmacOutputMethod(bits, padding, 'hex'); + method.create = function (key, outputBits, s) { + return new Kmac(bits, padding, outputBits).bytepad(['KMAC', s], w).bytepad([key], w); + }; + method.update = function (key, message, outputBits, s) { + return method.create(key, outputBits, s).update(message); + }; + return createOutputMethods(method, createKmacOutputMethod, bits, padding); + }; + + var algorithms = [ + { name: 'keccak', padding: KECCAK_PADDING, bits: BITS, createMethod: createMethod }, + { name: 'sha3', padding: PADDING, bits: BITS, createMethod: createMethod }, + { name: 'shake', padding: SHAKE_PADDING, bits: SHAKE_BITS, createMethod: createShakeMethod }, + { name: 'cshake', padding: CSHAKE_PADDING, bits: SHAKE_BITS, createMethod: createCshakeMethod }, + { name: 'kmac', padding: CSHAKE_PADDING, bits: SHAKE_BITS, createMethod: createKmacMethod } + ]; + + var methods = {}, methodNames = []; + + for (var i = 0; i < algorithms.length; ++i) { + var algorithm = algorithms[i]; + var bits = algorithm.bits; + for (var j = 0; j < bits.length; ++j) { + var methodName = algorithm.name + '_' + bits[j]; + methodNames.push(methodName); + methods[methodName] = algorithm.createMethod(bits[j], algorithm.padding); + if (algorithm.name !== 'sha3') { + var newMethodName = algorithm.name + bits[j]; + methodNames.push(newMethodName); + methods[newMethodName] = methods[methodName]; + } + } + } + + function Keccak(bits, padding, outputBits) { + this.blocks = []; + this.s = []; + this.padding = padding; + this.outputBits = outputBits; + this.reset = true; + this.finalized = false; + this.block = 0; + this.start = 0; + this.blockCount = (1600 - (bits << 1)) >> 5; + this.byteCount = this.blockCount << 2; + this.outputBlocks = outputBits >> 5; + this.extraBytes = (outputBits & 31) >> 3; + + for (var i = 0; i < 50; ++i) { + this.s[i] = 0; + } + } + + Keccak.prototype.update = function (message) { + if (this.finalized) { + throw new Error(FINALIZE_ERROR); + } + var result = formatMessage(message); + message = result[0]; + var isString = result[1]; + var blocks = this.blocks, byteCount = this.byteCount, length = message.length, + blockCount = this.blockCount, index = 0, s = this.s, i, code; + + while (index < length) { + if (this.reset) { + this.reset = false; + blocks[0] = this.block; + for (i = 1; i < blockCount + 1; ++i) { + blocks[i] = 0; + } + } + if (isString) { + for (i = this.start; index < length && i < byteCount; ++index) { + code = message.charCodeAt(index); + if (code < 0x80) { + blocks[i >> 2] |= code << SHIFT[i++ & 3]; + } else if (code < 0x800) { + blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else if (code < 0xd800 || code >= 0xe000) { + blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (message.charCodeAt(++index) & 0x3ff)); + blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3]; + blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3]; + } + } + } else { + for (i = this.start; index < length && i < byteCount; ++index) { + blocks[i >> 2] |= message[index] << SHIFT[i++ & 3]; + } + } + this.lastByteIndex = i; + if (i >= byteCount) { + this.start = i - byteCount; + this.block = blocks[blockCount]; + for (i = 0; i < blockCount; ++i) { + s[i] ^= blocks[i]; + } + f(s); + this.reset = true; + } else { + this.start = i; + } + } + return this; + }; + + Keccak.prototype.encode = function (x, right) { + var o = x & 255, n = 1; + var bytes = [o]; + x = x >> 8; + o = x & 255; + while (o > 0) { + bytes.unshift(o); + x = x >> 8; + o = x & 255; + ++n; + } + if (right) { + bytes.push(n); + } else { + bytes.unshift(n); + } + this.update(bytes); + return bytes.length; + }; + + Keccak.prototype.encodeString = function (str) { + var result = formatMessage(str); + str = result[0]; + var isString = result[1]; + var bytes = 0, length = str.length; + if (isString) { + for (var i = 0; i < str.length; ++i) { + var code = str.charCodeAt(i); + if (code < 0x80) { + bytes += 1; + } else if (code < 0x800) { + bytes += 2; + } else if (code < 0xd800 || code >= 0xe000) { + bytes += 3; + } else { + code = 0x10000 + (((code & 0x3ff) << 10) | (str.charCodeAt(++i) & 0x3ff)); + bytes += 4; + } + } + } else { + bytes = length; + } + bytes += this.encode(bytes * 8); + this.update(str); + return bytes; + }; + + Keccak.prototype.bytepad = function (strs, w) { + var bytes = this.encode(w); + for (var i = 0; i < strs.length; ++i) { + bytes += this.encodeString(strs[i]); + } + var paddingBytes = (w - bytes % w) % w; + var zeros = []; + zeros.length = paddingBytes; + this.update(zeros); + return this; + }; + + Keccak.prototype.finalize = function () { + if (this.finalized) { + return; + } + this.finalized = true; + var blocks = this.blocks, i = this.lastByteIndex, blockCount = this.blockCount, s = this.s; + blocks[i >> 2] |= this.padding[i & 3]; + if (this.lastByteIndex === this.byteCount) { + blocks[0] = blocks[blockCount]; + for (i = 1; i < blockCount + 1; ++i) { + blocks[i] = 0; + } + } + blocks[blockCount - 1] |= 0x80000000; + for (i = 0; i < blockCount; ++i) { + s[i] ^= blocks[i]; + } + f(s); + }; + + Keccak.prototype.toString = Keccak.prototype.hex = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var hex = '', block; + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + block = s[i]; + hex += HEX_CHARS[(block >> 4) & 0x0F] + HEX_CHARS[block & 0x0F] + + HEX_CHARS[(block >> 12) & 0x0F] + HEX_CHARS[(block >> 8) & 0x0F] + + HEX_CHARS[(block >> 20) & 0x0F] + HEX_CHARS[(block >> 16) & 0x0F] + + HEX_CHARS[(block >> 28) & 0x0F] + HEX_CHARS[(block >> 24) & 0x0F]; + } + if (j % blockCount === 0) { + s = cloneArray(s); + f(s); + i = 0; + } + } + if (extraBytes) { + block = s[i]; + hex += HEX_CHARS[(block >> 4) & 0x0F] + HEX_CHARS[block & 0x0F]; + if (extraBytes > 1) { + hex += HEX_CHARS[(block >> 12) & 0x0F] + HEX_CHARS[(block >> 8) & 0x0F]; + } + if (extraBytes > 2) { + hex += HEX_CHARS[(block >> 20) & 0x0F] + HEX_CHARS[(block >> 16) & 0x0F]; + } + } + return hex; + }; + + Keccak.prototype.arrayBuffer = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var bytes = this.outputBits >> 3; + var buffer; + if (extraBytes) { + buffer = new ArrayBuffer((outputBlocks + 1) << 2); + } else { + buffer = new ArrayBuffer(bytes); + } + var array = new Uint32Array(buffer); + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + array[j] = s[i]; + } + if (j % blockCount === 0) { + s = cloneArray(s); + f(s); + } + } + if (extraBytes) { + array[j] = s[i]; + buffer = buffer.slice(0, bytes); + } + return buffer; + }; + + Keccak.prototype.buffer = Keccak.prototype.arrayBuffer; + + Keccak.prototype.digest = Keccak.prototype.array = function () { + this.finalize(); + + var blockCount = this.blockCount, s = this.s, outputBlocks = this.outputBlocks, + extraBytes = this.extraBytes, i = 0, j = 0; + var array = [], offset, block; + while (j < outputBlocks) { + for (i = 0; i < blockCount && j < outputBlocks; ++i, ++j) { + offset = j << 2; + block = s[i]; + array[offset] = block & 0xFF; + array[offset + 1] = (block >> 8) & 0xFF; + array[offset + 2] = (block >> 16) & 0xFF; + array[offset + 3] = (block >> 24) & 0xFF; + } + if (j % blockCount === 0) { + s = cloneArray(s); + f(s); + } + } + if (extraBytes) { + offset = j << 2; + block = s[i]; + array[offset] = block & 0xFF; + if (extraBytes > 1) { + array[offset + 1] = (block >> 8) & 0xFF; + } + if (extraBytes > 2) { + array[offset + 2] = (block >> 16) & 0xFF; + } + } + return array; + }; + + function Kmac(bits, padding, outputBits) { + Keccak.call(this, bits, padding, outputBits); + } + + Kmac.prototype = new Keccak(); + + Kmac.prototype.finalize = function () { + this.encode(this.outputBits, true); + return Keccak.prototype.finalize.call(this); + }; + + var f = function (s) { + var h, l, n, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, + b0, b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, + b18, b19, b20, b21, b22, b23, b24, b25, b26, b27, b28, b29, b30, b31, b32, b33, + b34, b35, b36, b37, b38, b39, b40, b41, b42, b43, b44, b45, b46, b47, b48, b49; + for (n = 0; n < 48; n += 2) { + c0 = s[0] ^ s[10] ^ s[20] ^ s[30] ^ s[40]; + c1 = s[1] ^ s[11] ^ s[21] ^ s[31] ^ s[41]; + c2 = s[2] ^ s[12] ^ s[22] ^ s[32] ^ s[42]; + c3 = s[3] ^ s[13] ^ s[23] ^ s[33] ^ s[43]; + c4 = s[4] ^ s[14] ^ s[24] ^ s[34] ^ s[44]; + c5 = s[5] ^ s[15] ^ s[25] ^ s[35] ^ s[45]; + c6 = s[6] ^ s[16] ^ s[26] ^ s[36] ^ s[46]; + c7 = s[7] ^ s[17] ^ s[27] ^ s[37] ^ s[47]; + c8 = s[8] ^ s[18] ^ s[28] ^ s[38] ^ s[48]; + c9 = s[9] ^ s[19] ^ s[29] ^ s[39] ^ s[49]; + + h = c8 ^ ((c2 << 1) | (c3 >>> 31)); + l = c9 ^ ((c3 << 1) | (c2 >>> 31)); + s[0] ^= h; + s[1] ^= l; + s[10] ^= h; + s[11] ^= l; + s[20] ^= h; + s[21] ^= l; + s[30] ^= h; + s[31] ^= l; + s[40] ^= h; + s[41] ^= l; + h = c0 ^ ((c4 << 1) | (c5 >>> 31)); + l = c1 ^ ((c5 << 1) | (c4 >>> 31)); + s[2] ^= h; + s[3] ^= l; + s[12] ^= h; + s[13] ^= l; + s[22] ^= h; + s[23] ^= l; + s[32] ^= h; + s[33] ^= l; + s[42] ^= h; + s[43] ^= l; + h = c2 ^ ((c6 << 1) | (c7 >>> 31)); + l = c3 ^ ((c7 << 1) | (c6 >>> 31)); + s[4] ^= h; + s[5] ^= l; + s[14] ^= h; + s[15] ^= l; + s[24] ^= h; + s[25] ^= l; + s[34] ^= h; + s[35] ^= l; + s[44] ^= h; + s[45] ^= l; + h = c4 ^ ((c8 << 1) | (c9 >>> 31)); + l = c5 ^ ((c9 << 1) | (c8 >>> 31)); + s[6] ^= h; + s[7] ^= l; + s[16] ^= h; + s[17] ^= l; + s[26] ^= h; + s[27] ^= l; + s[36] ^= h; + s[37] ^= l; + s[46] ^= h; + s[47] ^= l; + h = c6 ^ ((c0 << 1) | (c1 >>> 31)); + l = c7 ^ ((c1 << 1) | (c0 >>> 31)); + s[8] ^= h; + s[9] ^= l; + s[18] ^= h; + s[19] ^= l; + s[28] ^= h; + s[29] ^= l; + s[38] ^= h; + s[39] ^= l; + s[48] ^= h; + s[49] ^= l; + + b0 = s[0]; + b1 = s[1]; + b32 = (s[11] << 4) | (s[10] >>> 28); + b33 = (s[10] << 4) | (s[11] >>> 28); + b14 = (s[20] << 3) | (s[21] >>> 29); + b15 = (s[21] << 3) | (s[20] >>> 29); + b46 = (s[31] << 9) | (s[30] >>> 23); + b47 = (s[30] << 9) | (s[31] >>> 23); + b28 = (s[40] << 18) | (s[41] >>> 14); + b29 = (s[41] << 18) | (s[40] >>> 14); + b20 = (s[2] << 1) | (s[3] >>> 31); + b21 = (s[3] << 1) | (s[2] >>> 31); + b2 = (s[13] << 12) | (s[12] >>> 20); + b3 = (s[12] << 12) | (s[13] >>> 20); + b34 = (s[22] << 10) | (s[23] >>> 22); + b35 = (s[23] << 10) | (s[22] >>> 22); + b16 = (s[33] << 13) | (s[32] >>> 19); + b17 = (s[32] << 13) | (s[33] >>> 19); + b48 = (s[42] << 2) | (s[43] >>> 30); + b49 = (s[43] << 2) | (s[42] >>> 30); + b40 = (s[5] << 30) | (s[4] >>> 2); + b41 = (s[4] << 30) | (s[5] >>> 2); + b22 = (s[14] << 6) | (s[15] >>> 26); + b23 = (s[15] << 6) | (s[14] >>> 26); + b4 = (s[25] << 11) | (s[24] >>> 21); + b5 = (s[24] << 11) | (s[25] >>> 21); + b36 = (s[34] << 15) | (s[35] >>> 17); + b37 = (s[35] << 15) | (s[34] >>> 17); + b18 = (s[45] << 29) | (s[44] >>> 3); + b19 = (s[44] << 29) | (s[45] >>> 3); + b10 = (s[6] << 28) | (s[7] >>> 4); + b11 = (s[7] << 28) | (s[6] >>> 4); + b42 = (s[17] << 23) | (s[16] >>> 9); + b43 = (s[16] << 23) | (s[17] >>> 9); + b24 = (s[26] << 25) | (s[27] >>> 7); + b25 = (s[27] << 25) | (s[26] >>> 7); + b6 = (s[36] << 21) | (s[37] >>> 11); + b7 = (s[37] << 21) | (s[36] >>> 11); + b38 = (s[47] << 24) | (s[46] >>> 8); + b39 = (s[46] << 24) | (s[47] >>> 8); + b30 = (s[8] << 27) | (s[9] >>> 5); + b31 = (s[9] << 27) | (s[8] >>> 5); + b12 = (s[18] << 20) | (s[19] >>> 12); + b13 = (s[19] << 20) | (s[18] >>> 12); + b44 = (s[29] << 7) | (s[28] >>> 25); + b45 = (s[28] << 7) | (s[29] >>> 25); + b26 = (s[38] << 8) | (s[39] >>> 24); + b27 = (s[39] << 8) | (s[38] >>> 24); + b8 = (s[48] << 14) | (s[49] >>> 18); + b9 = (s[49] << 14) | (s[48] >>> 18); + + s[0] = b0 ^ (~b2 & b4); + s[1] = b1 ^ (~b3 & b5); + s[10] = b10 ^ (~b12 & b14); + s[11] = b11 ^ (~b13 & b15); + s[20] = b20 ^ (~b22 & b24); + s[21] = b21 ^ (~b23 & b25); + s[30] = b30 ^ (~b32 & b34); + s[31] = b31 ^ (~b33 & b35); + s[40] = b40 ^ (~b42 & b44); + s[41] = b41 ^ (~b43 & b45); + s[2] = b2 ^ (~b4 & b6); + s[3] = b3 ^ (~b5 & b7); + s[12] = b12 ^ (~b14 & b16); + s[13] = b13 ^ (~b15 & b17); + s[22] = b22 ^ (~b24 & b26); + s[23] = b23 ^ (~b25 & b27); + s[32] = b32 ^ (~b34 & b36); + s[33] = b33 ^ (~b35 & b37); + s[42] = b42 ^ (~b44 & b46); + s[43] = b43 ^ (~b45 & b47); + s[4] = b4 ^ (~b6 & b8); + s[5] = b5 ^ (~b7 & b9); + s[14] = b14 ^ (~b16 & b18); + s[15] = b15 ^ (~b17 & b19); + s[24] = b24 ^ (~b26 & b28); + s[25] = b25 ^ (~b27 & b29); + s[34] = b34 ^ (~b36 & b38); + s[35] = b35 ^ (~b37 & b39); + s[44] = b44 ^ (~b46 & b48); + s[45] = b45 ^ (~b47 & b49); + s[6] = b6 ^ (~b8 & b0); + s[7] = b7 ^ (~b9 & b1); + s[16] = b16 ^ (~b18 & b10); + s[17] = b17 ^ (~b19 & b11); + s[26] = b26 ^ (~b28 & b20); + s[27] = b27 ^ (~b29 & b21); + s[36] = b36 ^ (~b38 & b30); + s[37] = b37 ^ (~b39 & b31); + s[46] = b46 ^ (~b48 & b40); + s[47] = b47 ^ (~b49 & b41); + s[8] = b8 ^ (~b0 & b2); + s[9] = b9 ^ (~b1 & b3); + s[18] = b18 ^ (~b10 & b12); + s[19] = b19 ^ (~b11 & b13); + s[28] = b28 ^ (~b20 & b22); + s[29] = b29 ^ (~b21 & b23); + s[38] = b38 ^ (~b30 & b32); + s[39] = b39 ^ (~b31 & b33); + s[48] = b48 ^ (~b40 & b42); + s[49] = b49 ^ (~b41 & b43); + + s[0] ^= RC[n]; + s[1] ^= RC[n + 1]; + } + }; + + if (COMMON_JS) { + module.exports = methods; + } else { + for (i = 0; i < methodNames.length; ++i) { + root[methodNames[i]] = methods[methodNames[i]]; + } + if (AMD) { + define(function () { + return methods; + }); + } + } +})(); From 7f968f516ab5dfc85cc546b4f44ae9848e4cf76b Mon Sep 17 00:00:00 2001 From: Peter Foldiak Date: Sun, 8 Nov 2026 01:35:20 +0530 Subject: [PATCH 2/5] CI: the JavaScript tests on Node 22 and 24, and stale fixtures fail js runs npm test (no install: no dependencies). js-fixtures reruns js/tools/make_fixtures.py --check against the Python code in src/, so a change to the wire format that leaves the JS fixtures stale turns the build red until they are regenerated. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/tests.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 58b9583..aa25f64 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,38 @@ jobs: # skip themselves — this runs the offline suite only. - run: pytest + # The JavaScript port (js/): its tests replay fixtures that the Python + # implementation generated and must reach the same bytes and roots. No + # npm install: the package has no dependencies. Node 22 is the floor + # (exact large integers need JSON.parse's source-text access). + js: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: ["22", "24"] + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + - run: npm test + working-directory: js + + # The fixtures above must be what the Python code in src/ produces today: + # a change to the wire format that leaves them stale fails here, until + # `python3 js/tools/make_fixtures.py` is rerun. The bee extra brings + # swarmfs, whose keccak the Swarm-addressed fixtures need. + js-fixtures: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - run: pip install -e ".[bee]" + - run: python3 js/tools/make_fixtures.py --check + # Unused and shadowed names (review 2026-10, question 9): in loopmarket a # second definition of a function silently replaced the first and changed # what users approve; pyflakes names that line. Pinned, so a new pyflakes From b091c3a3359ff0ededd53401f5152fa7ecc6efbf Mon Sep 17 00:00:00 2001 From: Peter Foldiak Date: Sun, 8 Nov 2026 21:23:32 +0400 Subject: [PATCH 3/5] JS: writing through window.swarm, checked in real Freedom test/freedom/freedom_check.cjs runs test/freedom/provider-check.html in Freedom built from a checkout (throwaway profile, prompts answered). On 8 November, with Swarm Desktop's Bee as Freedom's node, a store written through window.swarm got exactly the root computed offline under Swarm addressing and read back equal (9 chunks, 3.1 s); reads also ran through Freedom's own Ant node. Writing through Ant itself waits for a funded node: unfunded, it refuses with node-not-ready. SwarmProviderBytesStore now sends its first chunk alone. Freedom asks before each publish until "Always allow this site" is ticked, and chunks sent in parallel from the start each waited on a prompt: five for one commit, now one. A refused connection is asked again on the next write. test/provider.test.js covers the store offline with a fake provider. Co-Authored-By: Claude Opus 5.5 --- js/README.md | 28 ++++- js/src/stores.js | 37 +++++-- js/test/freedom/freedom_check.cjs | 155 ++++++++++++++++++++++++++++ js/test/freedom/provider-check.html | 26 +++++ js/test/freedom/provider-check.js | 109 +++++++++++++++++++ js/test/provider.test.js | 105 +++++++++++++++++++ 6 files changed, 446 insertions(+), 14 deletions(-) create mode 100644 js/test/freedom/freedom_check.cjs create mode 100644 js/test/freedom/provider-check.html create mode 100644 js/test/freedom/provider-check.js create mode 100644 js/test/provider.test.js diff --git a/js/README.md b/js/README.md index 4357c23..99a0995 100644 --- a/js/README.md +++ b/js/README.md @@ -73,10 +73,26 @@ const blobs = new SwarmProviderBytesStore(window.swarm); Reads use `swarm_readData` and count against Freedom's per-site read budget. Writes split each blob into chunks as Bee does and publish them with -`swarm_publishChunk`, so the references are Bee's and Python's; the first -write asks for access. **Not yet tried in Freedom.** Expect writes to be slow -there: one call per chunk, several seconds each through Ant in swarmtyp's -measurements. +`swarm_publishChunk`, so the references are Bee's and Python's. The first +write asks for access, and the store's first chunk goes out alone: Freedom +asks before each publish until the user ticks "Always allow this site", so +one approval then covers the rest of a commit (chunks sent in parallel from +the start each waited on a prompt: five for one commit). + +Checked in Freedom built from main (0.8.9-dev) on 8 November 2026 with +`test/freedom/freedom_check.cjs`, which runs `test/freedom/provider-check.html` +in a throwaway profile and answers Freedom's prompts: + +| | Freedom's node | Result | +|---|---|---| +| Read: 2 records and the 11 keys under a prefix of OntoDAG's `space` pack | its own Ant, just started | 65 reads, 13.5 s, one retry while the node found peers | +| The same read | Swarm Desktop's Bee | 0.4 s | +| Write a 3-record store (9 chunks, one over 4 KB), then read it back | Swarm Desktop's Bee | the root computed offline, read back equal; 3.1 s, one connect and one publish prompt | +| The same write | its own Ant, no postage batch | refused with `node-not-ready` (4900); the commit failed and left the root as it was | + +Writing through Freedom's own Ant node is not tested yet: its node needs a +funded wallet and a postage batch. Expect writes there to be slower: one +call per chunk, several seconds each through Ant in swarmtyp's measurements. ## The API @@ -135,6 +151,10 @@ string escapes, Python's float formatting. Where the languages differ: ```sh npm test # Node's own runner, no install RECORDSTORE_BEE_URL=http://localhost:1633 npm test # also read a live store + +# in real Freedom Browser, from a freedom-browser checkout beside this repo +node test/freedom/freedom_check.cjs # read through Freedom's own node +node test/freedom/freedom_check.cjs --bee=http://127.0.0.1:1633 --write # spends stamps ``` `test/fixtures/` is written by `tools/make_fixtures.py`, which runs the Python diff --git a/js/src/stores.js b/js/src/stores.js index ea0df79..019784c 100644 --- a/js/src/stores.js +++ b/js/src/stores.js @@ -187,21 +187,38 @@ export class SwarmProviderBytesStore { async put(data) { if (data.length === 0) throw new RangeError('the provider publishes chunks of 1 to 4096 bytes; an empty blob cannot be written'); - this._access ??= this.provider.request({ method: 'swarm_requestAccess' }); + // A refused or failed request is not remembered: the next write asks again. + this._access ??= this.provider.request({ method: 'swarm_requestAccess' }) + .catch((e) => { this._access = null; throw e; }); await this._access; const { root, chunks } = splitData(data); - await pool(chunks, this.concurrency, async ({ address, payload, span }) => { - const { reference } = await this.provider.request({ - method: 'swarm_publishChunk', - params: span === payload.length ? { data: payload } : { data: payload, span }, - }); - if (reference !== address) { - throw new VerificationError(`the provider published a chunk as ${reference}, expected ${address}`); - } - }); + let rest = chunks; + if (!this._primed) { + // The store's first chunk goes out alone. Freedom asks before each + // publish until the user ticks "Always allow this site"; requests sent + // in parallel would each be waiting on a prompt before that answer + // lands (five prompts for one commit in the first trial). The rest + // wait for this one, then run in parallel. + this._primed = this._publishChunk(chunks[0]).catch((e) => { this._primed = null; throw e; }); + await this._primed; + rest = chunks.slice(1); + } else { + await this._primed; + } + await pool(rest, this.concurrency, (chunk) => this._publishChunk(chunk)); return root; } + async _publishChunk({ address, payload, span }) { + const { reference } = await this.provider.request({ + method: 'swarm_publishChunk', + params: span === payload.length ? { data: payload } : { data: payload, span }, + }); + if (reference !== address) { + throw new VerificationError(`the provider published a chunk as ${reference}, expected ${address}`); + } + } + async getMany(refs) { const datas = await pool(refs, this.concurrency, (ref) => this.get(ref)); return new Map(refs.map((ref, i) => [ref, datas[i]])); diff --git a/js/test/freedom/freedom_check.cjs b/js/test/freedom/freedom_check.cjs new file mode 100644 index 0000000..644396a --- /dev/null +++ b/js/test/freedom/freedom_check.cjs @@ -0,0 +1,155 @@ +// Runs test/freedom/provider-check.html in real Freedom Browser, from a +// freedom-browser checkout, and prints what it found as JSON. +// +// node test/freedom/freedom_check.cjs [FREEDOM_DIR] [--bee=URL] [--write] +// +// FREEDOM_DIR: a freedom-browser checkout with its node_modules (default: +// beside this repository). Freedom is launched the way its live tests and +// ontodag's check launch it: a throwaway profile, and either its own Ant +// node (default) or, with --bee=URL, that Bee node as its external Swarm +// node. The page is served from this package's directory on 127.0.0.1 and +// opened through the address bar, so it reads and writes through Freedom's +// real window.swarm provider. +// +// Without --write only the read check runs. With --write the page also +// writes a small store, which spends stamps: the check approves Freedom's +// connect prompt and its publish prompt (ticking "Always allow this site", +// so the commit's chunks need one approval). Freedom's node needs a usable +// postage batch for that; without one the write fails with no-usable-stamps. +// +// Report and screenshots go to a temporary directory, named in the output. + +const fs = require('fs'); +const http = require('http'); +const os = require('os'); +const path = require('path'); +const { createRequire } = require('module'); + +const args = process.argv.slice(2); +const positional = args.filter((a) => !a.startsWith('--')); +const flag = (name) => args.find((a) => a === `--${name}` || a.startsWith(`--${name}=`)); +const bee = (flag('bee') || '').split('=')[1] || null; +const doWrite = Boolean(flag('write')); +const PACKAGE = path.resolve(__dirname, '..', '..'); +const freedomDir = path.resolve(positional[0] || path.join(PACKAGE, '..', '..', 'freedom-browser')); +const { _electron: electron } = createRequire(path.join(freedomDir, 'package.json'))('@playwright/test'); + +const TYPES = { '.html': 'text/html', '.js': 'text/javascript', '.json': 'application/json', '.css': 'text/css' }; + +/** Serve this package's directory (read-only, no listing) on 127.0.0.1. */ +function serve() { + const server = http.createServer((req, res) => { + const rel = decodeURIComponent(new URL(req.url, 'http://x').pathname).replace(/^\/+/, ''); + const file = path.resolve(PACKAGE, rel); + if (!file.startsWith(PACKAGE + path.sep) || !fs.existsSync(file) || fs.statSync(file).isDirectory()) { + res.writeHead(404).end(); + return; + } + res.writeHead(200, { 'content-type': TYPES[path.extname(file)] || 'application/octet-stream', 'cache-control': 'no-store' }); + fs.createReadStream(file).pipe(res); + }); + return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(server))); +} + +function inPage(window, code) { + return window.evaluate(async (snippet) => { + const wv = document.querySelector('webview:not(.hidden)'); + if (!wv || typeof wv.executeJavaScript !== 'function') return { error: 'no webview' }; + try { return await wv.executeJavaScript(snippet); } catch (e) { return { error: String(e) }; } + }, code); +} + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function visible(window, selector) { + return window.locator(selector).isVisible().catch(() => false); +} + +async function main() { + const server = await serve(); + const url = `http://127.0.0.1:${server.address().port}/test/freedom/provider-check.html`; + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'recordstore-freedom-')); + const dirs = Object.fromEntries(['userData', 'ant-data', 'ipfs-data', 'identity'].map((d) => { + const p = path.join(tmp, d); + fs.mkdirSync(p, { recursive: true }); + return [d, p]; + })); + fs.writeFileSync(path.join(dirs.userData, 'settings.json'), JSON.stringify({ + startIpfsAtLaunch: false, startRadicleAtLaunch: false, startTorAtLaunch: false, + })); + fs.writeFileSync(path.join(dirs.userData, 'external-node-decisions.json'), JSON.stringify({ + bee: bee ? { choice: 'external', endpoints: [bee] } : { choice: 'managed', endpoints: ['http://127.0.0.1:1633'] }, + })); + const app = await electron.launch({ + args: ['.'], cwd: freedomDir, timeout: 90_000, + env: { + ...process.env, + FREEDOM_TEST_USER_DATA: dirs.userData, FREEDOM_ANT_DATA: dirs['ant-data'], + FREEDOM_IPFS_DATA: dirs['ipfs-data'], FREEDOM_IDENTITY_DATA: dirs.identity, + ELECTRON_DISABLE_SECURITY_WARNINGS: 'true', LANG: 'en_US.UTF-8', + }, + }); + const out = { url, node: bee ? `external ${bee}` : "Freedom's own Ant", profile: tmp }; + try { + const window = await app.firstWindow(); + await window.waitForLoadState('domcontentloaded'); + const input = window.locator('[data-test="address-input"]'); + await input.waitFor({ state: 'visible', timeout: 60_000 }); + await input.click(); + await input.fill(url); + await input.press('Enter'); + let started = Date.now(); + while (Date.now() - started < 60_000) { + if ((await inPage(window, 'Boolean(window.rsCheck && window.rsCheck.ready)')) === true) break; + await sleep(1000); + } + out.provider = await inPage(window, 'window.rsCheck ? window.rsCheck.provider : null'); + + // Reads may wait for the node to find peers: retry for a few minutes. + started = Date.now(); + for (;;) { + out.read = await inPage(window, 'window.rsCheck.read()'); + if (!out.read || !out.read.error || Date.now() - started > 300_000) break; + out.readRetries = (out.readRetries || 0) + 1; + await sleep(15_000); + } + await window.screenshot({ path: path.join(tmp, 'read.png') }); + + if (doWrite) { + await inPage(window, 'window.rsCheck.pendingWrite = window.rsCheck.write(), true'); + const prompts = []; + started = Date.now(); + while (Date.now() - started < 600_000) { + if (await visible(window, '#swarm-connect-approve')) { + await window.waitForFunction(() => !document.getElementById('swarm-connect-approve').disabled, null, { timeout: 30_000 }); + await window.locator('#swarm-connect-approve').click(); + prompts.push('connect approved'); + } else if (await visible(window, '#swarm-publish-confirm')) { + const always = window.locator('#swarm-publish-auto-approve'); + if (!(await always.isChecked().catch(() => false))) await always.check({ force: true }).catch(() => {}); + const ticked = await always.isChecked().catch(() => false); + await window.locator('#swarm-publish-confirm').click(); + prompts.push(`publish approved (always allow ${ticked ? 'ticked' : 'NOT ticked'})`); + } + const result = await inPage(window, 'window.rsCheck.results.write || null'); + if (result) { + out.write = result; + break; + } + await sleep(1000); + } + out.prompts = prompts; + await window.screenshot({ path: path.join(tmp, 'write.png') }); + } + } finally { + await app.close().catch(() => {}); + server.close(); + } + fs.writeFileSync(path.join(tmp, 'report.json'), JSON.stringify(out, null, 1)); + console.log(JSON.stringify(out, null, 1)); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/js/test/freedom/provider-check.html b/js/test/freedom/provider-check.html new file mode 100644 index 0000000..b17a057 --- /dev/null +++ b/js/test/freedom/provider-check.html @@ -0,0 +1,26 @@ + + + + + +recordstore: window.swarm check + + + +

recordstore through window.swarm

+

Reads a store Python published, then writes a small store through Freedom's +provider and checks its root against the one computed offline. +Run it in Freedom Browser; test/freedom/freedom_check.cjs drives it.

+

+ + +

+

loading…

+

+
+
+
diff --git a/js/test/freedom/provider-check.js b/js/test/freedom/provider-check.js
new file mode 100644
index 0000000..6a01a2c
--- /dev/null
+++ b/js/test/freedom/provider-check.js
@@ -0,0 +1,109 @@
+// Checks recordstore's SwarmProviderBytesStore inside Freedom Browser.
+//
+// window.rsCheck.read()  reads OntoDAG's `space` pack (written by Python) through
+//                        window.swarm: a few records and the keys under a prefix.
+// window.rsCheck.write() writes a small store through window.swarm (one record
+//                        over 4096 bytes, so a chunk tree with an intermediate),
+//                        checks that its root equals the root computed offline
+//                        under Swarm addressing, and reads it back.
+// Both resolve to a plain object (also kept in window.rsCheck.results) and
+// never throw: an error comes back as {error, reason}.
+
+import { MemoryBytesStore, RecordStore, SwarmProviderBytesStore, canonicalJson } from '../../src/index.js';
+
+const SPACE_PACK = '67f6be2688e5fb7ca85a463c3d2929dad2b177bb3edb6bb910e8fa110598cff3';
+const $ = (id) => document.getElementById(id);
+
+function show(name, result) {
+  window.rsCheck.results[name] = result;
+  $('out').textContent = JSON.stringify(window.rsCheck.results, null, 2);
+  $('status').textContent = `${name}: ${result.error ? `failed (${result.reason || result.error})` : 'done'}`;
+  return result;
+}
+
+function failure(e) {
+  return { error: String(e && e.message || e), reason: e && e.data && e.data.reason, code: e && e.code };
+}
+
+async function collect(iterable) {
+  const out = [];
+  for await (const x of iterable) out.push(x);
+  return out;
+}
+
+/** A provider wrapper that counts calls per method. */
+function counting(provider) {
+  const calls = {};
+  return {
+    calls,
+    request(args) {
+      calls[args.method] = (calls[args.method] || 0) + 1;
+      return provider.request(args);
+    },
+  };
+}
+
+async function read(root = SPACE_PACK, prefix = 'ab', keys = ['abbey', 'abalone', 'not-a-key']) {
+  $('status').textContent = 'reading…';
+  const provider = counting(window.swarm);
+  const started = performance.now();
+  try {
+    const store = RecordStore.at(root, new SwarmProviderBytesStore(provider));
+    const values = {};
+    for (const k of keys) values[k] = await store.get(k);
+    const under = await collect(store.keys(prefix));
+    return show('read', {
+      root, values, prefix, keysUnderPrefix: under.length, firstKeys: under.slice(0, 5),
+      ms: Math.round(performance.now() - started), calls: provider.calls,
+    });
+  } catch (e) {
+    return show('read', { ...failure(e), ms: Math.round(performance.now() - started), calls: provider.calls });
+  }
+}
+
+const RECORDS = () => ({
+  'check/small': { hello: 'Freedom', at: new Date().toISOString().slice(0, 10) },
+  'check/unicode/é😀': ['é', '中文', '😀'],
+  'check/large': 'x'.repeat(5000), // over one chunk: a leaf pair and an intermediate
+});
+
+async function write() {
+  $('status').textContent = 'writing… (approve Freedom’s prompts)';
+  const provider = counting(window.swarm);
+  const started = performance.now();
+  const records = RECORDS();
+  let expected;
+  try {
+    const offline = new RecordStore(new MemoryBytesStore({ addressing: 'swarm' }));
+    for (const [k, v] of Object.entries(records)) offline.put(k, v);
+    expected = await offline.commit();
+
+    const store = new RecordStore(new SwarmProviderBytesStore(provider));
+    for (const [k, v] of Object.entries(records)) store.put(k, v);
+    const root = await store.commit();
+    const wroteMs = Math.round(performance.now() - started);
+
+    // Read back through a fresh store: nothing cached from the write.
+    const back = RecordStore.at(root, new SwarmProviderBytesStore(window.swarm));
+    const readBack = await collect(back.items());
+    // Compare canonically: stored objects come back with their keys sorted.
+    const same = canonicalJson(readBack) === canonicalJson(Object.entries(records).sort((a, b) => (a[0] < b[0] ? -1 : 1)));
+    return show('write', {
+      root, expected, rootMatches: root === expected, readBackMatches: same,
+      wroteMs, totalMs: Math.round(performance.now() - started), calls: provider.calls,
+    });
+  } catch (e) {
+    return show('write', { ...failure(e), expected, ms: Math.round(performance.now() - started), calls: provider.calls });
+  }
+}
+
+window.rsCheck = {
+  results: {},
+  provider: Boolean(window.swarm && window.swarm.isFreedomBrowser),
+  read,
+  write,
+  ready: true,
+};
+$('read').onclick = () => read();
+$('write').onclick = () => write();
+$('status').textContent = window.rsCheck.provider ? 'ready (Freedom provider found)' : 'no window.swarm: open this page in Freedom Browser';
diff --git a/js/test/provider.test.js b/js/test/provider.test.js
new file mode 100644
index 0000000..824c9c2
--- /dev/null
+++ b/js/test/provider.test.js
@@ -0,0 +1,105 @@
+// SwarmProviderBytesStore against a fake window.swarm provider, offline.
+// test/freedom/freedom_check.cjs runs the same store in real Freedom.
+
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { MemoryBytesStore, RecordStore, SwarmProviderBytesStore, VerificationError } from '../src/index.js';
+import { chunkAddress, encodeSpan } from '../src/address.js';
+import { concatBytes, toHex } from '../src/bytes.js';
+import { collect } from './helpers.js';
+
+/**
+ * A provider that publishes chunks under their BMT address and serves
+ * whole blobs by joining them, as Freedom's node does. It records the order
+ * of requests and how many publishes are in flight at once.
+ */
+function fakeProvider({ corrupt = false } = {}) {
+  const chunks = new Map(); // address -> {span, payload}
+  const log = [];
+  let inFlight = 0;
+  let maxInFlightBeforeFirstDone = 0;
+  let firstDone = false;
+  const join = (address) => {
+    const c = chunks.get(address);
+    if (!c) throw Object.assign(new Error('not found'), { data: { reason: 'chunk_not_found' } });
+    if (c.span <= 4096) return c.payload;
+    const parts = [];
+    for (let i = 0; i < c.payload.length; i += 32) parts.push(join(toHex(c.payload.subarray(i, i + 32))));
+    return concatBytes(...parts);
+  };
+  return {
+    log,
+    get maxInFlightBeforeFirstDone() { return maxInFlightBeforeFirstDone; },
+    async request({ method, params }) {
+      log.push(method);
+      if (method === 'swarm_requestAccess') return { connected: true };
+      if (method === 'swarm_publishChunk') {
+        inFlight++;
+        if (!firstDone) maxInFlightBeforeFirstDone = Math.max(maxInFlightBeforeFirstDone, inFlight);
+        await new Promise((r) => setTimeout(r, 1));
+        const payload = Uint8Array.from(params.data);
+        const span = params.span ?? payload.length;
+        const address = toHex(chunkAddress(concatBytes(encodeSpan(span), payload)));
+        chunks.set(address, { span, payload });
+        inFlight--;
+        firstDone = true;
+        return { reference: corrupt ? '00'.repeat(32) : address };
+      }
+      if (method === 'swarm_readData') {
+        const data = join(params.reference);
+        return { data: btoa(String.fromCharCode(...data)), encoding: 'base64' };
+      }
+      throw new Error(`unexpected ${method}`);
+    },
+  };
+}
+
+test('a store written through the provider has the offline Swarm root, and reads back', async () => {
+  const provider = fakeProvider();
+  const records = { small: { a: 1 }, large: 'x'.repeat(9000), 'é😀': [1, 2] };
+  const offline = new RecordStore(new MemoryBytesStore({ addressing: 'swarm' }));
+  const store = new RecordStore(new SwarmProviderBytesStore(provider));
+  for (const [k, v] of Object.entries(records)) {
+    offline.put(k, v);
+    store.put(k, v);
+  }
+  const root = await store.commit();
+  assert.equal(root, await offline.commit());
+  const back = RecordStore.at(root, new SwarmProviderBytesStore(provider));
+  assert.deepEqual(await collect(back.items()), [['large', records.large], ['small', { a: 1 }], ['é😀', [1, 2]]]);
+  assert.equal(provider.log[0], 'swarm_requestAccess');
+});
+
+test('the first chunk is published alone, so one "Always allow" covers the rest', async () => {
+  const provider = fakeProvider();
+  const store = new RecordStore(new SwarmProviderBytesStore(provider, { concurrency: 4 }));
+  for (let i = 0; i < 20; i++) store.put(`k${i}`, 'v'.repeat(5000)); // many chunks, published in parallel
+  await store.commit();
+  assert.equal(provider.maxInFlightBeforeFirstDone, 1);
+  assert.equal(provider.log.filter((m) => m === 'swarm_requestAccess').length, 1);
+});
+
+test('a reference the provider got wrong is caught', async () => {
+  const blobs = new SwarmProviderBytesStore(fakeProvider({ corrupt: true }));
+  await assert.rejects(blobs.put(new Uint8Array([1, 2, 3])), VerificationError);
+});
+
+test('a refused connection is asked again on the next write', async () => {
+  let asked = 0;
+  const provider = {
+    async request({ method }) {
+      if (method === 'swarm_requestAccess') {
+        asked++;
+        if (asked === 1) throw Object.assign(new Error('User rejected'), { code: 4001 });
+        return { connected: true };
+      }
+      return fakeProviderPublish(method);
+    },
+  };
+  const fallback = fakeProvider();
+  const fakeProviderPublish = (method) => fallback.request({ method, params: { data: new Uint8Array([7]) } });
+  const blobs = new SwarmProviderBytesStore(provider);
+  await assert.rejects(blobs.put(new Uint8Array([7])), /rejected/);
+  assert.match(await blobs.put(new Uint8Array([7])), /^[0-9a-f]{64}$/);
+  assert.equal(asked, 2);
+});

From 8c3fddee64fa8ede37583707e27ec40bfd5d5a03 Mon Sep 17 00:00:00 2001
From: Peter Foldiak 
Date: Sun, 8 Nov 2026 22:12:11 +0400
Subject: [PATCH 4/5] JS: SwarmFeedPointer, a store's latest root in a Swarm
 feed

Ported from the Python pointer and swarmfs's feeds: bee-js's update
format, signed with noble-secp256k1 2.3.0 (vendored from swarmlite, as
js-sha3 was). Signing is RFC 6979 with low S, as libsecp256k1, so the
fixtures pin whole signed updates: swarmfs's own FeedOps.update, run with
the clock fixed, and the JS must write the same bytes. The same defences
against light-node lookups: read-your-writes cache, monotonic index floor,
the tip probed over the update chunks, backoff that never goes back.
verifyFeedUpdate and verifyEquivocation check envelopes offline;
swarmStore(topic, {signer | owner, batch}) is Python's swarm_store.
Transports: Bee's API, and Freedom's window.swarm for reading
(swarm_readSingleOwnerChunk, re-checked here; writing waits for #647).

Live on 8 November: loopmarket's test maker book followed through its
feed (Bee, and Freedom's own Ant through window.swarm: the same root);
the OntoDAG page feed's 11 updates; a store published behind a new feed
from JS and read back from JS and from Python, both updates verified.

js-fixtures now installs the feeds extra for signing.

Co-Authored-By: Claude Opus 5.5 
---
 .github/workflows/tests.yml             |   7 +-
 js/README.md                            |  86 ++-
 js/src/feeds.js                         | 510 +++++++++++++++
 js/src/index.js                         |   5 +
 js/src/swarm.js                         |  26 +
 js/test/feeds.test.js                   | 235 +++++++
 js/test/fixtures/feeds.json             | 234 +++++++
 js/test/freedom/freedom_check.cjs       |   1 +
 js/test/freedom/provider-check.js       |  23 +-
 js/test/live.test.js                    |  66 +-
 js/tools/make_fixtures.py               |  99 +++
 js/vendor/noble-secp256k1/LICENSE       |  21 +
 js/vendor/noble-secp256k1/PROVENANCE.md |  24 +
 js/vendor/noble-secp256k1/index.js      | 790 ++++++++++++++++++++++++
 14 files changed, 2101 insertions(+), 26 deletions(-)
 create mode 100644 js/src/feeds.js
 create mode 100644 js/src/swarm.js
 create mode 100644 js/test/feeds.test.js
 create mode 100644 js/test/fixtures/feeds.json
 create mode 100644 js/vendor/noble-secp256k1/LICENSE
 create mode 100644 js/vendor/noble-secp256k1/PROVENANCE.md
 create mode 100644 js/vendor/noble-secp256k1/index.js

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index aa25f64..e6f7fdd 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -42,8 +42,9 @@ jobs:
 
   # The fixtures above must be what the Python code in src/ produces today:
   # a change to the wire format that leaves them stale fails here, until
-  # `python3 js/tools/make_fixtures.py` is rerun. The bee extra brings
-  # swarmfs, whose keccak the Swarm-addressed fixtures need.
+  # `python3 js/tools/make_fixtures.py` is rerun. The bee and feeds extras
+  # bring swarmfs with keccak and signing (coincurve): the Swarm-addressed
+  # fixtures need the one, the signed feed updates the other.
   js-fixtures:
     runs-on: ubuntu-latest
     steps:
@@ -51,7 +52,7 @@ jobs:
       - uses: actions/setup-python@v6
         with:
           python-version: "3.12"
-      - run: pip install -e ".[bee]"
+      - run: pip install -e ".[bee,feeds]"
       - run: python3 js/tools/make_fixtures.py --check
 
   # Unused and shadowed names (review 2026-10, question 9): in loopmarket a
diff --git a/js/README.md b/js/README.md
index 99a0995..a277b24 100644
--- a/js/README.md
+++ b/js/README.md
@@ -9,13 +9,16 @@ in both: byte for byte, under SHA-256 or Swarm addressing.
 **Status: early (0.1.0, not on npm yet).** The core is complete and tested
 against the Python implementation: records, staging, commits, sorted
 iteration, diff, three-way merge, and reconciling a moved pointer. Stores for
-memory, a Bee node, and Freedom Browser's `window.swarm`. Not yet ported:
-feed pointers (signing), proofs, history and undo, local-first storage.
+memory, a Bee node, and Freedom Browser's `window.swarm`; the store's latest
+root in a Swarm feed (`SwarmFeedPointer`), with feed updates verifiable
+offline. Not yet ported: the other proofs, history and undo, local-first
+storage.
 
 Plain JavaScript modules with JSDoc types and no npm dependencies, so a page
 can import it with relative URLs only, including a page published on Swarm.
-The one vendored file is js-sha3 0.9.3 for keccak, the same copy swarmlite
-uses (see [vendor/js-sha3/PROVENANCE.md](vendor/js-sha3/PROVENANCE.md)).
+Two files are vendored, the same copies swarmlite uses: js-sha3 0.9.3 for
+keccak and @noble/secp256k1 2.3.0 for signing feed updates (see their
+`PROVENANCE.md` under [vendor/](vendor/)).
 Node 22 or later, or any current browser (SHA-256 addressing uses Web Crypto,
 which browsers offer only in secure contexts).
 
@@ -62,6 +65,46 @@ Browser, cold, in 391 s, every blob checked (`test/live.test.js`). Listing
 everything walks every trie node; a single `get` touches only the nodes on
 its key's path.
 
+## A store behind a feed
+
+A root names one version; a feed gives a store a stable address that moves
+with every commit, as Python's `swarm_store` and `SwarmFeedPointer` do. The
+two implementations read each other's feeds.
+
+```js
+import { swarmStore } from './recordstore/js/src/index.js';
+
+// Publish your own: the feed is owned by your key; writes need a batch.
+const mine = await swarmStore('my-notes', { url: 'http://localhost:1633', signer: '', batch: '' });
+mine.put('note/1', 'hello');
+await mine.commit();                     // uploads the blobs, then the signed feed update
+
+// Follow someone else's: read-only, by their address.
+const theirs = await swarmStore('loop-twonode-1791730596', { owner: '300f4daa2653102abde4c66f75ea512c0bb81571' });
+for await (const [key, record] of theirs.items()) console.log(key);
+```
+
+`SwarmFeedPointer(urlOrTransport, topicName, {signer | owner, batch})` is
+the pointer by itself; `RecordStore.open(blobs, {pointer})` puts any store
+behind it. Light-node feed lookups are unreliable, so, as in Python, it
+never trusts one: a read-your-writes cache, a monotonic index floor for
+writes, the tip found by probing the update chunks themselves, retries
+with backoff that never go back to an older index, and every update
+checked against the owner's signature. `compareAndSet` (used by
+`commit({reconcile: true})`) is best effort: a feed cannot claim an index
+atomically. `pointer.update(i)` and `pointer.updates()` return a feed's
+signed updates as envelopes that `verifyFeedUpdate(envelope, owner, topic)`
+checks with no node; `verifyEquivocation` proves an owner signed two roots
+at one index. Signatures are deterministic (RFC 6979, low S), so the same
+key, update and timestamp give the same bytes as Python.
+
+Live on 8 November 2026, through Swarm Desktop's Bee (`test/live.test.js`):
+loopmarket's test maker book, a store Python keeps behind a feed, followed
+to its root with its 8 records (12 s); the OntoDAG page's feed read with its
+11 updates, the latest verified offline; and a store published behind a new
+feed from JavaScript, then followed from JavaScript and from Python, which
+read the same root and records and verified both updates.
+
 ### In Freedom Browser
 
 A page in Freedom cannot reach a node's HTTP API; it has `window.swarm`.
@@ -72,6 +115,16 @@ const blobs = new SwarmProviderBytesStore(window.swarm);
 ```
 
 Reads use `swarm_readData` and count against Freedom's per-site read budget.
+Any feed reads through `ProviderFeedTransport`, which fetches each update
+with `swarm_readSingleOwnerChunk` and checks it again here:
+
+```js
+const pointer = new SwarmFeedPointer(new ProviderFeedTransport(window.swarm), topic, { owner });
+const store = await RecordStore.open(blobs, { pointer });
+```
+
+Writing a feed with your own key from a page waits for Freedom to accept
+chunks a page signed (freedom-browser#647); until then `set` refuses.
 Writes split each blob into chunks as Bee does and publish them with
 `swarm_publishChunk`, so the references are Bee's and Python's. The first
 write asks for access, and the store's first chunk goes out alone: Freedom
@@ -89,6 +142,7 @@ in a throwaway profile and answers Freedom's prompts:
 | The same read | Swarm Desktop's Bee | 0.4 s |
 | Write a 3-record store (9 chunks, one over 4 KB), then read it back | Swarm Desktop's Bee | the root computed offline, read back equal; 3.1 s, one connect and one publish prompt |
 | The same write | its own Ant, no postage batch | refused with `node-not-ready` (4900); the commit failed and left the root as it was |
+| Follow loopmarket's test maker book through its feed | its own Ant | the same root as through Bee, 8 records; 8 update and 12 blob reads, 8.0 s |
 
 Writing through Freedom's own Ant node is not tested yet: its node needs a
 funded wallet and a postage batch. Expect writes there to be slower: one
@@ -114,6 +168,9 @@ Shaped like the Python one, but asynchronous and JavaScript-idiomatic:
 | `await store.commit({reconcile, resolver, retries})` | write, return the new root, move the pointer |
 | `await RecordStore.merge(blobs, base, ours, theirs, resolver)` | three-way merge; `MergeConflict` without a resolver |
 | `store.refsUnder(prefix)` | every blob the keys under `prefix` need (for keeping them alive) |
+| `await swarmStore(topic, {url, signer \| owner, batch})` | a store on Swarm behind its feed |
+| `new SwarmFeedPointer(urlOrTransport, topic, {signer \| owner, batch})` | the feed pointer: `get`, `set`, `compareAndSet`, `update(i)`, `updates()` |
+| `verifyFeedUpdate(envelope, owner, topic)` | `{index, root, timestamp}` of a signed update, checked offline; `ProofError` otherwise |
 
 Errors: `RecordUnavailable` when a record exists but a blob on its path cannot
 be read right now (never reported as absence), `MergeConflict`,
@@ -124,7 +181,10 @@ Bytes stores: `MemoryBytesStore({addressing: 'sha256' | 'swarm'})`,
 `SwarmProviderBytesStore(provider, {verify, concurrency})`. Anything with
 async `put(bytes) → ref` and `get(ref) → bytes` works, optionally with
 `putMany` and `getMany` (a `Map`). `MemoryPointer` is a pointer with
-compare-and-set for writers in one process.
+compare-and-set for writers in one process. Feed transports:
+`BeeFeedTransport(url, {batch})` and `ProviderFeedTransport(provider)`, or
+anything with `readSoc(address)` and `writeSoc(owner, identifier, signature,
+data, batch)`, optionally `feedHead(owner, topic, after)`.
 
 ## Compatibility with Python
 
@@ -150,7 +210,8 @@ string escapes, Python's float formatting. Where the languages differ:
 
 ```sh
 npm test                                            # Node's own runner, no install
-RECORDSTORE_BEE_URL=http://localhost:1633 npm test  # also read a live store
+RECORDSTORE_BEE_URL=http://localhost:1633 npm test  # also read live stores and feeds
+RECORDSTORE_BEE_URL=... RECORDSTORE_BEE_BATCH= npm test   # and write one (spends stamps)
 
 # in real Freedom Browser, from a freedom-browser checkout beside this repo
 node test/freedom/freedom_check.cjs                 # read through Freedom's own node
@@ -160,19 +221,22 @@ node test/freedom/freedom_check.cjs --bee=http://127.0.0.1:1633 --write   # spen
 `test/fixtures/` is written by `tools/make_fixtures.py`, which runs the Python
 implementation in `../src` (not an installed copy) and records canonical
 encodings, Swarm addresses, the root after every commit of scripted and random
-stores under both addressings, and three-way merges. The JavaScript tests
+stores under both addressings, three-way merges, and feeds: topics, owners,
+update addresses, signed updates written by swarmfs's own code (so JavaScript
+must reproduce the signatures byte for byte), and updates Python refuses. It
+needs swarmfs with signing (`pip install "recordstore[bee,feeds]"`). The JavaScript tests
 replay the same operations and must reach the same bytes and roots. After a
 change to the Python format, `npm run fixtures`; `npm run fixtures:check`
 fails while the fixtures are stale.
 
 ## Not yet ported
 
-- `SwarmFeedPointer`: the store's latest root in a Swarm feed (needs signing;
-  swarmlite vendors noble-secp256k1 for it).
-- Proofs (`verify_proof`, `verify_prefix`, `verify_extension`).
+- Proofs (`verify_proof`, `verify_prefix`, `verify_extension`); feed updates
+  are ported.
 - History, undo and redo (`FilePointer`'s timeline).
 - Local-first storage.
 
 ## License
 
-BSD-3-Clause, as the Python package. The vendored js-sha3 is MIT.
+BSD-3-Clause, as the Python package. The vendored js-sha3 and
+noble-secp256k1 are MIT.
diff --git a/js/src/feeds.js b/js/src/feeds.js
new file mode 100644
index 0000000..c53897b
--- /dev/null
+++ b/js/src/feeds.js
@@ -0,0 +1,510 @@
+// Swarm feeds as the store's pointer: SwarmFeedPointer, ported from the
+// Python package (which builds on swarmfs's feeds), and feed updates
+// verifiable offline.
+//
+// The format is bee-js's:
+// - a topic name is hashed: keccak256 of its UTF-8;
+// - the identifier of update `index` is keccak256(topic ‖ index as 8 bytes
+//   big-endian);
+// - the update is a single-owner chunk at keccak256(identifier ‖ owner):
+//   identifier (32) ‖ signature (65) ‖ span (8) ‖ payload, the payload
+//   being the writer's timestamp (8 bytes big-endian, unix seconds) ‖ root;
+// - the signature is Ethereum's personal-message signature, r ‖ s ‖ v with
+//   v in {27, 28}, over keccak256(identifier ‖ the wrapped chunk's address).
+//
+// Signing is RFC 6979 deterministic with low S (noble-secp256k1, as
+// libsecp256k1 in Python), so the same key, update and timestamp give the
+// same bytes in both languages.
+
+import { getPublicKey, signAsync, Signature } from '../vendor/noble-secp256k1/index.js';
+import { chunkAddress, encodeSpan, keccak256Bytes } from './address.js';
+import { concatBytes, fromBase64, fromHex, toHex, utf8 } from './bytes.js';
+import { VerificationError } from './stores.js';
+
+export const FEED_UPDATE_FORMAT = 'recordstore-feed-update';
+
+const SOC_IDENTIFIER_SIZE = 32;
+const SOC_SIGNATURE_SIZE = 65;
+const SOC_SPAN_OFFSET = SOC_IDENTIFIER_SIZE + SOC_SIGNATURE_SIZE; // 97
+const SOC_PAYLOAD_OFFSET = SOC_SPAN_OFFSET + 8; // 105
+const ETH_PREFIX = utf8('\x19Ethereum Signed Message:\n32');
+
+/** A feed update envelope (or pair of them) that does not prove what it claims. */
+export class ProofError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = 'ProofError';
+  }
+}
+
+/** A transport's answer for a chunk or lookup: `status` 404 means absent, 500 an incomplete retrieval. */
+export class FeedTransportError extends Error {
+  constructor(message, status) {
+    super(message);
+    this.name = 'FeedTransportError';
+    this.status = status;
+  }
+}
+
+// -- addressing -------------------------------------------------------------
+
+/** The 32-byte topic of a topic name: keccak256 of its UTF-8, always (as SwarmFeedPointer). */
+export function topicOf(name) {
+  if (typeof name !== 'string') throw new TypeError('a topic name is a string');
+  return keccak256Bytes(utf8(name));
+}
+
+/** keccak256(topic ‖ index as 8 bytes big-endian). */
+export function feedIdentifier(topic, index) {
+  const idx = new Uint8Array(8);
+  new DataView(idx.buffer).setBigUint64(0, BigInt(index), false);
+  return keccak256Bytes(concatBytes(topic, idx));
+}
+
+/** Where a single-owner chunk lives: keccak256(identifier ‖ owner). */
+export function socAddress(identifier, owner) {
+  return keccak256Bytes(concatBytes(identifier, owner));
+}
+
+function strip0x(hex) {
+  return hex.startsWith('0x') || hex.startsWith('0X') ? hex.slice(2) : hex;
+}
+
+/** 20 owner bytes from 40 hex (optional 0x, either case). */
+export function ownerBytes(owner) {
+  const h = strip0x(String(owner)).toLowerCase();
+  if (!/^[0-9a-f]{40}$/.test(h)) throw new TypeError(`${JSON.stringify(owner)} is not a 20-byte owner address`);
+  return fromHex(h);
+}
+
+function privateKeyBytes(key) {
+  const h = typeof key === 'string' ? strip0x(key).toLowerCase() : toHex(key);
+  if (!/^[0-9a-f]{64}$/.test(h)) throw new TypeError('a private key is 32 bytes (64 hex characters)');
+  return fromHex(h);
+}
+
+/** The address (20 bytes) of a secp256k1 private key: the last 20 bytes of keccak256 of the public key. */
+export function addressOf(privateKey) {
+  const pub = getPublicKey(privateKeyBytes(privateKey), false); // 0x04 ‖ x ‖ y
+  return keccak256Bytes(pub.subarray(1)).subarray(12);
+}
+
+// -- signatures -------------------------------------------------------------
+
+function messageDigest(digest32) {
+  return keccak256Bytes(concatBytes(ETH_PREFIX, digest32));
+}
+
+/** 65-byte r ‖ s ‖ v over the personal-message digest of `digest32`. */
+export async function signDigest(privateKey, digest32) {
+  const sig = await signAsync(messageDigest(digest32), privateKeyBytes(privateKey), { lowS: true });
+  return concatBytes(sig.toCompactRawBytes(), new Uint8Array([27 + sig.recovery]));
+}
+
+/** The 20-byte address whose key made `signature` over the personal-message digest of `digest32`. */
+export function recoverDigest(signature, digest32) {
+  if (signature.length !== 65) throw new VerificationError(`a signature is 65 bytes, got ${signature.length}`);
+  const v = signature[64];
+  const recid = v === 27 || v === 28 ? v - 27 : v;
+  if (recid !== 0 && recid !== 1) throw new VerificationError(`unsupported recovery byte ${v}`);
+  let pub;
+  try {
+    pub = Signature.fromCompact(signature.subarray(0, 64)).addRecoveryBit(recid)
+      .recoverPublicKey(messageDigest(digest32)).toBytes(false);
+  } catch (e) {
+    throw new VerificationError(`signature recovers no key: ${e.message || e}`);
+  }
+  return keccak256Bytes(pub.subarray(1)).subarray(12);
+}
+
+/**
+ * Check a single-owner chunk as a Bee node does on upload: its address
+ * derives from its identifier and `owner`, and its signature over
+ * (identifier, wrapped chunk address) recovers to `owner`.
+ */
+export function verifySoc(soc, owner, address) {
+  if (soc.length < SOC_PAYLOAD_OFFSET) throw new VerificationError(`single-owner chunk too short: ${soc.length} bytes`);
+  const identifier = soc.subarray(0, SOC_IDENTIFIER_SIZE);
+  const signature = soc.subarray(SOC_IDENTIFIER_SIZE, SOC_SPAN_OFFSET);
+  const cac = soc.subarray(SOC_SPAN_OFFSET);
+  if (toHex(socAddress(identifier, owner)) !== toHex(address)) {
+    throw new VerificationError('single-owner chunk does not match its address');
+  }
+  const recovered = recoverDigest(signature, keccak256Bytes(concatBytes(identifier, chunkAddress(cac))));
+  if (toHex(recovered) !== toHex(owner)) throw new VerificationError('feed update signature was not made by the feed owner');
+}
+
+/**
+ * The root and timestamp a feed update chunk publishes. Tolerates the other
+ * payload formats swarmfs reads: a bare reference, or a wrapped root chunk.
+ */
+export function payloadFromSoc(soc) {
+  if (soc.length < SOC_PAYLOAD_OFFSET) throw new VerificationError(`feed update chunk too short: ${soc.length} bytes`);
+  const payload = soc.subarray(SOC_PAYLOAD_OFFSET);
+  if (payload.length === 40 || payload.length === 72) {
+    return { reference: toHex(payload.subarray(8)), timestamp: Number(new DataView(payload.buffer, payload.byteOffset, 8).getBigUint64(0, false)) };
+  }
+  if (payload.length === 32 || payload.length === 64) return { reference: toHex(payload), timestamp: null };
+  return { reference: toHex(chunkAddress(soc.subarray(SOC_SPAN_OFFSET))), timestamp: null };
+}
+
+/**
+ * The feed update publishing `root` as update `index` of `topic` (bytes),
+ * signed with `privateKey`, as swarmfs (and bee-js) write it.
+ * @returns {Promise<{identifier: Uint8Array, signature: Uint8Array, data: Uint8Array, soc: Uint8Array, address: Uint8Array}>}
+ *   `data` (span ‖ payload) is what Bee's POST /soc takes; `soc` is the whole chunk.
+ */
+export async function buildFeedUpdate(privateKey, topic, index, root, timestamp) {
+  const ts = new Uint8Array(8);
+  new DataView(ts.buffer).setBigUint64(0, BigInt(timestamp), false);
+  const payload = concatBytes(ts, fromHex(root));
+  const data = concatBytes(encodeSpan(payload.length), payload);
+  const identifier = feedIdentifier(topic, index);
+  const signature = await signDigest(privateKey, keccak256Bytes(concatBytes(identifier, chunkAddress(data))));
+  return {
+    identifier, signature, data,
+    soc: concatBytes(identifier, signature, data),
+    address: socAddress(identifier, addressOf(privateKey)),
+  };
+}
+
+// -- offline verification of published updates ----------------------------
+
+function hexOwner(owner) {
+  const h = strip0x(String(owner));
+  if (h.length !== 40) throw new ProofError(`${JSON.stringify(owner)} is not a 20-byte owner address`);
+  return h.toLowerCase();
+}
+
+/** A 32-byte topic in hex, or a topic name as SwarmFeedPointer hashes it. */
+function hexTopic(topic) {
+  const h = strip0x(String(topic));
+  if (/^[0-9a-fA-F]{64}$/.test(h)) return h.toLowerCase();
+  return toHex(topicOf(topic));
+}
+
+/**
+ * Check that `update` (an envelope from `SwarmFeedPointer.update`) is
+ * `owner`'s signed update of the feed `topic` at the index it claims, and
+ * return what it publishes. Raises ProofError on any mismatch. Needs no node.
+ * @returns {{index: number, root: string, timestamp: number}}
+ */
+export function verifyFeedUpdate(update, owner, topic) {
+  if (!update || typeof update !== 'object' || update.format !== FEED_UPDATE_FORMAT) throw new ProofError(`not a ${FEED_UPDATE_FORMAT} envelope`);
+  if (update.version !== 1) throw new ProofError(`unsupported feed update version ${JSON.stringify(update.version)}`);
+  const ownerHex = hexOwner(owner);
+  const topicHex = hexTopic(topic);
+  if (update.owner !== ownerHex || update.topic !== topicHex) throw new ProofError("the update is another owner's or another feed's");
+  const { index } = update;
+  if (!Number.isSafeInteger(index) || index < 0) throw new ProofError('the update names no index');
+  const ownerB = fromHex(ownerHex);
+  const address = socAddress(feedIdentifier(fromHex(topicHex), index), ownerB);
+  let wire;
+  try {
+    wire = fromHex(typeof update.soc === 'string' ? update.soc : '');
+    verifySoc(wire, ownerB, address);
+  } catch {
+    throw new ProofError("the chunk is not the owner's signed update at this index");
+  }
+  const payload = wire.subarray(SOC_PAYLOAD_OFFSET);
+  if (payload.length !== 40 && payload.length !== 72) throw new ProofError("the update's payload is not a timestamp and a root");
+  return payloadToUpdate(index, payload);
+}
+
+function payloadToUpdate(index, payload) {
+  const timestamp = Number(new DataView(payload.buffer, payload.byteOffset, 8).getBigUint64(0, false));
+  return { index, root: toHex(payload.subarray(8)), timestamp };
+}
+
+/**
+ * Two updates by `owner` of one feed at one index publishing different
+ * roots: the owner equivocated, and this returns the index. ProofError when
+ * either does not verify, the indices differ, or the roots agree.
+ */
+export function verifyEquivocation(a, b, owner, topic) {
+  const ua = verifyFeedUpdate(a, owner, topic);
+  const ub = verifyFeedUpdate(b, owner, topic);
+  if (ua.index !== ub.index) throw new ProofError('updates at different indices are a sequence, not an equivocation');
+  if (ua.root === ub.root) throw new ProofError('the two updates publish the same root');
+  return ua.index;
+}
+
+// -- transports -------------------------------------------------------------
+
+/**
+ * Feeds over Bee's HTTP API (or Ant's): chunks from GET /chunks, uploads
+ * with POST /soc, and Bee's sequence lookup (GET /feeds) as a shortcut.
+ */
+export class BeeFeedTransport {
+  constructor(url, { batch = null, fetch: fetchFn } = {}) {
+    this.url = url.replace(/\/+$/, '');
+    this.batch = batch;
+    this._fetch = fetchFn || globalThis.fetch.bind(globalThis);
+  }
+
+  async readSoc(address) {
+    const res = await this._fetch(`${this.url}/chunks/${address}`);
+    if (!res.ok) throw new FeedTransportError(`GET /chunks/${address}: HTTP ${res.status}`, res.status);
+    return new Uint8Array(await res.arrayBuffer());
+  }
+
+  async writeSoc(owner, identifier, signature, data, batch = this.batch) {
+    if (!batch) throw new Error('writing a feed update to Bee needs a postage batch (the `batch` option)');
+    const res = await this._fetch(`${this.url}/soc/${owner}/${identifier}?sig=${signature}`, {
+      method: 'POST',
+      headers: { 'swarm-postage-batch-id': batch, 'content-type': 'application/octet-stream' },
+      body: data,
+    });
+    if (!res.ok) throw new FeedTransportError(`POST /soc: HTTP ${res.status} ${await res.text().catch(() => '')}`, res.status);
+  }
+
+  /** The latest index Bee's lookup finds, or null for an empty feed. */
+  async feedHead(owner, topic, after = null) {
+    let url = `${this.url}/feeds/${owner}/${topic}?type=sequence`;
+    if (after !== null) url += `&after=${after}`;
+    const res = await this._fetch(url, { headers: { 'swarm-only-root-chunk': 'true' } });
+    if (res.status === 404) return null;
+    if (!res.ok) throw new FeedTransportError(`GET /feeds: HTTP ${res.status}`, res.status);
+    await res.arrayBuffer().catch(() => {}); // only the headers matter
+    const index = res.headers.get('swarm-feed-index');
+    return index ? Number(BigInt(`0x${index}`)) : null;
+  }
+}
+
+/**
+ * Feeds through Freedom Browser's window.swarm. Reading works for any
+ * owner's feed: `swarm_readSingleOwnerChunk` returns the chunk with its
+ * signature, and the pointer checks it again here. Writing a feed with your
+ * own key waits for Freedom to accept chunks a page signed
+ * (freedom-browser#647).
+ */
+export class ProviderFeedTransport {
+  constructor(provider = globalThis.swarm) {
+    if (!provider || typeof provider.request !== 'function') throw new TypeError('no window.swarm provider (open the page in Freedom Browser)');
+    this.provider = provider;
+  }
+
+  async readSoc(address) {
+    let r;
+    try {
+      r = await this.provider.request({ method: 'swarm_readSingleOwnerChunk', params: { address } });
+    } catch (e) {
+      const reason = e && e.data && e.data.reason;
+      if (reason === 'chunk_not_found') throw new FeedTransportError(`no chunk at ${address}`, 404);
+      if (reason === 'rate_limited') throw new FeedTransportError('read budget exhausted', 429);
+      throw e;
+    }
+    const payload = fromBase64(r.data);
+    return concatBytes(fromHex(strip0x(r.identifier)), fromHex(strip0x(r.signature)), encodeSpan(r.span ?? payload.length), payload);
+  }
+
+  async writeSoc() {
+    throw new Error('writing a feed with your own key from a page waits for Freedom to accept chunks the page signed (freedom-browser#647)');
+  }
+}
+
+// -- the pointer ------------------------------------------------------------
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+const TRANSIENT = new Set([404, 429, 500]);
+
+/**
+ * The store's mutable "latest root" in a Swarm feed, as Python's
+ * SwarmFeedPointer: `set(root)` publishes a signed update, `get()` resolves
+ * the latest root. Light-node feed lookups are unreliable (404s, stale-early
+ * indexes), so it never trusts one: a read-your-writes cache, a monotonic
+ * index floor for writes, the tip found by probing the update chunks
+ * themselves, and retries with backoff that never go back to an older index.
+ * Every chunk read is verified against the owner.
+ *
+ * `transport` is a Bee API URL (or a BeeFeedTransport, ProviderFeedTransport,
+ * or anything with readSoc/writeSoc and optionally feedHead). Pass `signer`
+ * (private key hex) to read and write, or `owner` (address hex) to read.
+ * Writing to Bee also needs `batch`. `topic` is a name, hashed to the topic.
+ */
+export class SwarmFeedPointer {
+  constructor(transport, topic, {
+    signer = null, owner = null, batch = null, feedTtl = 15, maxLookupRetries = 15,
+    retryBackoff = 0.5, retryBackoffCap = 5, sleep: sleepFn = sleep, now = () => Date.now(),
+  } = {}) {
+    this._transport = typeof transport === 'string' ? new BeeFeedTransport(transport, { batch }) : transport;
+    this._topic = topicOf(topic);
+    if (signer) {
+      this._key = privateKeyBytes(signer);
+      this._owner = addressOf(this._key);
+    } else if (owner) {
+      this._key = null;
+      this._owner = ownerBytes(owner);
+    } else {
+      throw new TypeError('SwarmFeedPointer needs a signer (to read and write) or an owner address (read-only)');
+    }
+    if (batch !== null) {
+      const b = strip0x(String(batch)).toLowerCase();
+      if (!/^[0-9a-f]{64}$/.test(b)) throw new TypeError(`${JSON.stringify(batch)} is not a postage batch id`);
+      batch = b;
+    }
+    this._batch = batch;
+    this._ttl = feedTtl * 1000;
+    this._maxRetries = Math.max(1, maxLookupRetries);
+    this._backoff = retryBackoff * 1000;
+    this._backoffCap = retryBackoffCap * 1000;
+    this._sleep = sleepFn;
+    this._now = now;
+    // read-your-writes cache + monotonic index floor
+    this._cachedRef = null;
+    this._nextIndex = 0;
+    this._cacheExpiry = 0;
+  }
+
+  /** The feed owner's address, hex (no 0x). */
+  get owner() {
+    return toHex(this._owner);
+  }
+
+  /** The 32-byte topic, hex. */
+  get topic() {
+    return toHex(this._topic);
+  }
+
+  async set(root) {
+    if (!this._key) throw new Error('SwarmFeedPointer.set needs a signer');
+    let index;
+    if (this._nextIndex > 0) {
+      index = this._nextIndex; // a persistent writer's floor is authoritative
+    } else {
+      const probed = await this._probeLatestIndex();
+      index = probed === null ? 0 : probed + 1;
+    }
+    const update = await buildFeedUpdate(this._key, this._topic, index, root, Math.floor(this._now() / 1000));
+    await this._transport.writeSoc(this.owner, toHex(update.identifier), toHex(update.signature), update.data, this._batch);
+    this._cachedRef = root;
+    this._nextIndex = index + 1;
+    this._cacheExpiry = this._now() + this._ttl;
+  }
+
+  async get() {
+    if (this._cachedRef !== null && this._now() < this._cacheExpiry) return this._cachedRef;
+    let delay = this._backoff;
+    for (let attempt = 0; attempt < this._maxRetries; attempt++) {
+      try {
+        const latest = await this._resolveLatestIndex();
+        if (latest === null) return this._cachedRef; // empty feed (definitive)
+        const indexNext = latest + 1;
+        if (indexNext > this._nextIndex || this._cachedRef === null) {
+          const { reference } = await this._atIndex(latest);
+          this._cachedRef = reference;
+          this._nextIndex = indexNext;
+          this._cacheExpiry = this._now() + this._ttl;
+          return this._cachedRef;
+        }
+        if (indexNext === this._nextIndex) {
+          this._cacheExpiry = this._now() + this._ttl; // confirmed unchanged
+          return this._cachedRef;
+        }
+        // indexNext below the floor: a stale-early lookup; retry for a fresher one
+      } catch (e) {
+        if (!(e instanceof FeedTransportError && TRANSIENT.has(e.status))) throw e;
+      }
+      if (attempt < this._maxRetries - 1) {
+        await this._sleep(delay);
+        delay = Math.min(delay * 2, this._backoffCap);
+      }
+    }
+    return this._cachedRef; // last known, or null if never resolved
+  }
+
+  async _getFresh() {
+    this._cacheExpiry = 0;
+    return this.get();
+  }
+
+  /**
+   * Best-effort compare-and-set for reconciling commits: true only if the
+   * feed still resolved to `expected` and `root` was written and read back.
+   * A feed cannot claim an index atomically (Bee accepts a second update at
+   * a used index), so two writers at the same index at the same moment can
+   * both believe they won; this narrows the window, it does not close it.
+   */
+  async compareAndSet(expected, root) {
+    if ((await this._getFresh()) !== expected) return false;
+    await this.set(root);
+    return (await this._getFresh()) === root;
+  }
+
+  async _resolveLatestIndex() {
+    const hint = this._nextIndex - 2;
+    if (hint >= 1 && typeof this._transport.feedHead === 'function') {
+      try {
+        const head = await this._transport.feedHead(this.owner, this.topic, hint);
+        if (head !== null && head !== undefined) return head;
+      } catch (e) {
+        if (!(e instanceof FeedTransportError && TRANSIENT.has(e.status))) throw e;
+      }
+    }
+    return this._probeLatestIndex();
+  }
+
+  /** Highest existing index (null: empty), by exponential + binary search over the update chunks. */
+  async _probeLatestIndex() {
+    if (!(await this._socExists(0))) return null;
+    let lo = 0;
+    let hi = 1;
+    while (await this._socExists(hi)) {
+      lo = hi;
+      hi *= 2;
+    }
+    while (hi - lo > 1) {
+      const mid = Math.floor((lo + hi) / 2);
+      if (await this._socExists(mid)) lo = mid;
+      else hi = mid;
+    }
+    return lo;
+  }
+
+  /** Does the update at `index` exist? 404 is the one definitive no; 500 (and 429) are retried. */
+  async _socExists(index) {
+    let delay = this._backoff;
+    for (let attempt = 0; attempt < this._maxRetries; attempt++) {
+      try {
+        await this._atIndex(index);
+        return true;
+      } catch (e) {
+        if (!(e instanceof FeedTransportError)) throw e;
+        if (e.status === 404) return false;
+        if ((e.status !== 500 && e.status !== 429) || attempt === this._maxRetries - 1) throw e;
+      }
+      await this._sleep(delay);
+      delay = Math.min(delay * 2, this._backoffCap);
+    }
+    return false;
+  }
+
+  _address(index) {
+    return socAddress(feedIdentifier(this._topic, index), this._owner);
+  }
+
+  /** The verified update at `index`: {reference, timestamp}. */
+  async _atIndex(index) {
+    const address = this._address(index);
+    const soc = await this._transport.readSoc(toHex(address));
+    verifySoc(soc, this._owner, address);
+    return payloadFromSoc(soc);
+  }
+
+  /** The update at `index` as a self-contained envelope that `verifyFeedUpdate` checks with no node. */
+  async update(index) {
+    const soc = await this._transport.readSoc(toHex(this._address(index)));
+    return { format: FEED_UPDATE_FORMAT, version: 1, owner: this.owner, topic: this.topic, index, soc: toHex(soc) };
+  }
+
+  /** The updates from `start` up to `stop` (default: past the tip, found by probing), in index order. */
+  async updates(start = 0, stop = null) {
+    if (stop === null) {
+      const tip = await this._probeLatestIndex();
+      stop = tip === null ? 0 : tip + 1;
+    }
+    const out = [];
+    for (let i = start; i < stop; i++) out.push(await this.update(i));
+    return out;
+  }
+}
diff --git a/js/src/index.js b/js/src/index.js
index 15780ec..4f5fffe 100644
--- a/js/src/index.js
+++ b/js/src/index.js
@@ -14,3 +14,8 @@ export {
   swarmRef, sha256Ref, splitData, chunkAddress, bmtRoot, redundancyFor, CHUNK_SIZE,
 } from './address.js';
 export { DEFAULT_NODE_CACHE_SIZE } from './trie.js';
+export {
+  SwarmFeedPointer, BeeFeedTransport, ProviderFeedTransport, FeedTransportError, ProofError,
+  verifyFeedUpdate, verifyEquivocation, FEED_UPDATE_FORMAT, topicOf, feedIdentifier, socAddress, addressOf,
+} from './feeds.js';
+export { swarmStore } from './swarm.js';
diff --git a/js/src/swarm.js b/js/src/swarm.js
new file mode 100644
index 0000000..04de78d
--- /dev/null
+++ b/js/src/swarm.js
@@ -0,0 +1,26 @@
+// A whole store on Swarm in one call, as Python's `swarm_store`: blobs on a
+// Bee node, the latest root in a Swarm feed.
+
+import { RecordStore } from './recordstore.js';
+import { BeeBytesStore } from './stores.js';
+import { SwarmFeedPointer } from './feeds.js';
+
+/**
+ * A RecordStore that lives on Swarm, at its feed's current root.
+ *
+ *   await swarmStore('my-notes', { signer: key, batch })  // publish your own
+ *   await swarmStore('my-notes', { owner: address })      // follow someone else's
+ *
+ * Writing needs a postage batch of the node's (`batch`); unlike Python's
+ * `stamp="auto"`, none is picked for you.
+ */
+export async function swarmStore(topic, {
+  url = 'http://localhost:1633', batch = null, signer = null, owner = null, feedTtl = 15, concurrency = 32,
+} = {}) {
+  if (!signer && !owner) {
+    throw new TypeError('swarmStore needs a signer (private key hex, to publish) or an owner (address hex, to follow)');
+  }
+  const blobs = new BeeBytesStore(url, { batch, concurrency });
+  const pointer = new SwarmFeedPointer(url, topic, { signer, owner, batch: signer ? batch : null, feedTtl });
+  return RecordStore.open(blobs, { pointer });
+}
diff --git a/js/test/feeds.test.js b/js/test/feeds.test.js
new file mode 100644
index 0000000..83dfca8
--- /dev/null
+++ b/js/test/feeds.test.js
@@ -0,0 +1,235 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+  FeedTransportError, MemoryBytesStore, MemoryPointer, ProofError, ProviderFeedTransport, RecordStore,
+  SwarmFeedPointer, VerificationError, addressOf, feedIdentifier, socAddress, topicOf,
+  verifyEquivocation, verifyFeedUpdate,
+} from '../src/index.js';
+import { buildFeedUpdate, verifySoc } from '../src/feeds.js';
+import { concatBytes, fromHex, toHex } from '../src/bytes.js';
+import { collect, fixture } from './helpers.js';
+
+const feeds = fixture('feeds.json');
+const noWait = async () => {};
+
+// -- cross-language -----------------------------------------------------------
+
+test('owners and topics derive as in Python (a 64-hex name is hashed too)', () => {
+  for (const { key, owner } of feeds.owners) assert.equal(toHex(addressOf(key)), owner);
+  for (const { name, topic } of feeds.topics) assert.equal(toHex(topicOf(name)), topic);
+});
+
+test('identifiers and update addresses match Python', () => {
+  for (const a of feeds.addresses) {
+    const id = feedIdentifier(fromHex(a.topic), a.index);
+    assert.equal(toHex(id), a.identifier);
+    assert.equal(toHex(socAddress(id, fromHex(a.owner))), a.address);
+  }
+});
+
+test('signed updates are byte-identical to the ones swarmfs writes', async () => {
+  for (const u of feeds.updates) {
+    const built = await buildFeedUpdate(u.key, fromHex(u.topic), u.index, u.root, u.timestamp);
+    assert.equal(toHex(built.identifier), u.identifier);
+    assert.equal(toHex(built.address), u.address);
+    assert.equal(toHex(built.soc), u.soc, `update ${u.index}: same signature, same bytes`);
+  }
+});
+
+test('updates Python published verify offline to what Python read from them', () => {
+  for (const u of feeds.updates) assert.deepEqual(verifyFeedUpdate(u.envelope, u.owner, u.topic), u.verified);
+  const first = feeds.updates[0];
+  assert.deepEqual(verifyFeedUpdate(first.envelope, `0x${first.owner.toUpperCase()}`, 'recordstore/test'), first.verified);
+});
+
+test('an equivocation is proven, and a sequence is not one', () => {
+  const { a, b, index } = feeds.equivocation;
+  const [ua, ub] = [feeds.updates[a], feeds.updates[b]];
+  assert.equal(verifyEquivocation(ua.envelope, ub.envelope, ua.owner, ua.topic), index);
+  assert.throws(() => verifyEquivocation(feeds.updates[0].envelope, ua.envelope, ua.owner, ua.topic), ProofError);
+  assert.throws(() => verifyEquivocation(ua.envelope, ua.envelope, ua.owner, ua.topic), ProofError);
+});
+
+test('every update Python refuses is refused', () => {
+  for (const { name, envelope } of feeds.refused) {
+    assert.throws(() => verifyFeedUpdate(envelope, envelope.owner, feeds.topics[0].topic), ProofError, name);
+  }
+});
+
+// -- the pointer, against an in-memory feed -----------------------------------
+
+/** A feed store that checks uploads as Bee does, and can misbehave like a light node. */
+class MemoryFeedTransport {
+  constructor({ chunkFailures = 0, lookups = false, staleLookups = 0, seed = 1 } = {}) {
+    this.chunks = new Map();
+    this.chunkFailures = chunkFailures;
+    this.staleLookups = staleLookups;
+    this.seed = seed;
+    this.reads = 0;
+    if (lookups) this.feedHead = this._feedHead;
+  }
+
+  random() {
+    this.seed = (this.seed * 1103515245 + 12345) % 2 ** 31;
+    return this.seed / 2 ** 31;
+  }
+
+  async readSoc(address) {
+    this.reads++;
+    if (this.random() < this.chunkFailures) throw new FeedTransportError('read chunk failed', 500);
+    const soc = this.chunks.get(address);
+    if (!soc) throw new FeedTransportError('not found', 404);
+    return soc;
+  }
+
+  async writeSoc(owner, identifier, signature, data) {
+    const id = fromHex(identifier);
+    const ownerB = fromHex(owner);
+    const address = socAddress(id, ownerB);
+    const soc = concatBytes(id, fromHex(signature), data);
+    verifySoc(soc, ownerB, address); // Bee refuses an update the owner did not sign
+    this.chunks.set(toHex(address), soc); // and, like Bee, accepts a second one at a used index
+  }
+
+  async _feedHead(owner, topic) {
+    if (this.random() < this.staleLookups) {
+      if (this.random() < 0.5) throw new FeedTransportError('lookup failed', 404);
+      return 0; // stale-early
+    }
+    let i = 0;
+    while (this.chunks.has(toHex(socAddress(feedIdentifier(fromHex(topic), i), fromHex(owner))))) i++;
+    return i === 0 ? null : i - 1;
+  }
+}
+
+const KEY = '3b'.repeat(32);
+const OTHER = '4c'.repeat(32);
+const ROOT = (n) => n.toString(16).padStart(2, '0').repeat(32);
+
+test('an empty feed reads as null; a published root reads back, also to a follower', async () => {
+  const feed = new MemoryFeedTransport();
+  const writer = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  assert.equal(await writer.get(), null);
+  await writer.set(ROOT(1));
+  assert.equal(await writer.get(), ROOT(1));
+  const follower = new SwarmFeedPointer(feed, 'notes', { owner: writer.owner, sleep: noWait });
+  assert.equal(await follower.get(), ROOT(1));
+  assert.equal(follower.owner, toHex(addressOf(KEY)));
+  await assert.rejects(follower.set(ROOT(2)), /signer/);
+});
+
+test('updates go to consecutive indexes; a cold writer finds the tip by probing', async () => {
+  const feed = new MemoryFeedTransport();
+  const writer = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  for (let i = 1; i <= 5; i++) await writer.set(ROOT(i));
+  const cold = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  await cold.set(ROOT(6));
+  assert.equal(feed.chunks.size, 6);
+  const follower = new SwarmFeedPointer(feed, 'notes', { owner: writer.owner, sleep: noWait });
+  assert.equal(await follower.get(), ROOT(6));
+  const envelopes = await follower.updates();
+  assert.deepEqual(envelopes.map((e) => verifyFeedUpdate(e, writer.owner, 'notes').root), [1, 2, 3, 4, 5, 6].map(ROOT));
+});
+
+test('flaky chunk reads and stale or failed lookups still resolve the latest root', async () => {
+  const feed = new MemoryFeedTransport({ lookups: true });
+  const writer = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  for (let i = 1; i <= 9; i++) await writer.set(ROOT(i));
+  feed.chunkFailures = 0.3;
+  feed.staleLookups = 0.5;
+  let latest = ROOT(9);
+  for (let trial = 0; trial < 20; trial++) {
+    const follower = new SwarmFeedPointer(feed, 'notes', { owner: writer.owner, sleep: noWait, maxLookupRetries: 30 });
+    assert.equal(await follower.get(), latest);
+    const next = ROOT(10 + trial);
+    await writer.set(next);
+    assert.equal(await follower.get(), latest, 'within the cache lifetime the pointer does not look again');
+    assert.equal(await follower._getFresh(), next);
+    latest = next;
+  }
+});
+
+test('a tampered update is an error, not a missing one', async () => {
+  const feed = new MemoryFeedTransport();
+  const writer = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  await writer.set(ROOT(1));
+  const [address, soc] = [...feed.chunks][0];
+  const forged = Uint8Array.from(soc);
+  forged[forged.length - 1] ^= 1;
+  feed.chunks.set(address, forged);
+  const follower = new SwarmFeedPointer(feed, 'notes', { owner: writer.owner, sleep: noWait });
+  await assert.rejects(follower.get(), VerificationError);
+});
+
+test('compareAndSet moves the feed only from the expected root', async () => {
+  const feed = new MemoryFeedTransport();
+  const p = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  await p.set(ROOT(1));
+  assert.equal(await p.compareAndSet(ROOT(2), ROOT(3)), false);
+  assert.equal(await p.compareAndSet(ROOT(1), ROOT(3)), true);
+  assert.equal(await p.get(), ROOT(3));
+});
+
+test('a store behind a feed: a follower opens it, two writers reconcile', async () => {
+  const blobs = new MemoryBytesStore({ addressing: 'swarm' });
+  const feed = new MemoryFeedTransport();
+  const pointer = () => new SwarmFeedPointer(feed, 'shared', { signer: KEY, sleep: noWait });
+  const a = await RecordStore.open(blobs, { pointer: pointer() });
+  const b = await RecordStore.open(blobs, { pointer: pointer() });
+  a.put('from/a', 1);
+  b.put('from/b', 2);
+  await a.commit({ reconcile: true });
+  const root = await b.commit({ reconcile: true }); // b's pointer finds a's update and merges
+  const follower = await RecordStore.open(blobs, {
+    pointer: new SwarmFeedPointer(feed, 'shared', { owner: toHex(addressOf(KEY)), sleep: noWait }),
+  });
+  assert.equal(follower.root, root);
+  assert.deepEqual(await collect(follower.items()), [['from/a', 1], ['from/b', 2]]);
+});
+
+test('a feed is its owner’s alone: another key writes another feed', async () => {
+  const feed = new MemoryFeedTransport();
+  await new SwarmFeedPointer(feed, 'notes', { signer: OTHER, sleep: noWait }).set(ROOT(7));
+  const mine = new SwarmFeedPointer(feed, 'notes', { owner: toHex(addressOf(KEY)), sleep: noWait });
+  assert.equal(await mine.get(), null);
+});
+
+// -- through Freedom's provider ---------------------------------------------
+
+/** window.swarm's swarm_readSingleOwnerChunk over the in-memory feed. */
+function providerOver(feed) {
+  return {
+    async request({ method, params }) {
+      assert.equal(method, 'swarm_readSingleOwnerChunk');
+      const soc = feed.chunks.get(params.address);
+      if (!soc) throw Object.assign(new Error('not found'), { data: { reason: 'chunk_not_found' } });
+      const payload = soc.subarray(105);
+      return {
+        data: btoa(String.fromCharCode(...payload)), encoding: 'base64',
+        span: payload.length, reference: params.address,
+        identifier: toHex(soc.subarray(0, 32)), signature: `0x${toHex(soc.subarray(32, 97))}`,
+      };
+    },
+  };
+}
+
+test('through window.swarm: any feed reads (and is checked again); writing waits for #647', async () => {
+  const feed = new MemoryFeedTransport();
+  const writer = new SwarmFeedPointer(feed, 'notes', { signer: KEY, sleep: noWait });
+  for (let i = 1; i <= 3; i++) await writer.set(ROOT(i));
+  const transport = new ProviderFeedTransport(providerOver(feed));
+  const reader = new SwarmFeedPointer(transport, 'notes', { owner: writer.owner, sleep: noWait });
+  assert.equal(await reader.get(), ROOT(3));
+  const own = new SwarmFeedPointer(transport, 'notes', { signer: KEY, sleep: noWait });
+  await assert.rejects(own.set(ROOT(4)), /#647/);
+});
+
+test('MemoryPointer and SwarmFeedPointer are interchangeable to a store', async () => {
+  for (const pointer of [new MemoryPointer(), new SwarmFeedPointer(new MemoryFeedTransport(), 't', { signer: KEY, sleep: noWait })]) {
+    const store = await RecordStore.open(new MemoryBytesStore(), { pointer });
+    store.put('k', 'v');
+    assert.equal(await pointer.get(), null);
+    const root = await store.commit();
+    assert.equal(await pointer.get(), root);
+  }
+});
diff --git a/js/test/fixtures/feeds.json b/js/test/fixtures/feeds.json
new file mode 100644
index 0000000..c5432f0
--- /dev/null
+++ b/js/test/fixtures/feeds.json
@@ -0,0 +1,234 @@
+{
+ "note": "Generated by js/tools/make_fixtures.py from the Python reference implementation; do not edit by hand.",
+ "owners": [
+  {
+   "key": "1111111111111111111111111111111111111111111111111111111111111111",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a"
+  },
+  {
+   "key": "a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a701",
+   "owner": "087bf3818c89420a2be42c4e2affb4df77d402fa"
+  }
+ ],
+ "topics": [
+  {
+   "name": "recordstore/test",
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13"
+  },
+  {
+   "name": "loop-twonode-1791730596",
+   "topic": "d3905aa3645eaebb27eb1c091cd8b9a1a610a5f16a3668fdb630b511d95d32d1"
+  },
+  {
+   "name": "abababababababababababababababababababababababababababababababab",
+   "topic": "edfeca1e60bc3e32447bebed8111663879980928231fc72d42ad4ff98f1cfa94"
+  },
+  {
+   "name": "é😀",
+   "topic": "c387a34b042bca8dfbc38dbc6489ce0084ad0f6f09690bd69bdfc837d104de05"
+  }
+ ],
+ "addresses": [
+  {
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "index": 0,
+   "identifier": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb9",
+   "address": "e46a6ee5abeb540d02777e2ace19e24aaa88b86b2b882c9752210e0c22f0b813"
+  },
+  {
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "index": 1,
+   "identifier": "f12cb2f786e4974b850928d4a68427975a4bce7fae8e548071c682697a359c87",
+   "address": "2c6b7a2914df8e19ee860c98fb1ab6f40c908643244a87b9ddab3fa45e1f2222"
+  },
+  {
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "index": 255,
+   "identifier": "a6ae3f1de883606377d0b8858b05b3da4aa6ac903f4814ec427048f5f20ac039",
+   "address": "1cc6f9a9d6768e4df05a8fa8522fb2cb78e10b83782834da6204aea2add073e1"
+  },
+  {
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "index": 4294967301,
+   "identifier": "647f672f65833e9d91bc44137ec3ee5dd89ea6f57b791c347f6f0f91b67f3731",
+   "address": "b2c3ee08c34cf0aa7b15b3b8c200ec015f9c826851a7817b31139075452355db"
+  }
+ ],
+ "updates": [
+  {
+   "key": "1111111111111111111111111111111111111111111111111111111111111111",
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "index": 0,
+   "root": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+   "timestamp": 1790000000,
+   "identifier": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb9",
+   "address": "e46a6ee5abeb540d02777e2ace19e24aaa88b86b2b882c9752210e0c22f0b813",
+   "soc": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb94e471f0b5e11d98f30aace65e145f155398e3818a6cef186a660968e1e171aef1ea9a591ab7188523ce58d4c40f366d531b74ad09dc94c06449f3bd69a6156461c2800000000000000000000006ab13b805a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 0,
+    "soc": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb94e471f0b5e11d98f30aace65e145f155398e3818a6cef186a660968e1e171aef1ea9a591ab7188523ce58d4c40f366d531b74ad09dc94c06449f3bd69a6156461c2800000000000000000000006ab13b805a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
+   },
+   "verified": {
+    "index": 0,
+    "root": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+    "timestamp": 1790000000
+   }
+  },
+  {
+   "key": "1111111111111111111111111111111111111111111111111111111111111111",
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "index": 7,
+   "root": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
+   "timestamp": 1790000123,
+   "identifier": "a6e371abc13b2766f8bdea58c793b88a9b2fb81259d69fe29acbae1cdc65a521",
+   "address": "6684ad0551f5b22822518b055b6aee787f0eed07177d96678debff7e0030c7a4",
+   "soc": "a6e371abc13b2766f8bdea58c793b88a9b2fb81259d69fe29acbae1cdc65a521e1018c1c4c3a88e407805b41f276d2fefa654fd0f757a83e1018a9d3d5b87983514457b9edafac538a7b8999be372525ee93438ee3990640b1c26c64f5fc9a791b2800000000000000000000006ab13bfbc3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 7,
+    "soc": "a6e371abc13b2766f8bdea58c793b88a9b2fb81259d69fe29acbae1cdc65a521e1018c1c4c3a88e407805b41f276d2fefa654fd0f757a83e1018a9d3d5b87983514457b9edafac538a7b8999be372525ee93438ee3990640b1c26c64f5fc9a791b2800000000000000000000006ab13bfbc3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3"
+   },
+   "verified": {
+    "index": 7,
+    "root": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
+    "timestamp": 1790000123
+   }
+  },
+  {
+   "key": "a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a701",
+   "topic": "d3905aa3645eaebb27eb1c091cd8b9a1a610a5f16a3668fdb630b511d95d32d1",
+   "index": 3,
+   "root": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+   "timestamp": 1800000000,
+   "identifier": "f9f4904418b128833442d5ba2885ab3d8dcaa5f896d93ccf6bb31b89944f6a09",
+   "address": "9a86f6793b383f8220861b1fd104347026b198b3a9ac0bcec6dc2d84ec36910f",
+   "soc": "f9f4904418b128833442d5ba2885ab3d8dcaa5f896d93ccf6bb31b89944f6a09cccde17a6ec27456d19bf92680321b273f63bbbb0124ad98e8e73963e42b61e61107d71456217b44e6f92c01d73b7a8502f07412370ea7ecf7948931f46a16031c2800000000000000000000006b49d2005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+   "owner": "087bf3818c89420a2be42c4e2affb4df77d402fa",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "087bf3818c89420a2be42c4e2affb4df77d402fa",
+    "topic": "d3905aa3645eaebb27eb1c091cd8b9a1a610a5f16a3668fdb630b511d95d32d1",
+    "index": 3,
+    "soc": "f9f4904418b128833442d5ba2885ab3d8dcaa5f896d93ccf6bb31b89944f6a09cccde17a6ec27456d19bf92680321b273f63bbbb0124ad98e8e73963e42b61e61107d71456217b44e6f92c01d73b7a8502f07412370ea7ecf7948931f46a16031c2800000000000000000000006b49d2005a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
+   },
+   "verified": {
+    "index": 3,
+    "root": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+    "timestamp": 1800000000
+   }
+  },
+  {
+   "key": "1111111111111111111111111111111111111111111111111111111111111111",
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "index": 7,
+   "root": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+   "timestamp": 1790000124,
+   "identifier": "a6e371abc13b2766f8bdea58c793b88a9b2fb81259d69fe29acbae1cdc65a521",
+   "address": "6684ad0551f5b22822518b055b6aee787f0eed07177d96678debff7e0030c7a4",
+   "soc": "a6e371abc13b2766f8bdea58c793b88a9b2fb81259d69fe29acbae1cdc65a521342f33ac374698320485184ea8a294f848e60385c590ff5ea29fd079ff1832892d75bff5c3b5c34eda9e798b5d76002dda2c74ac005c5cdde0b3bf0ec52047951c2800000000000000000000006ab13bfc5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 7,
+    "soc": "a6e371abc13b2766f8bdea58c793b88a9b2fb81259d69fe29acbae1cdc65a521342f33ac374698320485184ea8a294f848e60385c590ff5ea29fd079ff1832892d75bff5c3b5c34eda9e798b5d76002dda2c74ac005c5cdde0b3bf0ec52047951c2800000000000000000000006ab13bfc5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
+   },
+   "verified": {
+    "index": 7,
+    "root": "5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a",
+    "timestamp": 1790000124
+   }
+  },
+  {
+   "key": "1111111111111111111111111111111111111111111111111111111111111111",
+   "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+   "index": 9,
+   "root": "77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777",
+   "timestamp": 1790000200,
+   "identifier": "37685181963c9ae62b50322900d43741262787a7d9c28e9086f70eb06a8ea034",
+   "address": "2a8ea65842dd1b7cb2956e9e2622bc83cb77c99729068cf83329f50f4b6c8b3a",
+   "soc": "37685181963c9ae62b50322900d43741262787a7d9c28e9086f70eb06a8ea034403382ff5634bc1f2519e5339e16cb011ab46e5572379b220ccc11e7ceffdf4973ec86a11af18e9cea12b370b63eefb8f0d0fe37700015ebfd50f6fc580032081b4800000000000000000000006ab13c4877777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777",
+   "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 9,
+    "soc": "37685181963c9ae62b50322900d43741262787a7d9c28e9086f70eb06a8ea034403382ff5634bc1f2519e5339e16cb011ab46e5572379b220ccc11e7ceffdf4973ec86a11af18e9cea12b370b63eefb8f0d0fe37700015ebfd50f6fc580032081b4800000000000000000000006ab13c4877777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777"
+   },
+   "verified": {
+    "index": 9,
+    "root": "77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777",
+    "timestamp": 1790000200
+   }
+  }
+ ],
+ "equivocation": {
+  "a": 1,
+  "b": 3,
+  "index": 7
+ },
+ "refused": [
+  {
+   "name": "signature byte flipped",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 0,
+    "soc": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb94e001f0b5e11d98f30aace65e145f155398e3818a6cef186a660968e1e171aef1ea9a591ab7188523ce58d4c40f366d531b74ad09dc94c06449f3bd69a6156461c2800000000000000000000006ab13b805a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
+   }
+  },
+  {
+   "name": "payload byte flipped",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 0,
+    "soc": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb94e471f0b5e11d98f30aace65e145f155398e3818a6cef186a660968e1e171aef1ea9a591ab7188523ce58d4c40f366d531b74ad09dc94c06449f3bd69a6156461c2800000000000000000000006ab13b805a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a00"
+   }
+  },
+  {
+   "name": "claims another index",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "19e7e376e7c213b7e7e7e46cc70a5dd086daff2a",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 1,
+    "soc": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb94e471f0b5e11d98f30aace65e145f155398e3818a6cef186a660968e1e171aef1ea9a591ab7188523ce58d4c40f366d531b74ad09dc94c06449f3bd69a6156461c2800000000000000000000006ab13b805a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
+   }
+  },
+  {
+   "name": "claims another owner",
+   "envelope": {
+    "format": "recordstore-feed-update",
+    "version": 1,
+    "owner": "0000000000000000000000000000000000000000",
+    "topic": "596916f18814d78dae04969ddfd46f793d05446f2aa60360dabfb26e831d9e13",
+    "index": 0,
+    "soc": "0d3f758d9a0a654f34f3e2f436236fbeb945622d5041d35fdcbf822cbaecfeb94e471f0b5e11d98f30aace65e145f155398e3818a6cef186a660968e1e171aef1ea9a591ab7188523ce58d4c40f366d531b74ad09dc94c06449f3bd69a6156461c2800000000000000000000006ab13b805a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a"
+   }
+  }
+ ]
+}
diff --git a/js/test/freedom/freedom_check.cjs b/js/test/freedom/freedom_check.cjs
index 644396a..ecd2862 100644
--- a/js/test/freedom/freedom_check.cjs
+++ b/js/test/freedom/freedom_check.cjs
@@ -113,6 +113,7 @@ async function main() {
       out.readRetries = (out.readRetries || 0) + 1;
       await sleep(15_000);
     }
+    out.feed = await inPage(window, 'window.rsCheck.feed()');
     await window.screenshot({ path: path.join(tmp, 'read.png') });
 
     if (doWrite) {
diff --git a/js/test/freedom/provider-check.js b/js/test/freedom/provider-check.js
index 6a01a2c..6277970 100644
--- a/js/test/freedom/provider-check.js
+++ b/js/test/freedom/provider-check.js
@@ -9,7 +9,9 @@
 // Both resolve to a plain object (also kept in window.rsCheck.results) and
 // never throw: an error comes back as {error, reason}.
 
-import { MemoryBytesStore, RecordStore, SwarmProviderBytesStore, canonicalJson } from '../../src/index.js';
+import {
+  MemoryBytesStore, ProviderFeedTransport, RecordStore, SwarmFeedPointer, SwarmProviderBytesStore, canonicalJson,
+} from '../../src/index.js';
 
 const SPACE_PACK = '67f6be2688e5fb7ca85a463c3d2929dad2b177bb3edb6bb910e8fa110598cff3';
 const $ = (id) => document.getElementById(id);
@@ -61,6 +63,24 @@ async function read(root = SPACE_PACK, prefix = 'ab', keys = ['abbey', 'abalone'
   }
 }
 
+/** Follow a store Python keeps behind a feed (loopmarket's test maker book), all through window.swarm. */
+async function feed(topic = 'loop-twonode-1791730596', owner = '300f4daa2653102abde4c66f75ea512c0bb81571') {
+  $('status').textContent = 'following a feed…';
+  const provider = counting(window.swarm);
+  const started = performance.now();
+  try {
+    const pointer = new SwarmFeedPointer(new ProviderFeedTransport(provider), topic, { owner });
+    const store = await RecordStore.open(new SwarmProviderBytesStore(provider), { pointer });
+    const keys = await collect(store.keys());
+    return show('feed', {
+      topic, owner, root: store.root, records: keys.length, firstKeys: keys.slice(0, 3),
+      ms: Math.round(performance.now() - started), calls: provider.calls,
+    });
+  } catch (e) {
+    return show('feed', { ...failure(e), ms: Math.round(performance.now() - started), calls: provider.calls });
+  }
+}
+
 const RECORDS = () => ({
   'check/small': { hello: 'Freedom', at: new Date().toISOString().slice(0, 10) },
   'check/unicode/é😀': ['é', '中文', '😀'],
@@ -101,6 +121,7 @@ window.rsCheck = {
   results: {},
   provider: Boolean(window.swarm && window.swarm.isFreedomBrowser),
   read,
+  feed,
   write,
   ready: true,
 };
diff --git a/js/test/live.test.js b/js/test/live.test.js
index 5b057f2..246a7ea 100644
--- a/js/test/live.test.js
+++ b/js/test/live.test.js
@@ -1,29 +1,73 @@
-// Live: read a store that Python published on Swarm, through a node with
-// Bee's /bytes API (Bee, or Ant inside Freedom Browser). Skipped unless
+// Live checks against Swarm, through a node with Bee's API (Bee, or Ant
+// inside Freedom Browser). Skipped unless RECORDSTORE_BEE_URL is set:
 //
 //   RECORDSTORE_BEE_URL=http://localhost:1633 npm test
 //
-// RECORDSTORE_LIVE_ROOT picks the root (default: OntoDAG's `space` pack,
-// core v14, kept on Swarm by swarm-keep). Every blob is checked against its
-// reference as it is read.
+// Reading needs nothing else. RECORDSTORE_BEE_BATCH (a postage batch of the
+// node's) also runs the write check, which spends a few chunks of it.
+// Every blob and feed update read is checked against its reference/owner.
 
 import { test } from 'node:test';
 import assert from 'node:assert/strict';
-import { BeeBytesStore, RecordStore } from '../src/index.js';
+import { BeeBytesStore, RecordStore, SwarmFeedPointer, swarmStore, verifyFeedUpdate } from '../src/index.js';
+import { toHex } from '../src/bytes.js';
 
 const url = process.env.RECORDSTORE_BEE_URL;
+const batch = process.env.RECORDSTORE_BEE_BATCH;
+const skip = !url && 'set RECORDSTORE_BEE_URL to run';
 const root = process.env.RECORDSTORE_LIVE_ROOT
-  || '67f6be2688e5fb7ca85a463c3d2929dad2b177bb3edb6bb910e8fa110598cff3';
+  || '67f6be2688e5fb7ca85a463c3d2929dad2b177bb3edb6bb910e8fa110598cff3'; // OntoDAG's `space` pack
 
-test('reads a store Python published on Swarm', { skip: !url && 'set RECORDSTORE_BEE_URL to run' }, async () => {
-  const store = RecordStore.at(root, new BeeBytesStore(url));
+test('reads a store Python published on Swarm', { skip }, async () => {
+  const store = RecordStore.at(root, new BeeBytesStore(url, { concurrency: 32 }));
   const started = Date.now();
   const keys = [];
   for await (const key of store.keys()) keys.push(key);
   const sorted = [...keys].sort((a, b) => (Buffer.from(a) < Buffer.from(b) ? -1 : 1));
   assert.ok(keys.length > 0, 'the store has records');
   assert.deepEqual(keys, sorted, 'keys arrive in UTF-8 byte order');
-  const sample = await store.get(keys[Math.floor(keys.length / 2)]);
-  assert.notEqual(sample, undefined);
+  assert.notEqual(await store.get(keys[Math.floor(keys.length / 2)]), undefined);
   console.log(`# ${keys.length} keys in ${Date.now() - started} ms; e.g. ${JSON.stringify(keys.slice(0, 3))}`);
 });
+
+test("follows a store Python keeps behind a feed (loopmarket's test maker book)", { skip }, async () => {
+  const started = Date.now();
+  const store = await swarmStore('loop-twonode-1791730596', { url, owner: '300f4daa2653102abde4c66f75ea512c0bb81571' });
+  assert.match(store.root, /^[0-9a-f]{64}$/);
+  const items = [];
+  for await (const item of store.items()) items.push(item);
+  assert.ok(items.length > 0);
+  console.log(`# book root ${store.root.slice(0, 16)}..., ${items.length} records in ${Date.now() - started} ms; keys ${JSON.stringify(items.map(([k]) => k).slice(0, 6))}`);
+});
+
+test("reads the OntoDAG page's feed (written by Python's SwarmFeedPointer)", { skip }, async () => {
+  const pointer = new SwarmFeedPointer(url, 'ontodag/freedom-page', { owner: 'e6d5bbdd5028b84d5eaf7be5fe07bed1ad73a110' });
+  assert.equal(pointer.topic, '19e8777d991f320d8d8158a4016858ad2f9c66b89abf0f9393ef239fabbbf2e5');
+  const ref = await pointer.get();
+  assert.match(ref, /^[0-9a-f]{64}$/);
+  const updates = await pointer.updates();
+  const last = verifyFeedUpdate(updates.at(-1), pointer.owner, 'ontodag/freedom-page');
+  assert.equal(last.root, ref);
+  console.log(`# page feed: ${updates.length} updates, latest ${ref.slice(0, 16)}... at ${new Date(last.timestamp * 1000).toISOString()}`);
+});
+
+test('publishes a store behind a new feed and follows it', { skip: skip || (!batch && 'set RECORDSTORE_BEE_BATCH to run the write check') }, async () => {
+  const signer = toHex(crypto.getRandomValues(new Uint8Array(32)));
+  const topic = `recordstore-js-live-${Date.now()}`;
+  const writer = await swarmStore(topic, { url, signer, batch });
+  assert.equal(writer.root, null);
+  writer.put('a', { n: 1 });
+  writer.put('b', 'two');
+  const first = await writer.commit();
+  writer.put('c', [3]);
+  await writer.delete('a');
+  const second = await writer.commit();
+  const follower = await swarmStore(topic, { url, owner: writer._pointer.owner });
+  assert.equal(follower.root, second);
+  const items = [];
+  for await (const item of follower.items()) items.push(item);
+  assert.deepEqual(items, [['b', 'two'], ['c', [3]]]);
+  const updates = await follower._pointer.updates();
+  assert.deepEqual(updates.map((u) => verifyFeedUpdate(u, writer._pointer.owner, topic).root), [first, second]);
+  console.log(`# wrote feed ${topic} (owner ${writer._pointer.owner}): roots ${first.slice(0, 12)}..., ${second.slice(0, 12)}...`);
+});
diff --git a/js/tools/make_fixtures.py b/js/tools/make_fixtures.py
index de0d116..4fe640f 100644
--- a/js/tools/make_fixtures.py
+++ b/js/tools/make_fixtures.py
@@ -331,6 +331,104 @@ def _op_json(op):
     return {"op": "commit"}
 
 
+# ---------------------------------------------------------------------------
+# Feeds: topics, owners, addresses, signed updates (needs swarmfs[feeds])
+# ---------------------------------------------------------------------------
+
+KEYS = ["11" * 32, "a7" * 31 + "01"]
+TOPIC_NAMES = ["recordstore/test", "loop-twonode-1791730596", "ab" * 32, "é😀"]
+
+
+def pointer_for(name, key):
+    """The Python pointer, for how it hashes topics and derives owners (no
+    network is touched until it is used)."""
+    from recordstore import SwarmFeedPointer
+    return SwarmFeedPointer("http://127.0.0.1:9", name, signer=key)
+
+
+def signed_update(key, topic_hex, index, root, timestamp):
+    """A feed update exactly as swarmfs's FeedOps.update publishes it (its
+    own code, with the clock fixed and the upload captured)."""
+    import asyncio
+    from unittest import mock
+    from swarmfs.feeds import FeedOps, FeedSigner, feed_identifier, soc_address
+
+    captured = {}
+
+    class Capture:
+        async def soc_post(self, owner, identifier, signature, data, stamp):
+            captured.update(owner=owner, identifier=identifier,
+                            signature=signature, data=bytes(data))
+            return "00" * 32
+
+    signer = FeedSigner(key)
+    with mock.patch("swarmfs.feeds.time.time", return_value=float(timestamp)):
+        asyncio.run(FeedOps(Capture()).update(
+            signer, bytes.fromhex(topic_hex), index, root, "00" * 32))
+    identifier = bytes.fromhex(captured["identifier"])
+    soc = identifier + bytes.fromhex(captured["signature"]) + captured["data"]
+    return {
+        "key": key, "topic": topic_hex, "index": index, "root": root,
+        "timestamp": timestamp,
+        "identifier": captured["identifier"],
+        "address": soc_address(feed_identifier(bytes.fromhex(topic_hex), index),
+                               signer.owner).hex(),
+        "soc": soc.hex(),
+    }
+
+
+def feed_cases():
+    from recordstore import FEED_UPDATE_FORMAT, ProofError, verify_equivocation, verify_feed_update
+    from swarmfs.feeds import feed_identifier, soc_address
+
+    owners = [{"key": k, "owner": pointer_for("x", k).owner} for k in KEYS]
+    topics = [{"name": n, "topic": pointer_for(n, KEYS[0]).topic} for n in TOPIC_NAMES]
+    t0 = topics[0]["topic"]
+    owner0 = bytes.fromhex(owners[0]["owner"])
+    addresses = []
+    for index in (0, 1, 255, 2**32 + 5):
+        ident = feed_identifier(bytes.fromhex(t0), index)
+        addresses.append({"topic": t0, "owner": owners[0]["owner"], "index": index,
+                          "identifier": ident.hex(),
+                          "address": soc_address(ident, owner0).hex()})
+
+    root_a = "5a" * 32
+    root_b = "c3" * 32
+    root_64 = "77" * 64  # an encrypted reference: 64 bytes
+    updates = [signed_update(KEYS[0], t0, 0, root_a, 1_790_000_000),
+               signed_update(KEYS[0], t0, 7, root_b, 1_790_000_123),
+               signed_update(KEYS[1], topics[1]["topic"], 3, root_a, 1_800_000_000),
+               signed_update(KEYS[0], t0, 7, root_a, 1_790_000_124),   # equivocates with [1]
+               signed_update(KEYS[0], t0, 9, root_64, 1_790_000_200)]
+    for u in updates:
+        owner = pointer_for("x", u["key"]).owner
+        envelope = {"format": FEED_UPDATE_FORMAT, "version": 1, "owner": owner,
+                    "topic": u["topic"], "index": u["index"], "soc": u["soc"]}
+        verified = verify_feed_update(envelope, owner, u["topic"])
+        u.update(owner=owner, envelope=envelope,
+                 verified={"index": verified.index, "root": verified.root,
+                           "timestamp": verified.timestamp})
+    equivocation_index = verify_equivocation(updates[1]["envelope"], updates[3]["envelope"],
+                                             updates[1]["owner"], t0)
+    # Each must be refused by Python, and so by JavaScript.
+    bad = []
+    for name, tamper in [
+        ("signature byte flipped", lambda e: {**e, "soc": e["soc"][:66] + ("00" if e["soc"][66:68] != "00" else "01") + e["soc"][68:]}),
+        ("payload byte flipped", lambda e: {**e, "soc": e["soc"][:-2] + ("00" if e["soc"][-2:] != "00" else "01")}),
+        ("claims another index", lambda e: {**e, "index": e["index"] + 1}),
+        ("claims another owner", lambda e: {**e, "owner": "00" * 20}),
+    ]:
+        envelope = tamper(updates[0]["envelope"])
+        try:
+            verify_feed_update(envelope, envelope["owner"], t0)
+            raise AssertionError(f"Python accepted a tampered update: {name}")
+        except ProofError:
+            bad.append({"name": name, "envelope": envelope})
+    return {"owners": owners, "topics": topics, "addresses": addresses,
+            "updates": updates, "equivocation": {"a": 1, "b": 3, "index": equivocation_index},
+            "refused": bad}
+
+
 # ---------------------------------------------------------------------------
 
 def fixtures():
@@ -345,6 +443,7 @@ def fixtures():
         "stores.json": {"note": note, "cases": store_cases()},
         "merges.json": {"note": note, "resolver": "ours wins: DELETE if ours "
                         "is ABSENT, else ours", "cases": merge_cases()},
+        "feeds.json": {"note": note, **feed_cases()},
     }
 
 
diff --git a/js/vendor/noble-secp256k1/LICENSE b/js/vendor/noble-secp256k1/LICENSE
new file mode 100644
index 0000000..a5e0151
--- /dev/null
+++ b/js/vendor/noble-secp256k1/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019 Paul Miller (https://paulmillr.com)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the “Software”), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/js/vendor/noble-secp256k1/PROVENANCE.md b/js/vendor/noble-secp256k1/PROVENANCE.md
new file mode 100644
index 0000000..cfb988f
--- /dev/null
+++ b/js/vendor/noble-secp256k1/PROVENANCE.md
@@ -0,0 +1,24 @@
+# Vendored: @noble/secp256k1 2.3.0
+
+Source: the `@noble/secp256k1` npm package, version 2.3.0
+(https://github.com/paulmillr/noble-secp256k1), MIT license (see LICENSE).
+Single-file, zero-dependency, audited ECDSA implementation.
+
+Vendored subset: `index.js` only (unmodified ESM). Copied from swarmlite's
+`js/vendor/noble-secp256k1/`, which vendored it first; same file, same
+version.
+
+SHA-256 of `index.js`:
+`8ba6138826758ef7d13243f18f7fbf8b26c81c6abea706e692baa83dfe3827e1`
+
+Used for feed updates (`src/feeds.js`): signing them (`signAsync`, RFC 6979
+deterministic with low S, the same signatures libsecp256k1 makes for
+Python) and recovering the signer of each one read, so a forged update is
+caught even through an untrusted node or gateway.
+
+Vendored (rather than npm-installed) so that a page published on Swarm can
+import recordstore with relative URLs only.
+
+To upgrade: `npm pack @noble/secp256k1`, extract, copy `index.js` and
+LICENSE, update the version and hash here, run `npm test` (the feed
+fixtures pin signatures byte for byte).
diff --git a/js/vendor/noble-secp256k1/index.js b/js/vendor/noble-secp256k1/index.js
new file mode 100644
index 0000000..187f25d
--- /dev/null
+++ b/js/vendor/noble-secp256k1/index.js
@@ -0,0 +1,790 @@
+/*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */
+/**
+ * 4KB JS implementation of secp256k1 ECDSA / Schnorr signatures & ECDH.
+ * Compliant with RFC6979 & BIP340.
+ * @module
+ */
+/**
+ * Curve params. secp256k1 is short weierstrass / koblitz curve. Equation is y² == x³ + ax + b.
+ * * P = `2n**256n-2n**32n-2n**977n` // field over which calculations are done
+ * * N = `2n**256n - 0x14551231950b75fc4402da1732fc9bebfn` // group order, amount of curve points
+ * * h = `1n` // cofactor
+ * * a = `0n` // equation param
+ * * b = `7n` // equation param
+ * * Gx, Gy are coordinates of Generator / base point
+ */
+const secp256k1_CURVE = {
+    p: 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2fn,
+    n: 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n,
+    h: 1n,
+    a: 0n,
+    b: 7n,
+    Gx: 0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798n,
+    Gy: 0x483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8n,
+};
+const { p: P, n: N, Gx, Gy, b: _b } = secp256k1_CURVE;
+const L = 32; // field / group byte length
+const L2 = 64;
+// Helpers and Precomputes sections are reused between libraries
+// ## Helpers
+// ----------
+// error helper, messes-up stack trace
+const err = (m = '') => {
+    throw new Error(m);
+};
+const isBig = (n) => typeof n === 'bigint'; // is big integer
+const isStr = (s) => typeof s === 'string'; // is string
+const isBytes = (a) => a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array');
+/** assert is Uint8Array (of specific length) */
+const abytes = (a, l) => !isBytes(a) || (typeof l === 'number' && l > 0 && a.length !== l)
+    ? err('Uint8Array expected')
+    : a;
+/** create Uint8Array */
+const u8n = (len) => new Uint8Array(len);
+const u8fr = (buf) => Uint8Array.from(buf);
+const padh = (n, pad) => n.toString(16).padStart(pad, '0');
+const bytesToHex = (b) => Array.from(abytes(b))
+    .map((e) => padh(e, 2))
+    .join('');
+const C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 }; // ASCII characters
+const _ch = (ch) => {
+    if (ch >= C._0 && ch <= C._9)
+        return ch - C._0; // '2' => 50-48
+    if (ch >= C.A && ch <= C.F)
+        return ch - (C.A - 10); // 'B' => 66-(65-10)
+    if (ch >= C.a && ch <= C.f)
+        return ch - (C.a - 10); // 'b' => 98-(97-10)
+    return;
+};
+const hexToBytes = (hex) => {
+    const e = 'hex invalid';
+    if (!isStr(hex))
+        return err(e);
+    const hl = hex.length;
+    const al = hl / 2;
+    if (hl % 2)
+        return err(e);
+    const array = u8n(al);
+    for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
+        // treat each char as ASCII
+        const n1 = _ch(hex.charCodeAt(hi)); // parse first char, multiply it by 16
+        const n2 = _ch(hex.charCodeAt(hi + 1)); // parse second char
+        if (n1 === undefined || n2 === undefined)
+            return err(e);
+        array[ai] = n1 * 16 + n2; // example: 'A9' => 10*16 + 9
+    }
+    return array;
+};
+/** normalize hex or ui8a to ui8a */
+const toU8 = (a, len) => abytes(isStr(a) ? hexToBytes(a) : u8fr(abytes(a)), len);
+const cr = () => globalThis?.crypto; // WebCrypto is available in all modern environments
+const subtle = () => cr()?.subtle ?? err('crypto.subtle must be defined');
+// prettier-ignore
+const concatBytes = (...arrs) => {
+    const r = u8n(arrs.reduce((sum, a) => sum + abytes(a).length, 0)); // create u8a of summed length
+    let pad = 0; // walk through each array,
+    arrs.forEach(a => { r.set(a, pad); pad += a.length; }); // ensure they have proper type
+    return r;
+};
+/** WebCrypto OS-level CSPRNG (random number generator). Will throw when not available. */
+const randomBytes = (len = L) => {
+    const c = cr();
+    return c.getRandomValues(u8n(len));
+};
+const big = BigInt;
+const arange = (n, min, max, msg = 'bad number: out of range') => isBig(n) && min <= n && n < max ? n : err(msg);
+/** modular division */
+const M = (a, b = P) => {
+    const r = a % b;
+    return r >= 0n ? r : b + r;
+};
+const modN = (a) => M(a, N);
+/** Modular inversion using eucledian GCD (non-CT). No negative exponent for now. */
+// prettier-ignore
+const invert = (num, md) => {
+    if (num === 0n || md <= 0n)
+        err('no inverse n=' + num + ' mod=' + md);
+    let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
+    while (a !== 0n) {
+        const q = b / a, r = b % a;
+        const m = x - u * q, n = y - v * q;
+        b = a, a = r, x = u, y = v, u = m, v = n;
+    }
+    return b === 1n ? M(x, md) : err('no inverse'); // b is gcd at this point
+};
+const callHash = (name) => {
+    // @ts-ignore
+    const fn = etc[name];
+    if (typeof fn !== 'function')
+        err('hashes.' + name + ' not set');
+    return fn;
+};
+const apoint = (p) => (p instanceof Point ? p : err('Point expected'));
+// ## End of Helpers
+// -----------------
+/** secp256k1 formula. Koblitz curves are subclass of weierstrass curves with a=0, making it x³+b */
+const koblitz = (x) => M(M(x * x) * x + _b);
+/** assert is field element or 0 */
+const afield0 = (n) => arange(n, 0n, P);
+/** assert is field element */
+const afield = (n) => arange(n, 1n, P);
+/** assert is group elem */
+const agroup = (n) => arange(n, 1n, N);
+const isEven = (y) => (y & 1n) === 0n;
+/** create Uint8Array of byte n */
+const u8of = (n) => Uint8Array.of(n);
+const getPrefix = (y) => u8of(isEven(y) ? 0x02 : 0x03);
+/** lift_x from BIP340 calculates square root. Validates x, then validates root*root. */
+const lift_x = (x) => {
+    // Let c = x³ + 7 mod p. Fail if x ≥ p. (also fail if x < 1)
+    const c = koblitz(afield(x));
+    // c = √y
+    // y = c^((p+1)/4) mod p
+    // This formula works for fields p = 3 mod 4 -- a special, fast case.
+    // Paper: "Square Roots from 1;24,51,10 to Dan Shanks".
+    let r = 1n;
+    for (let num = c, e = (P + 1n) / 4n; e > 0n; e >>= 1n) {
+        // powMod: modular exponentiation.
+        if (e & 1n)
+            r = (r * num) % P; // Uses exponentiation by squaring.
+        num = (num * num) % P; // Not constant-time.
+    }
+    return M(r * r) === c ? r : err('sqrt invalid'); // check if result is valid
+};
+/** Point in 3d xyz projective coordinates. 3d takes less inversions than 2d. */
+class Point {
+    static BASE;
+    static ZERO;
+    px;
+    py;
+    pz;
+    constructor(px, py, pz) {
+        this.px = afield0(px);
+        this.py = afield(py); // y can't be 0 in Projective
+        this.pz = afield0(pz);
+        Object.freeze(this);
+    }
+    /** Convert Uint8Array or hex string to Point. */
+    static fromBytes(bytes) {
+        abytes(bytes);
+        let p = undefined;
+        // First byte is prefix, rest is data. There are 2 kinds: compressed & uncompressed:
+        // * [0x02 or 0x03][32-byte x coordinate]
+        // * [0x04]        [32-byte x coordinate][32-byte y coordinate]
+        const head = bytes[0];
+        const tail = bytes.subarray(1);
+        const x = sliceBytesNumBE(tail, 0, L);
+        const len = bytes.length;
+        // Compressed 33-byte point, 0x02 or 0x03 prefix
+        if (len === L + 1 && [0x02, 0x03].includes(head)) {
+            // Equation is y² == x³ + ax + b. We calculate y from x.
+            // y = √y²; there are two solutions: y, -y. Determine proper solution based on prefix
+            let y = lift_x(x);
+            const evenY = isEven(y);
+            const evenH = isEven(big(head));
+            if (evenH !== evenY)
+                y = M(-y);
+            p = new Point(x, y, 1n);
+        }
+        // Uncompressed 65-byte point, 0x04 prefix
+        if (len === L2 + 1 && head === 0x04)
+            p = new Point(x, sliceBytesNumBE(tail, L, L2), 1n);
+        // Validate point
+        return p ? p.assertValidity() : err('bad point: not on curve');
+    }
+    /** Equality check: compare points P&Q. */
+    equals(other) {
+        const { px: X1, py: Y1, pz: Z1 } = this;
+        const { px: X2, py: Y2, pz: Z2 } = apoint(other); // checks class equality
+        const X1Z2 = M(X1 * Z2);
+        const X2Z1 = M(X2 * Z1);
+        const Y1Z2 = M(Y1 * Z2);
+        const Y2Z1 = M(Y2 * Z1);
+        return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
+    }
+    is0() {
+        return this.equals(I);
+    }
+    /** Flip point over y coordinate. */
+    negate() {
+        return new Point(this.px, M(-this.py), this.pz);
+    }
+    /** Point doubling: P+P, complete formula. */
+    double() {
+        return this.add(this);
+    }
+    /**
+     * Point addition: P+Q, complete, exception-free formula
+     * (Renes-Costello-Batina, algo 1 of [2015/1060](https://eprint.iacr.org/2015/1060)).
+     * Cost: `12M + 0S + 3*a + 3*b3 + 23add`.
+     */
+    // prettier-ignore
+    add(other) {
+        const { px: X1, py: Y1, pz: Z1 } = this;
+        const { px: X2, py: Y2, pz: Z2 } = apoint(other);
+        const a = 0n;
+        const b = _b;
+        let X3 = 0n, Y3 = 0n, Z3 = 0n;
+        const b3 = M(b * 3n);
+        let t0 = M(X1 * X2), t1 = M(Y1 * Y2), t2 = M(Z1 * Z2), t3 = M(X1 + Y1); // step 1
+        let t4 = M(X2 + Y2); // step 5
+        t3 = M(t3 * t4);
+        t4 = M(t0 + t1);
+        t3 = M(t3 - t4);
+        t4 = M(X1 + Z1);
+        let t5 = M(X2 + Z2); // step 10
+        t4 = M(t4 * t5);
+        t5 = M(t0 + t2);
+        t4 = M(t4 - t5);
+        t5 = M(Y1 + Z1);
+        X3 = M(Y2 + Z2); // step 15
+        t5 = M(t5 * X3);
+        X3 = M(t1 + t2);
+        t5 = M(t5 - X3);
+        Z3 = M(a * t4);
+        X3 = M(b3 * t2); // step 20
+        Z3 = M(X3 + Z3);
+        X3 = M(t1 - Z3);
+        Z3 = M(t1 + Z3);
+        Y3 = M(X3 * Z3);
+        t1 = M(t0 + t0); // step 25
+        t1 = M(t1 + t0);
+        t2 = M(a * t2);
+        t4 = M(b3 * t4);
+        t1 = M(t1 + t2);
+        t2 = M(t0 - t2); // step 30
+        t2 = M(a * t2);
+        t4 = M(t4 + t2);
+        t0 = M(t1 * t4);
+        Y3 = M(Y3 + t0);
+        t0 = M(t5 * t4); // step 35
+        X3 = M(t3 * X3);
+        X3 = M(X3 - t0);
+        t0 = M(t3 * t1);
+        Z3 = M(t5 * Z3);
+        Z3 = M(Z3 + t0); // step 40
+        return new Point(X3, Y3, Z3);
+    }
+    /**
+     * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
+     * Uses {@link wNAF} for base point.
+     * Uses fake point to mitigate side-channel leakage.
+     * @param n scalar by which point is multiplied
+     * @param safe safe mode guards against timing attacks; unsafe mode is faster
+     */
+    multiply(n, safe = true) {
+        if (!safe && n === 0n)
+            return I;
+        agroup(n);
+        if (n === 1n)
+            return this;
+        if (this.equals(G))
+            return wNAF(n).p;
+        // init result point & fake point
+        let p = I;
+        let f = G;
+        for (let d = this; n > 0n; d = d.double(), n >>= 1n) {
+            // if bit is present, add to point
+            // if not present, add to fake, for timing safety
+            if (n & 1n)
+                p = p.add(d);
+            else if (safe)
+                f = f.add(d);
+        }
+        return p;
+    }
+    /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
+    toAffine() {
+        const { px: x, py: y, pz: z } = this;
+        // fast-paths for ZERO point OR Z=1
+        if (this.equals(I))
+            return { x: 0n, y: 0n };
+        if (z === 1n)
+            return { x, y };
+        const iz = invert(z, P);
+        // (Z * Z^-1) must be 1, otherwise bad math
+        if (M(z * iz) !== 1n)
+            err('inverse invalid');
+        // x = X*Z^-1; y = Y*Z^-1
+        return { x: M(x * iz), y: M(y * iz) };
+    }
+    /** Checks if the point is valid and on-curve. */
+    assertValidity() {
+        const { x, y } = this.toAffine(); // convert to 2d xy affine point.
+        afield(x); // must be in range 1 <= x,y < P
+        afield(y);
+        // y² == x³ + ax + b, equation sides must be equal
+        return M(y * y) === koblitz(x) ? this : err('bad point: not on curve');
+    }
+    /** Converts point to 33/65-byte Uint8Array. */
+    toBytes(isCompressed = true) {
+        const { x, y } = this.assertValidity().toAffine();
+        const x32b = numTo32b(x);
+        if (isCompressed)
+            return concatBytes(getPrefix(y), x32b);
+        return concatBytes(u8of(0x04), x32b, numTo32b(y));
+    }
+    /** Create 3d xyz point from 2d xy. (0, 0) => (0, 1, 0), not (0, 0, 1) */
+    static fromAffine(ap) {
+        const { x, y } = ap;
+        return x === 0n && y === 0n ? I : new Point(x, y, 1n);
+    }
+    toHex(isCompressed) {
+        return bytesToHex(this.toBytes(isCompressed));
+    }
+    static fromPrivateKey(k) {
+        return G.multiply(toPrivScalar(k));
+    }
+    static fromHex(hex) {
+        return Point.fromBytes(toU8(hex));
+    }
+    get x() {
+        return this.toAffine().x;
+    }
+    get y() {
+        return this.toAffine().y;
+    }
+    toRawBytes(isCompressed) {
+        return this.toBytes(isCompressed);
+    }
+}
+/** Generator / base point */
+const G = new Point(Gx, Gy, 1n);
+/** Identity / zero point */
+const I = new Point(0n, 1n, 0n);
+// Static aliases
+Point.BASE = G;
+Point.ZERO = I;
+/** `Q = u1⋅G + u2⋅R`. Verifies Q is not ZERO. Unsafe: non-CT. */
+const doubleScalarMulUns = (R, u1, u2) => {
+    return G.multiply(u1, false).add(R.multiply(u2, false)).assertValidity();
+};
+const bytesToNumBE = (b) => big('0x' + (bytesToHex(b) || '0'));
+const sliceBytesNumBE = (b, from, to) => bytesToNumBE(b.subarray(from, to));
+const B256 = 2n ** 256n; // secp256k1 is weierstrass curve. Equation is x³ + ax + b.
+/** Number to 32b. Must be 0 <= num < B256. validate, pad, to bytes. */
+const numTo32b = (num) => hexToBytes(padh(arange(num, 0n, B256), L2));
+/** Normalize private key to scalar (bigint). Verifies scalar is in range 1 {
+    const num = isBig(pr) ? pr : bytesToNumBE(toU8(pr, L));
+    return arange(num, 1n, N, 'private key invalid 3');
+};
+/** For Signature malleability, validates sig.s is bigger than N/2. */
+const highS = (n) => n > N >> 1n;
+/** Creates 33/65-byte public key from 32-byte private key. */
+const getPublicKey = (privKey, isCompressed = true) => {
+    return G.multiply(toPrivScalar(privKey)).toBytes(isCompressed);
+};
+/** ECDSA Signature class. Supports only compact 64-byte representation, not DER. */
+class Signature {
+    r;
+    s;
+    recovery;
+    constructor(r, s, recovery) {
+        this.r = agroup(r); // 1 <= r < N
+        this.s = agroup(s); // 1 <= s < N
+        if (recovery != null)
+            this.recovery = recovery;
+        Object.freeze(this);
+    }
+    /** Create signature from 64b compact (r || s) representation. */
+    static fromBytes(b) {
+        abytes(b, L2);
+        const r = sliceBytesNumBE(b, 0, L);
+        const s = sliceBytesNumBE(b, L, L2);
+        return new Signature(r, s);
+    }
+    toBytes() {
+        const { r, s } = this;
+        return concatBytes(numTo32b(r), numTo32b(s));
+    }
+    /** Copy signature, with newly added recovery bit. */
+    addRecoveryBit(bit) {
+        return new Signature(this.r, this.s, bit);
+    }
+    hasHighS() {
+        return highS(this.s);
+    }
+    toCompactRawBytes() {
+        return this.toBytes();
+    }
+    toCompactHex() {
+        return bytesToHex(this.toBytes());
+    }
+    recoverPublicKey(msg) {
+        return recoverPublicKey(this, msg);
+    }
+    static fromCompact(hex) {
+        return Signature.fromBytes(toU8(hex, L2));
+    }
+    assertValidity() {
+        return this;
+    }
+    normalizeS() {
+        const { r, s, recovery } = this;
+        return highS(s) ? new Signature(r, modN(-s), recovery) : this;
+    }
+}
+/**
+ * RFC6979: ensure ECDSA msg is X bytes, convert to BigInt.
+ * RFC suggests optional truncating via bits2octets.
+ * FIPS 186-4 4.6 suggests the leftmost min(nBitLen, outLen) bits,
+ * which matches bits2int. bits2int can produce res>N.
+ */
+const bits2int = (bytes) => {
+    const delta = bytes.length * 8 - 256;
+    if (delta > 1024)
+        err('msg invalid'); // our CUSTOM check, "just-in-case": prohibit long inputs
+    const num = bytesToNumBE(bytes);
+    return delta > 0 ? num >> big(delta) : num;
+};
+/** int2octets can't be used; pads small msgs with 0: BAD for truncation as per RFC vectors */
+const bits2int_modN = (bytes) => modN(bits2int(abytes(bytes)));
+const signOpts = { lowS: true };
+const veriOpts = { lowS: true };
+// RFC6979 signature generation, preparation step.
+const prepSig = (msgh, priv, opts = signOpts) => {
+    if (['der', 'recovered', 'canonical'].some((k) => k in opts))
+        // legacy opts
+        err('option not supported');
+    let { lowS, extraEntropy } = opts; // generates low-s sigs by default
+    if (lowS == null)
+        lowS = true; // RFC6979 3.2: we skip step A
+    const i2o = numTo32b; // int to octets
+    const h1i = bits2int_modN(toU8(msgh)); // msg bigint
+    const h1o = i2o(h1i); // msg octets
+    const d = toPrivScalar(priv); // validate private key, convert to bigint
+    const seed = [i2o(d), h1o]; // Step D of RFC6979 3.2
+    /** RFC6979 3.6: additional k' (optional). See {@link ExtraEntropy}. */
+    // K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1) || k')
+    if (extraEntropy)
+        seed.push(extraEntropy === true ? randomBytes(L) : toU8(extraEntropy));
+    const m = h1i; // convert msg to bigint
+    // Converts signature params into point w r/s, checks result for validity.
+    // To transform k => Signature:
+    // q = k⋅G
+    // r = q.x mod n
+    // s = k^-1(m + rd) mod n
+    const k2sig = (kBytes) => {
+        // RFC 6979 Section 3.2, step 3: k = bits2int(T)
+        // Important: all mod() calls here must be done over N
+        const k = bits2int(kBytes);
+        if (!(1n <= k && k < N))
+            return; // Check 0 < k < CURVE.n
+        const q = G.multiply(k).toAffine(); // q = k⋅G
+        const r = modN(q.x); // r = q.x mod n
+        if (r === 0n)
+            return;
+        const ik = invert(k, N); // k^-1 mod n, NOT mod P
+        const s = modN(ik * modN(m + modN(d * r))); // s = k^-1(m + rd) mod n
+        if (s === 0n)
+            return;
+        let normS = s; // normalized S
+        let recovery = (q.x === r ? 0 : 2) | Number(q.y & 1n); // recovery bit (2 or 3, when q.x > n)
+        if (lowS && highS(s)) {
+            // if lowS was passed, ensure s is always
+            normS = modN(-s); // in the bottom half of CURVE.n
+            recovery ^= 1;
+        }
+        return new Signature(r, normS, recovery); // use normS, not s
+    };
+    return { seed: concatBytes(...seed), k2sig };
+};
+// HMAC-DRBG from NIST 800-90. Minimal, non-full-spec - used for RFC6979 signatures.
+const hmacDrbg = (asynchronous) => {
+    let v = u8n(L); // Steps B, C of RFC6979 3.2: set hashLen
+    let k = u8n(L); // In our case, it's always equal to L
+    let i = 0; // Iterations counter, will throw when over max
+    const NULL = u8n(0);
+    const reset = () => {
+        v.fill(1);
+        k.fill(0);
+        i = 0;
+    };
+    const max = 1000;
+    const _e = 'drbg: tried 1000 values';
+    if (asynchronous) {
+        // asynchronous=true
+        // h = hmac(K || V || ...)
+        const h = (...b) => etc.hmacSha256Async(k, v, ...b);
+        const reseed = async (seed = NULL) => {
+            // HMAC-DRBG reseed() function. Steps D-G
+            k = await h(u8of(0x00), seed); // k = hmac(K || V || 0x00 || seed)
+            v = await h(); // v = hmac(K || V)
+            if (seed.length === 0)
+                return;
+            k = await h(u8of(0x01), seed); // k = hmac(K || V || 0x01 || seed)
+            v = await h(); // v = hmac(K || V)
+        };
+        // HMAC-DRBG generate() function
+        const gen = async () => {
+            if (i++ >= max)
+                err(_e);
+            v = await h(); // v = hmac(K || V)
+            return v; // this diverges from noble-curves: we don't allow arbitrary output len!
+        };
+        // Do not reuse returned fn for more than 1 sig:
+        // 1) it's slower (JIT screws up). 2. unsafe (async race conditions)
+        return async (seed, pred) => {
+            reset();
+            await reseed(seed); // Steps D-G
+            let res = undefined; // Step H: grind until k is in [1..n-1]
+            while (!(res = pred(await gen())))
+                await reseed(); // test predicate until it returns ok
+            reset();
+            return res;
+        };
+    }
+    else {
+        // asynchronous=false; same as above, but synchronous
+        // h = hmac(K || V || ...)
+        const h = (...b) => callHash('hmacSha256Sync')(k, v, ...b);
+        const reseed = (seed = NULL) => {
+            // HMAC-DRBG reseed() function. Steps D-G
+            k = h(u8of(0x00), seed); // k = hmac(k || v || 0x00 || seed)
+            v = h(); // v = hmac(k || v)
+            if (seed.length === 0)
+                return;
+            k = h(u8of(0x01), seed); // k = hmac(k || v || 0x01 || seed)
+            v = h(); // v = hmac(k || v)
+        };
+        // HMAC-DRBG generate() function
+        const gen = () => {
+            if (i++ >= max)
+                err(_e);
+            v = h(); // v = hmac(k || v)
+            return v; // this diverges from noble-curves: we don't allow arbitrary output len!
+        };
+        // Do not reuse returned fn for more than 1 sig:
+        // 1) it's slower (JIT screws up). 2. unsafe (async race conditions)
+        return (seed, pred) => {
+            reset();
+            reseed(seed); // Steps D-G
+            let res = undefined; // Step H: grind until k is in [1..n-1]
+            while (!(res = pred(gen())))
+                reseed(); // test predicate until it returns ok
+            reset();
+            return res;
+        };
+    }
+};
+/**
+ * Sign a msg hash using secp256k1. Async.
+ * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979.
+ * It's suggested to enable hedging ({@link ExtraEntropy}) to prevent fault attacks.
+ * @param msgh - message HASH, not message itself e.g. sha256(message)
+ * @param priv - private key
+ * @param opts - `lowS: true` prevents malleability, `extraEntropy: true` enables hedging
+ */
+const signAsync = async (msgh, priv, opts = signOpts) => {
+    // Re-run drbg until k2sig returns ok
+    const { seed, k2sig } = prepSig(msgh, priv, opts);
+    const sig = await hmacDrbg(true)(seed, k2sig);
+    return sig;
+};
+/**
+ * Sign a msg hash using secp256k1.
+ * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.2 & RFC6979.
+ * It's suggested to enable hedging ({@link ExtraEntropy}) to prevent fault attacks.
+ * @param msgh - message HASH, not message itself e.g. sha256(message)
+ * @param priv - private key
+ * @param opts - `lowS: true` prevents malleability, `extraEntropy: true` enables hedging
+ * @example
+ * const sig = sign(sha256('hello'), privKey, { extraEntropy: true }).toBytes();
+ */
+const sign = (msgh, priv, opts = signOpts) => {
+    // Re-run drbg until k2sig returns ok
+    const { seed, k2sig } = prepSig(msgh, priv, opts);
+    const sig = hmacDrbg(false)(seed, k2sig);
+    return sig;
+};
+/**
+ * Verify a signature using secp256k1.
+ * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.4.
+ * Default lowS=true, prevents malleability.
+ * @param sig - signature, 64-byte or Signature instance
+ * @param msgh - message HASH, not message itself e.g. sha256(message)
+ * @param pub - public key
+ * @param opts - { lowS: true } is default, prohibits s >= CURVE.n/2 to prevent malleability
+ */
+const verify = (sig, msgh, pub, opts = veriOpts) => {
+    let { lowS } = opts;
+    if (lowS == null)
+        lowS = true;
+    if ('strict' in opts)
+        err('option not supported');
+    let sigg;
+    // Previous ver supported DER sigs.
+    // We throw error when DER is suspected now.
+    const rs = sig && typeof sig === 'object' && 'r' in sig;
+    if (!rs && toU8(sig).length !== L2)
+        err('signature must be 64 bytes');
+    try {
+        sigg = rs ? new Signature(sig.r, sig.s) : Signature.fromCompact(sig);
+        const h = bits2int_modN(toU8(msgh)); // Truncate hash
+        const P = Point.fromBytes(toU8(pub)); // Validate public key
+        const { r, s } = sigg;
+        if (lowS && highS(s))
+            return false; // lowS bans sig.s >= CURVE.n/2
+        const is = invert(s, N); // s^-1
+        const u1 = modN(h * is); // u1 = hs^-1 mod n
+        const u2 = modN(r * is); // u2 = rs^-1 mod n
+        const R = doubleScalarMulUns(P, u1, u2).toAffine(); // R = u1⋅G + u2⋅P
+        // Stop if R is identity / zero point. Check is done inside `doubleScalarMulUns`
+        const v = modN(R.x); // R.x must be in N's field, not P's
+        return v === r; // mod(R.x, n) == r
+    }
+    catch (error) {
+        return false;
+    }
+};
+/**
+ * ECDSA public key recovery. Requires msg hash and recovery id.
+ * Follows [SEC1](https://secg.org/sec1-v2.pdf) 4.1.6.
+ */
+const recoverPublicKey = (sig, msgh) => {
+    const { r, s, recovery } = sig;
+    // 0 or 1 recovery id determines sign of "y" coordinate.
+    // 2 or 3 means q.x was >N.
+    if (![0, 1, 2, 3].includes(recovery))
+        err('recovery id invalid');
+    const h = bits2int_modN(toU8(msgh, L)); // Truncate hash
+    const radj = recovery === 2 || recovery === 3 ? r + N : r;
+    afield(radj); // ensure q.x is still a field element
+    const head = getPrefix(big(recovery)); // head is 0x02 or 0x03
+    const Rb = concatBytes(head, numTo32b(radj)); // concat head + r
+    const R = Point.fromBytes(Rb);
+    const ir = invert(radj, N); // r^-1
+    const u1 = modN(-h * ir); // -hr^-1
+    const u2 = modN(s * ir); // sr^-1
+    return doubleScalarMulUns(R, u1, u2); // (sr^-1)R-(hr^-1)G = -(hr^-1)G + (sr^-1)
+};
+/**
+ * Elliptic Curve Diffie-Hellman (ECDH) on secp256k1.
+ * Result is **NOT hashed**. Use hash or KDF on it if you need.
+ * @param privA private key A
+ * @param pubB public key B
+ * @param isCompressed 33-byte (true) or 65-byte (false) output
+ * @returns public key C
+ */
+const getSharedSecret = (privA, pubB, isCompressed = true) => {
+    return Point.fromBytes(toU8(pubB)).multiply(toPrivScalar(privA)).toBytes(isCompressed);
+};
+// FIPS 186 B.4.1 compliant key generation produces private keys with modulo bias being neglible.
+// takes >N+8 bytes, returns (hash mod n-1)+1
+const hashToPrivateKey = (hash) => {
+    hash = toU8(hash);
+    if (hash.length < L + 8 || hash.length > 1024)
+        err('expected 40-1024b');
+    const num = M(bytesToNumBE(hash), N - 1n);
+    return numTo32b(num + 1n);
+};
+const randomPrivateKey = () => hashToPrivateKey(randomBytes(L + 16)); // FIPS 186 B.4.1.
+const _sha = 'SHA-256';
+/** Math, hex, byte helpers. Not in `utils` because utils share API with noble-curves. */
+const etc = {
+    hexToBytes: hexToBytes,
+    bytesToHex: bytesToHex,
+    concatBytes: concatBytes,
+    bytesToNumberBE: bytesToNumBE,
+    numberToBytesBE: numTo32b,
+    mod: M,
+    invert: invert, // math utilities
+    hmacSha256Async: async (key, ...msgs) => {
+        const s = subtle();
+        const name = 'HMAC';
+        const k = await s.importKey('raw', key, { name, hash: { name: _sha } }, false, ['sign']);
+        return u8n(await s.sign(name, k, concatBytes(...msgs)));
+    },
+    hmacSha256Sync: undefined, // For TypeScript. Actual logic is below
+    hashToPrivateKey: hashToPrivateKey,
+    randomBytes: randomBytes,
+};
+/** Curve-specific utilities for private keys. */
+const utils = {
+    normPrivateKeyToScalar: toPrivScalar,
+    isValidPrivateKey: (key) => {
+        try {
+            return !!toPrivScalar(key);
+        }
+        catch (e) {
+            return false;
+        }
+    },
+    randomPrivateKey: randomPrivateKey,
+    precompute: (w = 8, p = G) => {
+        p.multiply(3n);
+        w;
+        return p;
+    },
+};
+// ## Precomputes
+// --------------
+const W = 8; // W is window size
+const scalarBits = 256;
+const pwindows = Math.ceil(scalarBits / W) + 1; // 33 for W=8
+const pwindowSize = 2 ** (W - 1); // 128 for W=8
+const precompute = () => {
+    const points = [];
+    let p = G;
+    let b = p;
+    for (let w = 0; w < pwindows; w++) {
+        b = p;
+        points.push(b);
+        for (let i = 1; i < pwindowSize; i++) {
+            b = b.add(p);
+            points.push(b);
+        } // i=1, bc we skip 0
+        p = b.double();
+    }
+    return points;
+};
+let Gpows = undefined; // precomputes for base point G
+// const-time negate
+const ctneg = (cnd, p) => {
+    const n = p.negate();
+    return cnd ? n : p;
+};
+/**
+ * Precomputes give 12x faster getPublicKey(), 10x sign(), 2x verify() by
+ * caching multiples of G (base point). Cache is stored in 32MB of RAM.
+ * Any time `G.multiply` is done, precomputes are used.
+ * Not used for getSharedSecret, which instead multiplies random pubkey `P.multiply`.
+ *
+ * w-ary non-adjacent form (wNAF) precomputation method is 10% slower than windowed method,
+ * but takes 2x less RAM. RAM reduction is possible by utilizing `.subtract`.
+ *
+ * !! Precomputes can be disabled by commenting-out call of the wNAF() inside Point#multiply().
+ */
+const wNAF = (n) => {
+    const comp = Gpows || (Gpows = precompute());
+    let p = I;
+    let f = G; // f must be G, or could become I in the end
+    const pow_2_w = 2 ** W; // 256 for W=8
+    const maxNum = pow_2_w; // 256 for W=8
+    const mask = big(pow_2_w - 1); // 255 for W=8 == mask 0b11111111
+    const shiftBy = big(W); // 8 for W=8
+    for (let w = 0; w < pwindows; w++) {
+        let wbits = Number(n & mask); // extract W bits.
+        n >>= shiftBy; // shift number by W bits.
+        if (wbits > pwindowSize) {
+            wbits -= maxNum;
+            n += 1n;
+        } // split if bits > max: +224 => 256-32
+        const off = w * pwindowSize;
+        const offF = off; // offsets, evaluate both
+        const offP = off + Math.abs(wbits) - 1;
+        const isEven = w % 2 !== 0; // conditions, evaluate both
+        const isNeg = wbits < 0;
+        if (wbits === 0) {
+            // off == I: can't add it. Adding random offF instead.
+            f = f.add(ctneg(isEven, comp[offF])); // bits are 0: add garbage to fake point
+        }
+        else {
+            p = p.add(ctneg(isNeg, comp[offP])); // bits are 1: add to result point
+        }
+    }
+    return { p, f }; // return both real and fake points for JIT
+};
+// !! Remove the export below to easily use in REPL / browser console
+export { secp256k1_CURVE as CURVE, etc, getPublicKey, getSharedSecret, Point, Point as ProjectivePoint, sign, signAsync, Signature, utils, verify, };

From d5aba4d53f0dc8928436100bf581d77333988d32 Mon Sep 17 00:00:00 2001
From: Peter Foldiak 
Date: Sun, 8 Nov 2026 22:19:42 +0400
Subject: [PATCH 5/5] JS: proofs, identical to Python's

store.prove(key), provePrefix(prefix), extends(base, prefixes) and
proveExtension(base, prefixes), with verifyProof, verifyPrefix and
verifyExtension: a key's record or its absence, every record under a
prefix, a later root keeping an earlier one's records. Same formats as
Python; every envelope checked before it is handed out. ProofError moves
to proofs.js, shared with the feed updates.

The fixtures require JavaScript's envelopes to equal Python's for the
same store (key, prefix and extension proofs under both addressings),
the same verdicts from Python's envelopes, the same refused extensions
(naming the same key), and every tampered proof refused.

Live: proofs about OntoDAG's space pack made through Bee (abbey present,
4 nodes; abbeyz absent; the 11 records under 'ab'), checked offline in
JS and accepted by Python's verifiers.

Co-Authored-By: Claude Opus 5.5 
---
 js/README.md                 |   48 +-
 js/src/feeds.js              |    9 +-
 js/src/index.js              |    5 +-
 js/src/proofs.js             |  315 ++++
 js/src/recordstore.js        |  124 +-
 js/src/trie.js               |    2 +-
 js/test/fixtures/proofs.json | 2855 ++++++++++++++++++++++++++++++++++
 js/test/live.test.js         |   20 +-
 js/test/proofs.test.js       |   90 ++
 js/tools/make_fixtures.py    |  109 ++
 10 files changed, 3559 insertions(+), 18 deletions(-)
 create mode 100644 js/src/proofs.js
 create mode 100644 js/test/fixtures/proofs.json
 create mode 100644 js/test/proofs.test.js

diff --git a/js/README.md b/js/README.md
index a277b24..d1c0c77 100644
--- a/js/README.md
+++ b/js/README.md
@@ -10,9 +10,10 @@ in both: byte for byte, under SHA-256 or Swarm addressing.
 against the Python implementation: records, staging, commits, sorted
 iteration, diff, three-way merge, and reconciling a moved pointer. Stores for
 memory, a Bee node, and Freedom Browser's `window.swarm`; the store's latest
-root in a Swarm feed (`SwarmFeedPointer`), with feed updates verifiable
-offline. Not yet ported: the other proofs, history and undo, local-first
-storage.
+root in a Swarm feed (`SwarmFeedPointer`); and proofs anyone can check with
+no store: a key's record or its absence, everything under a prefix, a later
+root extending an earlier one, a feed's signed updates. Not yet ported:
+history and undo, local-first storage.
 
 Plain JavaScript modules with JSDoc types and no npm dependencies, so a page
 can import it with relative URLs only, including a page published on Swarm.
@@ -105,6 +106,38 @@ to its root with its 8 records (12 s); the OntoDAG page's feed read with its
 feed from JavaScript, then followed from JavaScript and from Python, which
 read the same root and records and verified both updates.
 
+## Proofs
+
+A root is a commitment to a whole store, so a store can prove statements
+about it that anyone holding only the root can check: no store, no network,
+no trust in the prover. An envelope carries the raw trie nodes (and values);
+the verifier recomputes their references and replays the walk. The trie is
+canonical, so a key has one possible place under a root, and absence is as
+provable as presence.
+
+```js
+const proof = await store.prove('users/alice');           // inclusion, or absence
+await verifyProof(proof, root);                           // the record, or ABSENT
+
+const all = await store.provePrefix('users/');           // every record under it, and no others
+await verifyPrefix(all, root);                            // a Map, proven complete
+
+const grew = await store.proveExtension(oldRoot, ['log/']);  // nothing under log/ removed or changed
+await verifyExtension(grew, oldRoot, root);                  // the prefixes proven
+```
+
+Each envelope is checked before it is handed out, and a verifier rejects
+with `ProofError`. The formats are Python's (`recordstore-trie-proof`,
+`-prefix-proof`, `-extension-proof`, version 1), and the same store gives
+the same envelope in both languages: the fixtures require JavaScript's
+proofs to equal Python's, field for field. Verifiers return promises,
+because Web Crypto's SHA-256 is asynchronous.
+
+Live on 8 November 2026: proofs about OntoDAG's `space` pack, made through
+Bee in 0.4 s, that `abbey` holds its record (4 nodes, 8.4 KB as JSON), that
+`abbeyz` is absent (8.2 KB), and of the 11 records under `ab` (13 KB), all
+three accepted by Python's verifiers.
+
 ### In Freedom Browser
 
 A page in Freedom cannot reach a node's HTTP API; it has `window.swarm`.
@@ -171,6 +204,10 @@ Shaped like the Python one, but asynchronous and JavaScript-idiomatic:
 | `await swarmStore(topic, {url, signer \| owner, batch})` | a store on Swarm behind its feed |
 | `new SwarmFeedPointer(urlOrTransport, topic, {signer \| owner, batch})` | the feed pointer: `get`, `set`, `compareAndSet`, `update(i)`, `updates()` |
 | `verifyFeedUpdate(envelope, owner, topic)` | `{index, root, timestamp}` of a signed update, checked offline; `ProofError` otherwise |
+| `await store.prove(key)` / `verifyProof(proof, root)` | a key's record or its absence, proven |
+| `await store.provePrefix(prefix)` / `verifyPrefix(proof, root)` | every record under a prefix, proven complete (a `Map`) |
+| `await store.extends(base, prefixes)` | does the root keep every record `base` held under the prefixes? |
+| `await store.proveExtension(base, prefixes)` / `verifyExtension(proof, base, root)` | that it does, proven |
 
 Errors: `RecordUnavailable` when a record exists but a blob on its path cannot
 be read right now (never reported as absence), `MergeConflict`,
@@ -221,7 +258,8 @@ node test/freedom/freedom_check.cjs --bee=http://127.0.0.1:1633 --write   # spen
 `test/fixtures/` is written by `tools/make_fixtures.py`, which runs the Python
 implementation in `../src` (not an installed copy) and records canonical
 encodings, Swarm addresses, the root after every commit of scripted and random
-stores under both addressings, three-way merges, and feeds: topics, owners,
+stores under both addressings, three-way merges, proofs (key, prefix and
+extension envelopes, their verdicts, and tampered ones Python refuses), and feeds: topics, owners,
 update addresses, signed updates written by swarmfs's own code (so JavaScript
 must reproduce the signatures byte for byte), and updates Python refuses. It
 needs swarmfs with signing (`pip install "recordstore[bee,feeds]"`). The JavaScript tests
@@ -231,8 +269,6 @@ fails while the fixtures are stale.
 
 ## Not yet ported
 
-- Proofs (`verify_proof`, `verify_prefix`, `verify_extension`); feed updates
-  are ported.
 - History, undo and redo (`FilePointer`'s timeline).
 - Local-first storage.
 
diff --git a/js/src/feeds.js b/js/src/feeds.js
index c53897b..65c9c29 100644
--- a/js/src/feeds.js
+++ b/js/src/feeds.js
@@ -20,6 +20,7 @@ import { getPublicKey, signAsync, Signature } from '../vendor/noble-secp256k1/in
 import { chunkAddress, encodeSpan, keccak256Bytes } from './address.js';
 import { concatBytes, fromBase64, fromHex, toHex, utf8 } from './bytes.js';
 import { VerificationError } from './stores.js';
+import { ProofError } from './proofs.js';
 
 export const FEED_UPDATE_FORMAT = 'recordstore-feed-update';
 
@@ -29,14 +30,6 @@ const SOC_SPAN_OFFSET = SOC_IDENTIFIER_SIZE + SOC_SIGNATURE_SIZE; // 97
 const SOC_PAYLOAD_OFFSET = SOC_SPAN_OFFSET + 8; // 105
 const ETH_PREFIX = utf8('\x19Ethereum Signed Message:\n32');
 
-/** A feed update envelope (or pair of them) that does not prove what it claims. */
-export class ProofError extends Error {
-  constructor(message) {
-    super(message);
-    this.name = 'ProofError';
-  }
-}
-
 /** A transport's answer for a chunk or lookup: `status` 404 means absent, 500 an incomplete retrieval. */
 export class FeedTransportError extends Error {
   constructor(message, status) {
diff --git a/js/src/index.js b/js/src/index.js
index 4f5fffe..9e707c6 100644
--- a/js/src/index.js
+++ b/js/src/index.js
@@ -15,7 +15,10 @@ export {
 } from './address.js';
 export { DEFAULT_NODE_CACHE_SIZE } from './trie.js';
 export {
-  SwarmFeedPointer, BeeFeedTransport, ProviderFeedTransport, FeedTransportError, ProofError,
+  SwarmFeedPointer, BeeFeedTransport, ProviderFeedTransport, FeedTransportError,
   verifyFeedUpdate, verifyEquivocation, FEED_UPDATE_FORMAT, topicOf, feedIdentifier, socAddress, addressOf,
 } from './feeds.js';
+export {
+  ProofError, verifyProof, verifyPrefix, verifyExtension, PROOF_FORMAT, PREFIX_FORMAT, EXTENSION_FORMAT,
+} from './proofs.js';
 export { swarmStore } from './swarm.js';
diff --git a/js/src/proofs.js b/js/src/proofs.js
new file mode 100644
index 0000000..41abf47
--- /dev/null
+++ b/js/src/proofs.js
@@ -0,0 +1,315 @@
+// Proofs about committed roots, verifiable with no store, ported from the
+// Python package:
+//
+// - a key proof (format "recordstore-trie-proof"): a key's record, or its
+//   absence. The trie is canonical, so a key has exactly one possible place
+//   under a root, which is what makes absence provable;
+// - a prefix proof ("recordstore-prefix-proof"): every record under a
+//   prefix, and no others;
+// - an extension proof ("recordstore-extension-proof"): a later root holds
+//   every record an earlier root held under some prefixes, unchanged.
+//
+// An envelope carries the raw node and value blobs; a verifier recomputes
+// their references (SHA-256 or Swarm, as the envelope names) and replays the
+// walk from the root it trusts. Equal stores give equal envelopes in Python
+// and JavaScript. Verifiers return promises (Web Crypto's SHA-256 is async).
+
+import { resolveAddressing } from './address.js';
+import { compareCodePoints, fromHex, hexUtf8, isWellFormed, utf8Hex } from './bytes.js';
+import { decodeValue } from './canonical.js';
+import { commonPrefix, Trie, TrieNode } from './trie.js';
+
+export const PROOF_FORMAT = 'recordstore-trie-proof';
+export const PREFIX_FORMAT = 'recordstore-prefix-proof';
+export const EXTENSION_FORMAT = 'recordstore-extension-proof';
+
+/** The same symbol as recordstore.js's ABSENT (Symbol.for). */
+const ABSENT = Symbol.for('recordstore.ABSENT');
+
+/** A proof (or feed update) that does not verify against the given root. */
+export class ProofError extends Error {
+  constructor(message) {
+    super(message);
+    this.name = 'ProofError';
+  }
+}
+
+function envelope(proof, format) {
+  if (!proof || typeof proof !== 'object' || proof.format !== format) throw new ProofError(`not a ${format} envelope`);
+  if (proof.version !== 1) throw new ProofError(`unsupported proof version ${JSON.stringify(proof.version)}`);
+}
+
+function addressing(name) {
+  if (name !== 'sha256' && name !== 'swarm') throw new ProofError(`unknown addressing ${JSON.stringify(name)}`);
+  return resolveAddressing(name);
+}
+
+function blob(hex, what) {
+  try {
+    return fromHex(hex);
+  } catch {
+    throw new ProofError(`malformed ${what} bytes in proof`);
+  }
+}
+
+function blobs(list, what) {
+  if (list === undefined || list === null) return [];
+  if (!Array.isArray(list)) throw new ProofError(`malformed ${what} bytes in proof`);
+  return list.map((h) => blob(h, what));
+}
+
+function decodeNode(bytes, label) {
+  try {
+    return Trie.decode(bytes);
+  } catch {
+    throw new ProofError(`${label} is not a valid trie node`);
+  }
+}
+
+function keyText(hex) {
+  try {
+    return hexUtf8(hex);
+  } catch {
+    return hex;
+  }
+}
+
+function prefixHex(prefix) {
+  if (!isWellFormed(prefix)) throw new TypeError('prefix contains a lone surrogate');
+  return utf8Hex(prefix);
+}
+
+/**
+ * The addressing name a proof envelope carries: the bytes store's, which
+ * must be one any verifier can recompute ('sha256' or 'swarm').
+ */
+export function addressingName(store) {
+  if (store && (store.addressing === 'sha256' || store.addressing === 'swarm')) return store.addressing;
+  throw new TypeError("cannot determine this bytes store's addressing scheme; pass {addressing: 'sha256' | 'swarm'}");
+}
+
+// -- key proofs ---------------------------------------------------------------
+
+/**
+ * Check a key proof against `root` (the reference the verifier trusts; null
+ * for an empty store). Resolves to the proven record, or ABSENT for an
+ * absence proof; rejects with ProofError on any mismatch.
+ */
+export async function verifyProof(proof, root = null) {
+  envelope(proof, PROOF_FORMAT);
+  if ((proof.root ?? null) !== root) throw new ProofError(`proof is about root ${JSON.stringify(proof.root)}, not ${JSON.stringify(root)}`);
+  const refOf = addressing(proof.addressing);
+  const { key } = proof;
+  if (typeof key !== 'string' || key === '' || !isWellFormed(key)) throw new ProofError('proof carries no key');
+  const nodes = blobs(proof.nodes, 'node');
+
+  let expected = root;
+  let remaining = utf8Hex(key);
+  let verdict = null; // [found, value ref] once the walk concludes
+  for (let i = 0; i < nodes.length; i++) {
+    if (verdict) throw new ProofError(`node ${i} continues past the walk's conclusion`);
+    if ((await refOf(nodes[i])) !== expected) throw new ProofError(`node ${i} does not hash to the expected reference`);
+    const node = decodeNode(nodes[i], `node ${i}`);
+    if (!remaining.startsWith(node.prefix)) {
+      verdict = [false, null]; // diverges inside the prefix
+    } else {
+      remaining = remaining.slice(node.prefix.length);
+      if (remaining === '') {
+        verdict = [node.valueRef !== null, node.valueRef];
+      } else {
+        const child = node.children.get(remaining.slice(0, 2));
+        if (child === undefined) {
+          verdict = [false, null]; // nowhere to descend
+        } else {
+          expected = child;
+          remaining = remaining.slice(2);
+        }
+      }
+    }
+  }
+  if (!verdict) {
+    if (root === null) verdict = [false, null]; // the empty store holds nothing
+    else throw new ProofError('proof ends before the walk does');
+  }
+
+  const [found, valueRef] = verdict;
+  if (found !== Boolean(proof.present)) throw new ProofError("the proof's claim contradicts its own path");
+  if (!found) {
+    if (proof.value !== null && proof.value !== undefined) throw new ProofError('absence proof carries a value');
+    return ABSENT;
+  }
+  const value = blob(proof.value, 'value');
+  if ((await refOf(value)) !== valueRef) throw new ProofError("value bytes do not hash to the trie's value reference");
+  return decodeValue(value);
+}
+
+// -- walks shared by prefix and extension proofs ------------------------------
+
+/** Can a subtree whose keys start with `keyPrefix` hold a key starting with `prefix`? */
+function meets(keyPrefix, prefix) {
+  return keyPrefix.startsWith(prefix) || prefix.startsWith(keyPrefix);
+}
+
+/**
+ * [key, value ref, null] for every value under `prefix` in the subtree at
+ * `node`, in key order; children that cannot hold the prefix are not loaded.
+ */
+export async function* itemsUnder(load, node, acc, prefix) {
+  const stack = [[node, acc]];
+  while (stack.length) {
+    const [n, a] = stack.pop();
+    const full = a + n.prefix;
+    if (!meets(full, prefix)) continue;
+    if (n.valueRef !== null && full.startsWith(prefix)) yield [full, n.valueRef, null];
+    for (const byte of [...n.children.keys()].sort().reverse()) {
+      const keyPrefix = full + byte;
+      if (meets(keyPrefix, prefix)) stack.push([await load(n.children.get(byte)), keyPrefix]);
+    }
+  }
+}
+
+/**
+ * [key, a value ref, b value ref | null] for every record of `a`'s subtree
+ * under `prefix` that `b`'s subtree does not hold with the same value: the
+ * three shapes two canonical tries can differ in, one way only.
+ */
+async function* extensionFaults(load, a, b, acc, prefix) {
+  if (a === null) return;
+  if (!meets(acc + a.prefix, prefix)) return;
+  if (b === null) {
+    yield* itemsUnder(load, a, acc, prefix);
+    return;
+  }
+  const pa = a.prefix;
+  const pb = b.prefix;
+  if (pa === pb) {
+    const ka = acc + pa;
+    if (a.valueRef !== null && ka.startsWith(prefix) && a.valueRef !== b.valueRef) yield [ka, a.valueRef, b.valueRef];
+    for (const byte of [...a.children.keys()].sort()) {
+      const ca = a.children.get(byte);
+      const cb = b.children.get(byte) ?? null;
+      const keyPrefix = ka + byte;
+      if (ca === cb || !meets(keyPrefix, prefix)) continue; // shared subtree, or outside the prefix
+      const na = await load(ca);
+      yield* extensionFaults(load, na, cb !== null ? await load(cb) : null, keyPrefix, prefix);
+    }
+    return;
+  }
+  const common = commonPrefix(pa, pb);
+  if (common.length < pa.length && common.length < pb.length) {
+    yield* itemsUnder(load, a, acc, prefix); // disjoint key ranges
+  } else if (common.length === pa.length) {
+    // a's edge is a proper prefix of b's: b lives under one of a's branches
+    const ka = acc + pa;
+    const bb = pb.slice(pa.length, pa.length + 2);
+    const bSplit = new TrieNode(pb.slice(pa.length + 2), b.valueRef, b.children);
+    if (a.valueRef !== null && ka.startsWith(prefix)) yield [ka, a.valueRef, null];
+    for (const byte of [...a.children.keys()].sort()) {
+      const keyPrefix = ka + byte;
+      if (!meets(keyPrefix, prefix)) continue;
+      const child = await load(a.children.get(byte));
+      if (byte === bb) yield* extensionFaults(load, child, bSplit, keyPrefix, prefix);
+      else yield* itemsUnder(load, child, keyPrefix, prefix);
+    }
+  } else {
+    // b's edge is a proper prefix of a's: a lives under one of b's branches
+    const kb = acc + pb;
+    const ab = pa.slice(pb.length, pb.length + 2);
+    const aSplit = new TrieNode(pa.slice(pb.length + 2), a.valueRef, a.children);
+    const cb = b.children.get(ab) ?? null;
+    yield* extensionFaults(load, aSplit, cb !== null ? await load(cb) : null, kb + ab, prefix);
+  }
+}
+
+/** The records `base` held under `prefix` (hex) that `root` lacks or changed. */
+export async function* extensionWalk(load, base, root, prefix) {
+  if (base === null || base === root) return; // nothing held before, or the same state
+  const a = await load(base);
+  yield* extensionFaults(load, a, root !== null ? await load(root) : null, '', prefix);
+}
+
+/** Prefixes as an extension proof names them: unique, in code point order, at least one. */
+export function checkPrefixes(prefixes) {
+  if (typeof prefixes === 'string') throw new TypeError('prefixes is a list of strings, not one string');
+  const list = [...prefixes];
+  if (!list.length || !list.every((p) => typeof p === 'string')) {
+    throw new TypeError("an extension proof names at least one string prefix ('' for every key)");
+  }
+  return [...new Set(list)].sort(compareCodePoints);
+}
+
+/** A loader over the blobs an envelope carries, indexed by their recomputed references. */
+async function carried(list, refOf, what) {
+  const byRef = new Map();
+  for (const b of blobs(list, what)) byRef.set(await refOf(b), b);
+  return byRef;
+}
+
+function loaderOver(nodes) {
+  return async (ref) => {
+    const bytes = nodes.get(ref);
+    if (!bytes) throw new ProofError(`the proof lacks node ${ref}`);
+    return decodeNode(bytes, `node ${ref}`);
+  };
+}
+
+// -- prefix proofs ------------------------------------------------------------
+
+/**
+ * Check a prefix proof against `root` and resolve to a Map of every record
+ * under its prefix, proven complete; ProofError on any mismatch, including a
+ * node or value the walk needs and the proof lacks.
+ */
+export async function verifyPrefix(proof, root = null) {
+  envelope(proof, PREFIX_FORMAT);
+  if ((proof.root ?? null) !== root) throw new ProofError(`proof is about root ${JSON.stringify(proof.root)}, not ${JSON.stringify(root)}`);
+  const { prefix } = proof;
+  if (typeof prefix !== 'string' || !isWellFormed(prefix)) throw new ProofError('proof carries no prefix');
+  const refOf = addressing(proof.addressing);
+  const nodes = await carried(proof.nodes, refOf, 'node');
+  const values = await carried(proof.values, refOf, 'value');
+  const load = loaderOver(nodes);
+  const out = new Map();
+  if (root === null) return out; // the empty store holds nothing
+  for await (const [key, valueRef] of itemsUnder(load, await load(root), '', utf8Hex(prefix))) {
+    const value = values.get(valueRef);
+    if (!value) throw new ProofError(`the proof lacks the value of ${JSON.stringify(keyText(key))}`);
+    out.set(hexUtf8(key), decodeValue(value));
+  }
+  return out;
+}
+
+// -- extension proofs ---------------------------------------------------------
+
+/**
+ * Check that `root` extends `base` under the proof's prefixes (every record
+ * `base` held under them is held by `root` with the same value) and resolve
+ * to the prefixes proven. `base` and `root` are the references the verifier
+ * trusts (null: the empty store).
+ */
+export async function verifyExtension(proof, base = null, root = null) {
+  envelope(proof, EXTENSION_FORMAT);
+  if ((proof.base ?? null) !== base || (proof.root ?? null) !== root) {
+    throw new ProofError(`proof is about ${JSON.stringify(proof.base)} -> ${JSON.stringify(proof.root)}, not ${JSON.stringify(base)} -> ${JSON.stringify(root)}`);
+  }
+  const refOf = addressing(proof.addressing);
+  let prefixes;
+  try {
+    prefixes = checkPrefixes(proof.prefixes || []);
+  } catch (e) {
+    throw new ProofError(e.message);
+  }
+  if (!Array.isArray(proof.prefixes) || prefixes.length !== proof.prefixes.length || prefixes.some((p, i) => p !== proof.prefixes[i])) {
+    throw new ProofError("the proof's prefixes are not in canonical order");
+  }
+  const load = loaderOver(await carried(proof.nodes, refOf, 'node'));
+  for (const prefix of prefixes) {
+    if (!isWellFormed(prefix)) throw new ProofError('a prefix contains a lone surrogate');
+    for await (const [key, , b] of extensionWalk(load, base, root, utf8Hex(prefix))) {
+      throw new ProofError(`${JSON.stringify(keyText(key))} ${b === null ? 'is absent from' : 'changed in'} the later root`);
+    }
+  }
+  return prefixes;
+}
+
+export { prefixHex };
diff --git a/js/src/recordstore.js b/js/src/recordstore.js
index e36751f..5ac1500 100644
--- a/js/src/recordstore.js
+++ b/js/src/recordstore.js
@@ -8,8 +8,12 @@
 // - `has` is Python's `contains`.
 
 import { canonicalCopy, decodeValue, encodeValue } from './canonical.js';
-import { hexUtf8, isWellFormed, utf8Hex } from './bytes.js';
+import { compareCodePoints, hexUtf8, isWellFormed, toHex, utf8Hex } from './bytes.js';
 import { DEFAULT_NODE_CACHE_SIZE, KeyMissing, Trie } from './trie.js';
+import {
+  EXTENSION_FORMAT, PREFIX_FORMAT, PROOF_FORMAT, addressingName, checkPrefixes, extensionWalk, itemsUnder,
+  verifyExtension, verifyPrefix, verifyProof,
+} from './proofs.js';
 
 /** A side of a diff or merge where the key is absent. */
 export const ABSENT = Symbol.for('recordstore.ABSENT');
@@ -270,6 +274,124 @@ export class RecordStore {
     return this._trie.refsUnder(this._root, prefixHex(prefix));
   }
 
+  // -- proofs ---------------------------------------------------------------
+
+  /**
+   * A proof that `key` holds its record in the committed root, or that it is
+   * absent: the raw nodes on the key's one possible path, and the value.
+   * `verifyProof(proof, root)` checks it with no store. Refused for a key
+   * with staged changes (proofs are about committed roots). `addressing`
+   * defaults to the bytes store's. Every proof is checked before it is
+   * returned.
+   */
+  async prove(key, { addressing } = {}) {
+    const kh = keyHex(key);
+    if (this._staged.has(key)) {
+      throw new Error(`${JSON.stringify(key)} has staged, uncommitted changes; proofs are statements about a committed root, commit() first`);
+    }
+    const name = addressing ?? addressingName(this._blobs);
+    const nodes = [];
+    let present = false;
+    let value = null;
+    let ref = this._root;
+    let remaining = kh;
+    while (ref !== null) {
+      const blob = await this._blobs.get(ref); // the exact bytes behind the reference
+      nodes.push(toHex(blob));
+      const node = Trie.decode(blob);
+      if (!remaining.startsWith(node.prefix)) break;
+      remaining = remaining.slice(node.prefix.length);
+      if (remaining === '') {
+        if (node.valueRef !== null) {
+          present = true;
+          value = toHex(await this._blobs.get(node.valueRef));
+        }
+        break;
+      }
+      ref = node.children.get(remaining.slice(0, 2)) ?? null;
+      remaining = remaining.slice(2);
+    }
+    const proof = { format: PROOF_FORMAT, version: 1, addressing: name, root: this._root, key, present, nodes, value };
+    await verifyProof(proof, this._root); // never hand out a broken proof
+    return proof;
+  }
+
+  /**
+   * A proof of every record under `prefix` ('' for the whole store) in the
+   * committed root, and of no others. `verifyPrefix(proof, root)` returns
+   * them all. Its size is the size of what is under the prefix.
+   */
+  async provePrefix(prefix = '', { addressing } = {}) {
+    if (typeof prefix !== 'string') throw new TypeError("prefix is a string ('' for every key)");
+    const staged = [...this._staged.keys()].filter((k) => k.startsWith(prefix)).sort(compareCodePoints);
+    if (staged.length) throw new Error(`${JSON.stringify(staged[0])} has staged, uncommitted changes under the prefix; commit() first`);
+    const name = addressing ?? addressingName(this._blobs);
+    const nodes = new Map();
+    const values = new Map();
+    const load = async (ref) => {
+      const blob = await this._blobs.get(ref);
+      nodes.set(ref, toHex(blob));
+      return Trie.decode(blob);
+    };
+    if (this._root !== null) {
+      for await (const [, valueRef] of itemsUnder(load, await load(this._root), '', prefixHex(prefix))) {
+        values.set(valueRef, toHex(await this._blobs.get(valueRef)));
+      }
+    }
+    const proof = {
+      format: PREFIX_FORMAT, version: 1, addressing: name, root: this._root, prefix,
+      nodes: [...nodes.values()].sort(), values: [...values.values()].sort(),
+    };
+    await verifyPrefix(proof, this._root);
+    return proof;
+  }
+
+  /**
+   * Does the committed root hold, under each of `prefixes`, every record
+   * `base` held, unchanged (additions allowed)? `base`'s nodes must be in
+   * this store's bytes store.
+   */
+  async extends(base, prefixes = ['']) {
+    const load = (ref) => this._trie.load(ref);
+    for (const p of checkPrefixes(prefixes)) {
+      const faults = extensionWalk(load, base ?? null, this._root, prefixHex(p));
+      if (!(await faults.next()).done) return false;
+    }
+    return true;
+  }
+
+  /**
+   * A proof that the committed root extends `base` under `prefixes`: no
+   * record `base` held under them was removed or changed. Its nodes are
+   * those of the two roots along what changed under the prefixes. Throws
+   * when the root does not extend `base` (naming the key).
+   */
+  async proveExtension(base, prefixes = [''], { addressing } = {}) {
+    const list = checkPrefixes(prefixes);
+    const staged = [...this._staged.keys()].filter((k) => list.some((p) => k.startsWith(p)));
+    if (staged.length) throw new Error(`${JSON.stringify(staged[0])} has staged, uncommitted changes under the prefixes; commit() first`);
+    const name = addressing ?? addressingName(this._blobs);
+    const blobs = new Map();
+    const load = async (ref) => {
+      const blob = await this._blobs.get(ref);
+      blobs.set(ref, toHex(blob));
+      return Trie.decode(blob);
+    };
+    base = base ?? null;
+    for (const prefix of list) {
+      for await (const [key, , b] of extensionWalk(load, base, this._root, prefixHex(prefix))) {
+        throw new Error(`${JSON.stringify(this._root)} does not extend ${JSON.stringify(base)} under ${JSON.stringify(prefix)}: `
+          + `${JSON.stringify(hexUtf8(key))} is ${b === null ? 'absent from' : 'changed in'} it`);
+      }
+    }
+    const proof = {
+      format: EXTENSION_FORMAT, version: 1, addressing: name, base, root: this._root,
+      prefixes: list, nodes: [...blobs.values()].sort(),
+    };
+    await verifyExtension(proof, base, this._root);
+    return proof;
+  }
+
   // -- writing --------------------------------------------------------------
 
   /** Stage `value` under `key` (validated and copied now, written on commit). */
diff --git a/js/src/trie.js b/js/src/trie.js
index 9d9d3dd..2949211 100644
--- a/js/src/trie.js
+++ b/js/src/trie.js
@@ -71,7 +71,7 @@ class NodeCache {
 }
 
 /** Longest common prefix of two hex strings, on a byte boundary. */
-function commonPrefix(a, b) {
+export function commonPrefix(a, b) {
   const n = Math.min(a.length, b.length);
   let i = 0;
   while (i < n && a[i] === b[i]) i++;
diff --git a/js/test/fixtures/proofs.json b/js/test/fixtures/proofs.json
new file mode 100644
index 0000000..16697d9
--- /dev/null
+++ b/js/test/fixtures/proofs.json
@@ -0,0 +1,2855 @@
+{
+ "note": "Generated by js/tools/make_fixtures.py from the Python reference implementation; do not edit by hand.",
+ "cases": [
+  {
+   "addressing": "sha256",
+   "first": [
+    {
+     "op": "put",
+     "key": "log/1",
+     "value": {
+      "event": 1
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/2",
+     "value": {
+      "event": 2
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/3",
+     "value": {
+      "event": 3
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/4",
+     "value": {
+      "event": 4
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/5",
+     "value": {
+      "event": 5
+     }
+    },
+    {
+     "op": "put",
+     "key": "state/x",
+     "value": "old"
+    },
+    {
+     "op": "put",
+     "key": "state/y",
+     "value": [
+      1,
+      2
+     ]
+    },
+    {
+     "op": "put",
+     "key": "é",
+     "value": "é"
+    },
+    {
+     "op": "put",
+     "key": "😀",
+     "value": null
+    },
+    {
+     "op": "put",
+     "key": "users/alice",
+     "value": {
+      "n": "Alice"
+     }
+    },
+    {
+     "op": "put",
+     "key": "users/al",
+     "value": 0.5
+    },
+    {
+     "op": "commit"
+    }
+   ],
+   "second": [
+    {
+     "op": "put",
+     "key": "log/6",
+     "value": {
+      "event": 6
+     }
+    },
+    {
+     "op": "put",
+     "key": "state/x",
+     "value": "new"
+    },
+    {
+     "op": "delete",
+     "key": "users/al"
+    },
+    {
+     "op": "put",
+     "key": "zz",
+     "value": true
+    },
+    {
+     "op": "commit"
+    }
+   ],
+   "roots": {
+    "a": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+    "b": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+   },
+   "empty": {
+    "key": "anything",
+    "proof": {
+     "format": "recordstore-trie-proof",
+     "version": 1,
+     "addressing": "sha256",
+     "root": null,
+     "key": "anything",
+     "present": false,
+     "nodes": [],
+     "value": null
+    }
+   },
+   "keys": [
+    {
+     "key": "log/3",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "present": true,
+     "value": {
+      "event": 3
+     }
+    },
+    {
+     "key": "state/x",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "state/x",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2265336131316335666461363065373439336366613661323562656538623962316532633763346462313930383636636633316361363831373736616339393165222c223739223a2236356435646466626364653363646335633564663533613663306137356235643834646439346165643965653261626131633137343564363634333966343230227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2265373335636664306138613534363931343437643063623630383665366262633864626466323137356431313665316139626366363234316663373063393436227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a226f6c64227d"
+     },
+     "present": true,
+     "value": "old"
+    },
+    {
+     "key": "users/alice",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "users/alice",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+     },
+     "present": true,
+     "value": {
+      "n": "Alice"
+     }
+    },
+    {
+     "key": "users/al",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "users/al",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a302e357d"
+     },
+     "present": true,
+     "value": 0.5
+    },
+    {
+     "key": "users/a",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "users/a",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "users/alicex",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "users/alicex",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "é",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "é",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2266653862643536303133383036356664636461343037353034316264316630343935393263626666326165633762313939366336346164323463653637383039227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a22c3a9227d"
+     },
+     "present": true,
+     "value": "é"
+    },
+    {
+     "key": "😀",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "😀",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2232306662646531306239386663633362396564366363653166316637616365663465343066356462636164633465353235396165613537366131333961393662227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a6e756c6c7d"
+     },
+     "present": true,
+     "value": null
+    },
+    {
+     "key": "nope",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "nope",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "log/",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "log/",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "zzz",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "key": "zzz",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "log/3",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "present": true,
+     "value": {
+      "event": 3
+     }
+    },
+    {
+     "key": "state/x",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "state/x",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2235643232636333316166373262343365646439383937313062306532303566343135313731313333363264303032313766633465663661306161663338616232222c223739223a2236356435646466626364653363646335633564663533613663306137356235643834646439346165643965653261626131633137343564363634333966343230227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238626138343432323735393662656538353831376436363833386438653034653963656230643436623766646337393232353234633165633539663339366133227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a226e6577227d"
+     },
+     "present": true,
+     "value": "new"
+    },
+    {
+     "key": "users/alice",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "users/alice",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+     },
+     "present": true,
+     "value": {
+      "n": "Alice"
+     }
+    },
+    {
+     "key": "users/al",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "users/al",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "users/a",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "users/a",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "users/alicex",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "users/alicex",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "é",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "é",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2266653862643536303133383036356664636461343037353034316264316630343935393263626666326165633762313939366336346164323463653637383039227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a22c3a9227d"
+     },
+     "present": true,
+     "value": "é"
+    },
+    {
+     "key": "😀",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "😀",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2232306662646531306239386663633362396564366363653166316637616365663465343066356462636164633465353235396165613537366131333961393662227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a6e756c6c7d"
+     },
+     "present": true,
+     "value": null
+    },
+    {
+     "key": "nope",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "nope",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "log/",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "zzz",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "zzz",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223761222c22746e223a312c2276223a2263363061636538653238616363336437386566623533646632393063326335666439363063363833626265356164646336613463316639376166643636356537227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    }
+   ],
+   "prefixes": [
+    {
+     "prefix": "",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "",
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2265336131316335666461363065373439336366613661323562656538623962316532633763346462313930383636636633316361363831373736616339393165222c223739223a2236356435646466626364653363646335633564663533613663306137356235643834646439346165643965653261626131633137343564363634333966343230227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2230613331653765653936303835383837306564373365363732623931316565663764303631656336363238646530636366636338343435373230636163333335227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235333838373961663638373934343136626135643934343264666230653932666630373161393637343731653964613231316562313664303733623334663131227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236383132643736356331376565373730343931353931623337633439366530653264343632633132643834333766323462333463626465313539633037633235227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2262373331313832313335376466333964396663616635353966663262666638373062313635363933626361396665316238386661623332623935373735323437227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263393933333233336639646464616236663934363062636161366666626565653734383766376531316535333934636330353863616236636163633266623261227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2265373335636664306138613534363931343437643063623630383665366262633864626466323137356431313665316139626366363234316663373063393436227d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2232306662646531306239386663633362396564366363653166316637616365663465343066356462636164633465353235396165613537366131333961393662227d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2266653862643536303133383036356664636461343037353034316264316630343935393263626666326165633762313939366336346164323463653637383039227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226f6c64227d",
+       "7b22727376223a312c2276616c223a22c3a9227d",
+       "7b22727376223a312c2276616c223a302e357d",
+       "7b22727376223a312c2276616c223a5b312c325d7d",
+       "7b22727376223a312c2276616c223a6e756c6c7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ],
+      [
+       "state/x",
+       "old"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ],
+      [
+       "users/al",
+       0.5
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ],
+      [
+       "é",
+       "é"
+      ],
+      [
+       "😀",
+       null
+      ]
+     ]
+    },
+    {
+     "prefix": "log/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "log/",
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2230613331653765653936303835383837306564373365363732623931316565663764303631656336363238646530636366636338343435373230636163333335227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236383132643736356331376565373730343931353931623337633439366530653264343632633132643834333766323462333463626465313539633037633235227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2262373331313832313335376466333964396663616635353966663262666638373062313635363933626361396665316238386661623332623935373735323437227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263393933333233336639646464616236663934363062636161366666626565653734383766376531316535333934636330353863616236636163633266623261227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "users/",
+      "nodes": [
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a302e357d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/al",
+       0.5
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/al",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "users/al",
+      "nodes": [
+       "7b2263223a7b223639223a2238353962306465643966386431393639343733633334313833656234626438343264636330663439616238616633653635386639353965303861343665393866227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2238333335643137626236313966313332666262333038306538333436383561383265383230316534613162363937386332316536343638393035323739656531227d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a302e357d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/al",
+       0.5
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "state/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "state/",
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2265336131316335666461363065373439336366613661323562656538623962316532633763346462313930383636636633316361363831373736616339393165222c223739223a2236356435646466626364653363646335633564663533613663306137356235643834646439346165643965653261626131633137343564363634333966343230227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235333838373961663638373934343136626135643934343264666230653932666630373161393637343731653964613231316562313664303733623334663131227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2265373335636664306138613534363931343437643063623630383665366262633864626466323137356431313665316139626366363234316663373063393436227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226f6c64227d",
+       "7b22727376223a312c2276616c223a5b312c325d7d"
+      ]
+     },
+     "records": [
+      [
+       "state/x",
+       "old"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ]
+     ]
+    },
+    {
+     "prefix": "é",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "é",
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2266653862643536303133383036356664636461343037353034316264316630343935393263626666326165633762313939366336346164323463653637383039227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a22c3a9227d"
+      ]
+     },
+     "records": [
+      [
+       "é",
+       "é"
+      ]
+     ]
+    },
+    {
+     "prefix": "zz",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "zz",
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "values": []
+     },
+     "records": []
+    },
+    {
+     "prefix": "x",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "prefix": "x",
+      "nodes": [
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "values": []
+     },
+     "records": []
+    },
+    {
+     "prefix": "",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "",
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2235643232636333316166373262343365646439383937313062306532303566343135313731313333363264303032313766633465663661306161663338616232222c223739223a2236356435646466626364653363646335633564663533613663306137356235643834646439346165643965653261626131633137343564363634333966343230227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2230613331653765653936303835383837306564373365363732623931316565663764303631656336363238646530636366636338343435373230636163333335227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235333838373961663638373934343136626135643934343264666230653932666630373161393637343731653964613231316562313664303733623334663131227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236383132643736356331376565373730343931353931623337633439366530653264343632633132643834333766323462333463626465313539633037633235227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238626138343432323735393662656538353831376436363833386438653034653963656230643436623766646337393232353234633165633539663339366133227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2262373331313832313335376466333964396663616635353966663262666638373062313635363933626361396665316238386661623332623935373735323437227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263393933333233336639646464616236663934363062636161366666626565653734383766376531316535333934636330353863616236636163633266623261227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2265343636646235346663626533663663613663633932643233383464316664346664333232373935336531396533373635653933343361633663623539613838227d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d",
+       "7b2263223a7b7d2c2270223a223761222c22746e223a312c2276223a2263363061636538653238616363336437386566623533646632393063326335666439363063363833626265356164646336613463316639376166643636356537227d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2232306662646531306239386663633362396564366363653166316637616365663465343066356462636164633465353235396165613537366131333961393662227d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2266653862643536303133383036356664636461343037353034316264316630343935393263626666326165633762313939366336346164323463653637383039227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226e6577227d",
+       "7b22727376223a312c2276616c223a22c3a9227d",
+       "7b22727376223a312c2276616c223a5b312c325d7d",
+       "7b22727376223a312c2276616c223a6e756c6c7d",
+       "7b22727376223a312c2276616c223a747275657d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a367d7d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ],
+      [
+       "log/6",
+       {
+        "event": 6
+       }
+      ],
+      [
+       "state/x",
+       "new"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ],
+      [
+       "zz",
+       true
+      ],
+      [
+       "é",
+       "é"
+      ],
+      [
+       "😀",
+       null
+      ]
+     ]
+    },
+    {
+     "prefix": "log/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "log/",
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2230613331653765653936303835383837306564373365363732623931316565663764303631656336363238646530636366636338343435373230636163333335227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236383132643736356331376565373730343931353931623337633439366530653264343632633132643834333766323462333463626465313539633037633235227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2262373331313832313335376466333964396663616635353966663262666638373062313635363933626361396665316238386661623332623935373735323437227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263393933333233336639646464616236663934363062636161366666626565653734383766376531316535333934636330353863616236636163633266623261227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2265343636646235346663626533663663613663633932643233383464316664346664333232373935336531396533373635653933343361633663623539613838227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a367d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ],
+      [
+       "log/6",
+       {
+        "event": 6
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "users/",
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/al",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "users/al",
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "state/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "state/",
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2235643232636333316166373262343365646439383937313062306532303566343135313731313333363264303032313766633465663661306161663338616232222c223739223a2236356435646466626364653363646335633564663533613663306137356235643834646439346165643965653261626131633137343564363634333966343230227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235333838373961663638373934343136626135643934343264666230653932666630373161393637343731653964613231316562313664303733623334663131227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238626138343432323735393662656538353831376436363833386438653034653963656230643436623766646337393232353234633165633539663339366133227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226e6577227d",
+       "7b22727376223a312c2276616c223a5b312c325d7d"
+      ]
+     },
+     "records": [
+      [
+       "state/x",
+       "new"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ]
+     ]
+    },
+    {
+     "prefix": "é",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "é",
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2266653862643536303133383036356664636461343037353034316264316630343935393263626666326165633762313939366336346164323463653637383039227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a22c3a9227d"
+      ]
+     },
+     "records": [
+      [
+       "é",
+       "é"
+      ]
+     ]
+    },
+    {
+     "prefix": "zz",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "zz",
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223761222c22746e223a312c2276223a2263363061636538653238616363336437386566623533646632393063326335666439363063363833626265356164646336613463316639376166643636356537227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a747275657d"
+      ]
+     },
+     "records": [
+      [
+       "zz",
+       true
+      ]
+     ]
+    },
+    {
+     "prefix": "x",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "x",
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "values": []
+     },
+     "records": []
+    }
+   ],
+   "extensions": [
+    {
+     "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      "log/"
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefixes": [
+       "log/"
+      ],
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ]
+     },
+     "proven": [
+      "log/"
+     ]
+    },
+    {
+     "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      "log/",
+      "é",
+      "😀"
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefixes": [
+       "log/",
+       "é",
+       "😀"
+      ],
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ]
+     },
+     "proven": [
+      "log/",
+      "é",
+      "😀"
+     ]
+    },
+    {
+     "base": null,
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      ""
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "base": null,
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefixes": [
+       ""
+      ],
+      "nodes": []
+     },
+     "proven": [
+      ""
+     ]
+    },
+    {
+     "base": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      ""
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "base": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefixes": [
+       ""
+      ],
+      "nodes": []
+     },
+     "proven": [
+      ""
+     ]
+    },
+    {
+     "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      ""
+     ],
+     "extends": false,
+     "error": "'b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786' does not extend '7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f' under '': 'state/x' is changed in it"
+    },
+    {
+     "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      "state/"
+     ],
+     "extends": false,
+     "error": "'b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786' does not extend '7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f' under 'state/': 'state/x' is changed in it"
+    },
+    {
+     "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+     "prefixes": [
+      "users/"
+     ],
+     "extends": false,
+     "error": "'b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786' does not extend '7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f' under 'users/': 'users/al' is absent from it"
+    }
+   ],
+   "refused": [
+    {
+     "kind": "key",
+     "name": "a node byte changed",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c00",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+    },
+    {
+     "kind": "key",
+     "name": "the value changed",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a333030"
+     },
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+    },
+    {
+     "kind": "key",
+     "name": "claims absence",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/3",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d"
+      ],
+      "value": null
+     },
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+    },
+    {
+     "kind": "key",
+     "name": "about another root",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238633764616338643132323535363239626535313333336364303965303437656435373339613437643761353162356166323535663830653638323964626330227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "root": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f"
+    },
+    {
+     "kind": "key",
+     "name": "the last node dropped",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+    },
+    {
+     "kind": "prefix",
+     "name": "a node missing",
+     "envelope": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefix": "users/",
+      "nodes": [
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2232613737396132356663613365633261646566376632313865643039666365613135323239313832313262356463623839633565666636633661646161666366227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+    },
+    {
+     "kind": "extension",
+     "name": "claims a wider prefix",
+     "envelope": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "sha256",
+      "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+      "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786",
+      "prefixes": [
+       ""
+      ],
+      "nodes": [
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862222c223336223a2231376339663662636463373364616430383934366563353063653433326333613530343366323038316232323636383664373733306331376334646462396636227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2230366338396638666265356163396436353464373630393031633930653961373830383465376634303661616465663633333539373866316362333365316332222c223332223a2238303966343430313362323366323938393065636464383961346236366331613931356465326533373063396338336233623566383230333462353738636432222c223333223a2266643162356666373565363038346463633739326463303866666634613231656431643731393538313636336131333131323165613634653232646432356135222c223334223a2232303262303937323931343933343932613664346331366532333161613630333063376662366438383436636464383161643634303863656334656165636264222c223335223a2239626235346133313662393231346437306138333838663838363337636165313264336363353330623763383039363965666236356430626566393963343862227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2237386637613836353034326262646139353665343434373864393463303734323939323431656161633563653035366133653735306136356238336662386362222c223733223a2235616461306165393539353764656566383062366564333261383037373435376530646336616463383732626137633734363236333439613661316631663666222c223735223a2232633135653164366531303039353530653931366335356431623037353135626531326633373161346535643030386463613433383039633533306562383864222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2266656232633233333632306164333638323263346237616364346139626333386239336461366630623537326334633763396537313139653836306462666232222c223733223a2234353130333634313561313966303737356331623638626634386666633761363633663430393133346662306265623539303565333234343662306462306362222c223735223a2234653934656436356432343662623831396534633134666265316361656433356363303130366532616135313134396632313963656263363363316130356564222c223761223a2232626331393038643761643739353365336234643232393638343635366538396432633738376163623031353962343664383835623766326264356332353566222c226333223a2231656332343330326165303464643033343333663037386639333631363333626661393730663364313338643132613634323731303937633433626339623535222c226630223a2231303166663464313661316433626662333933623866353738623135386137643730303061643432346564656436326338646437623466613264383434613761227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ]
+     },
+     "base": "7a9b8b34ad0ea7cee9567e43a7dd6eb870673c8081ae07240fdeffb9dff19a8f",
+     "root": "b48efdba30412778238abb5a5c90ed9e7141a017395a6e748229dc86b2173786"
+    }
+   ]
+  },
+  {
+   "addressing": "swarm",
+   "first": [
+    {
+     "op": "put",
+     "key": "log/1",
+     "value": {
+      "event": 1
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/2",
+     "value": {
+      "event": 2
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/3",
+     "value": {
+      "event": 3
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/4",
+     "value": {
+      "event": 4
+     }
+    },
+    {
+     "op": "put",
+     "key": "log/5",
+     "value": {
+      "event": 5
+     }
+    },
+    {
+     "op": "put",
+     "key": "state/x",
+     "value": "old"
+    },
+    {
+     "op": "put",
+     "key": "state/y",
+     "value": [
+      1,
+      2
+     ]
+    },
+    {
+     "op": "put",
+     "key": "é",
+     "value": "é"
+    },
+    {
+     "op": "put",
+     "key": "😀",
+     "value": null
+    },
+    {
+     "op": "put",
+     "key": "users/alice",
+     "value": {
+      "n": "Alice"
+     }
+    },
+    {
+     "op": "put",
+     "key": "users/al",
+     "value": 0.5
+    },
+    {
+     "op": "commit"
+    }
+   ],
+   "second": [
+    {
+     "op": "put",
+     "key": "log/6",
+     "value": {
+      "event": 6
+     }
+    },
+    {
+     "op": "put",
+     "key": "state/x",
+     "value": "new"
+    },
+    {
+     "op": "delete",
+     "key": "users/al"
+    },
+    {
+     "op": "put",
+     "key": "zz",
+     "value": true
+    },
+    {
+     "op": "commit"
+    }
+   ],
+   "roots": {
+    "a": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+    "b": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+   },
+   "empty": {
+    "key": "anything",
+    "proof": {
+     "format": "recordstore-trie-proof",
+     "version": 1,
+     "addressing": "swarm",
+     "root": null,
+     "key": "anything",
+     "present": false,
+     "nodes": [],
+     "value": null
+    }
+   },
+   "keys": [
+    {
+     "key": "log/3",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "present": true,
+     "value": {
+      "event": 3
+     }
+    },
+    {
+     "key": "state/x",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "state/x",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2231663564376334643936306461333234663762663534343662356339363436383734353261363762633531376238336137646331663166643863373066336431222c223739223a2263353964386263383036356563366633663630643435383134666465663735333931613630663231616433343538383866393735373337393539336236366134227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238656437343537333961303632653539333830613965383661343939333961373934363231386562376636323265653431353036633764356436633363373962227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a226f6c64227d"
+     },
+     "present": true,
+     "value": "old"
+    },
+    {
+     "key": "users/alice",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "users/alice",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+     },
+     "present": true,
+     "value": {
+      "n": "Alice"
+     }
+    },
+    {
+     "key": "users/al",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "users/al",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a302e357d"
+     },
+     "present": true,
+     "value": 0.5
+    },
+    {
+     "key": "users/a",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "users/a",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "users/alicex",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "users/alicex",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "é",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "é",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2231633638303138653464323430663431633430386563363265353531653136396264383236343938656437616239656232663634363236626233306530623639227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a22c3a9227d"
+     },
+     "present": true,
+     "value": "é"
+    },
+    {
+     "key": "😀",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "😀",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2231613936623031386233653763656661633961646438613763333833616538643762643637623732336164343838306133656163313365613330363563303561227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a6e756c6c7d"
+     },
+     "present": true,
+     "value": null
+    },
+    {
+     "key": "nope",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "nope",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "log/",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "log/",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "zzz",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "key": "zzz",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "log/3",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "present": true,
+     "value": {
+      "event": 3
+     }
+    },
+    {
+     "key": "state/x",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "state/x",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2234643932316430316330386537663537636466623362316434633736626164633361623439656237613063623562663161313636386233383865623037343661222c223739223a2263353964386263383036356563366633663630643435383134666465663735333931613630663231616433343538383866393735373337393539336236366134227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233373038386231663865623938636435353366636565656532336262633034626363326366363566643033306431366534313435666566383930646565326431227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a226e6577227d"
+     },
+     "present": true,
+     "value": "new"
+    },
+    {
+     "key": "users/alice",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "users/alice",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+     },
+     "present": true,
+     "value": {
+      "n": "Alice"
+     }
+    },
+    {
+     "key": "users/al",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "users/al",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "users/a",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "users/a",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "users/alicex",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "users/alicex",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "é",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "é",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2231633638303138653464323430663431633430386563363265353531653136396264383236343938656437616239656232663634363236626233306530623639227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a22c3a9227d"
+     },
+     "present": true,
+     "value": "é"
+    },
+    {
+     "key": "😀",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "😀",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2231613936623031386233653763656661633961646438613763333833616538643762643637623732336164343838306133656163313365613330363563303561227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a6e756c6c7d"
+     },
+     "present": true,
+     "value": null
+    },
+    {
+     "key": "nope",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "nope",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "log/",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    },
+    {
+     "key": "zzz",
+     "proof": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "zzz",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223761222c22746e223a312c2276223a2230383835623730376366326632366463383237383763623163616333373436646335376461643435323137656439643762386338633635633133396562636163227d"
+      ],
+      "value": null
+     },
+     "present": false,
+     "value": null
+    }
+   ],
+   "prefixes": [
+    {
+     "prefix": "",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "",
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2231663564376334643936306461333234663762663534343662356339363436383734353261363762633531376238336137646331663166643863373066336431222c223739223a2263353964386263383036356563366633663630643435383134666465663735333931613630663231616433343538383866393735373337393539336236366134227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231373463363865373562396365626635613566313064336630653533356438623765326238346433346438633331636138313532663737366136363135666430227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231653265323565333561363135656166626333653664353834336436313163346330666332626566616463666464373735333034643766396361393961306532227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235613166373964643833316138623135316534343730356366626564383834313437313836613662343931373130613065643236653732626162616533396334227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236366532346338303234343362653736343330326635343136386362663437656438323234653762623561633364613439326365303437313834653833643462227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238656437343537333961303632653539333830613965383661343939333961373934363231386562376636323265653431353036633764356436633363373962227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263373163353534303563353431333635383632653130656565323161373562323739333230653838303565366335616332616332303230376335656265373930227d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2231613936623031386233653763656661633961646438613763333833616538643762643637623732336164343838306133656163313365613330363563303561227d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2231633638303138653464323430663431633430386563363265353531653136396264383236343938656437616239656232663634363236626233306530623639227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226f6c64227d",
+       "7b22727376223a312c2276616c223a22c3a9227d",
+       "7b22727376223a312c2276616c223a302e357d",
+       "7b22727376223a312c2276616c223a5b312c325d7d",
+       "7b22727376223a312c2276616c223a6e756c6c7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ],
+      [
+       "state/x",
+       "old"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ],
+      [
+       "users/al",
+       0.5
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ],
+      [
+       "é",
+       "é"
+      ],
+      [
+       "😀",
+       null
+      ]
+     ]
+    },
+    {
+     "prefix": "log/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "log/",
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231373463363865373562396365626635613566313064336630653533356438623765326238346433346438633331636138313532663737366136363135666430227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235613166373964643833316138623135316534343730356366626564383834313437313836613662343931373130613065643236653732626162616533396334227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236366532346338303234343362653736343330326635343136386362663437656438323234653762623561633364613439326365303437313834653833643462227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263373163353534303563353431333635383632653130656565323161373562323739333230653838303565366335616332616332303230376335656265373930227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "users/",
+      "nodes": [
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a302e357d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/al",
+       0.5
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/al",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "users/al",
+      "nodes": [
+       "7b2263223a7b223639223a2233306338616239383837393334306137643062346537633336383138643631643665366135326633313562336463383963396335613234303634616562313262227d2c2270223a223733363537323733326636313663222c22746e223a312c2276223a2232636364353430323935646632323636376663363036386232376534313665393239323766316434616130346335303233643335366164616439363066393031227d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a2236333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a302e357d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/al",
+       0.5
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "state/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "state/",
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2231663564376334643936306461333234663762663534343662356339363436383734353261363762633531376238336137646331663166643863373066336431222c223739223a2263353964386263383036356563366633663630643435383134666465663735333931613630663231616433343538383866393735373337393539336236366134227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231653265323565333561363135656166626333653664353834336436313163346330666332626566616463666464373735333034643766396361393961306532227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2238656437343537333961303632653539333830613965383661343939333961373934363231386562376636323265653431353036633764356436633363373962227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226f6c64227d",
+       "7b22727376223a312c2276616c223a5b312c325d7d"
+      ]
+     },
+     "records": [
+      [
+       "state/x",
+       "old"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ]
+     ]
+    },
+    {
+     "prefix": "é",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "é",
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2231633638303138653464323430663431633430386563363265353531653136396264383236343938656437616239656232663634363236626233306530623639227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a22c3a9227d"
+      ]
+     },
+     "records": [
+      [
+       "é",
+       "é"
+      ]
+     ]
+    },
+    {
+     "prefix": "zz",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "zz",
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "values": []
+     },
+     "records": []
+    },
+    {
+     "prefix": "x",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "prefix": "x",
+      "nodes": [
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "values": []
+     },
+     "records": []
+    },
+    {
+     "prefix": "",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "",
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2234643932316430316330386537663537636466623362316434633736626164633361623439656237613063623562663161313636386233383865623037343661222c223739223a2263353964386263383036356563366633663630643435383134666465663735333931613630663231616433343538383866393735373337393539336236366134227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231373463363865373562396365626635613566313064336630653533356438623765326238346433346438633331636138313532663737366136363135666430227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231653265323565333561363135656166626333653664353834336436313163346330666332626566616463666464373735333034643766396361393961306532227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233373038386231663865623938636435353366636565656532336262633034626363326366363566643033306431366534313435666566383930646565326431227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235613166373964643833316138623135316534343730356366626564383834313437313836613662343931373130613065643236653732626162616533396334227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236366532346338303234343362653736343330326635343136386362663437656438323234653762623561633364613439326365303437313834653833643462227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236613731346539353030386534333038616437623834323331323238643062363366363535666433376362373133303634393033646561333261313734633935227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263373163353534303563353431333635383632653130656565323161373562323739333230653838303565366335616332616332303230376335656265373930227d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d",
+       "7b2263223a7b7d2c2270223a223761222c22746e223a312c2276223a2230383835623730376366326632366463383237383763623163616333373436646335376461643435323137656439643762386338633635633133396562636163227d",
+       "7b2263223a7b7d2c2270223a22396639383830222c22746e223a312c2276223a2231613936623031386233653763656661633961646438613763333833616538643762643637623732336164343838306133656163313365613330363563303561227d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2231633638303138653464323430663431633430386563363265353531653136396264383236343938656437616239656232663634363236626233306530623639227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226e6577227d",
+       "7b22727376223a312c2276616c223a22c3a9227d",
+       "7b22727376223a312c2276616c223a5b312c325d7d",
+       "7b22727376223a312c2276616c223a6e756c6c7d",
+       "7b22727376223a312c2276616c223a747275657d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a367d7d",
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ],
+      [
+       "log/6",
+       {
+        "event": 6
+       }
+      ],
+      [
+       "state/x",
+       "new"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ],
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ],
+      [
+       "zz",
+       true
+      ],
+      [
+       "é",
+       "é"
+      ],
+      [
+       "😀",
+       null
+      ]
+     ]
+    },
+    {
+     "prefix": "log/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "log/",
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231373463363865373562396365626635613566313064336630653533356438623765326238346433346438633331636138313532663737366136363135666430227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2235613166373964643833316138623135316534343730356366626564383834313437313836613662343931373130613065643236653732626162616533396334227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236366532346338303234343362653736343330326635343136386362663437656438323234653762623561633364613439326365303437313834653833643462227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2236613731346539353030386534333038616437623834323331323238643062363366363535666433376362373133303634393033646561333261313734633935227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2263373163353534303563353431333635383632653130656565323161373562323739333230653838303565366335616332616332303230376335656265373930227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226576656e74223a317d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a327d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a337d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a347d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a357d7d",
+       "7b22727376223a312c2276616c223a7b226576656e74223a367d7d"
+      ]
+     },
+     "records": [
+      [
+       "log/1",
+       {
+        "event": 1
+       }
+      ],
+      [
+       "log/2",
+       {
+        "event": 2
+       }
+      ],
+      [
+       "log/3",
+       {
+        "event": 3
+       }
+      ],
+      [
+       "log/4",
+       {
+        "event": 4
+       }
+      ],
+      [
+       "log/5",
+       {
+        "event": 5
+       }
+      ],
+      [
+       "log/6",
+       {
+        "event": 6
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "users/",
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "users/al",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "users/al",
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "records": [
+      [
+       "users/alice",
+       {
+        "n": "Alice"
+       }
+      ]
+     ]
+    },
+    {
+     "prefix": "state/",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "state/",
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223738223a2234643932316430316330386537663537636466623362316434633736626164633361623439656237613063623562663161313636386233383865623037343661222c223739223a2263353964386263383036356563366633663630643435383134666465663735333931613630663231616433343538383866393735373337393539336236366134227d2c2270223a2237343631373436353266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2231653265323565333561363135656166626333653664353834336436313163346330666332626566616463666464373735333034643766396361393961306532227d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233373038386231663865623938636435353366636565656532336262633034626363326366363566643033306431366534313435666566383930646565326431227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a226e6577227d",
+       "7b22727376223a312c2276616c223a5b312c325d7d"
+      ]
+     },
+     "records": [
+      [
+       "state/x",
+       "new"
+      ],
+      [
+       "state/y",
+       [
+        1,
+        2
+       ]
+      ]
+     ]
+    },
+    {
+     "prefix": "é",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "é",
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a226139222c22746e223a312c2276223a2231633638303138653464323430663431633430386563363265353531653136396264383236343938656437616239656232663634363236626233306530623639227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a22c3a9227d"
+      ]
+     },
+     "records": [
+      [
+       "é",
+       "é"
+      ]
+     ]
+    },
+    {
+     "prefix": "zz",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "zz",
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a223761222c22746e223a312c2276223a2230383835623730376366326632366463383237383763623163616333373436646335376461643435323137656439643762386338633635633133396562636163227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a747275657d"
+      ]
+     },
+     "records": [
+      [
+       "zz",
+       true
+      ]
+     ]
+    },
+    {
+     "prefix": "x",
+     "proof": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "x",
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "values": []
+     },
+     "records": []
+    }
+   ],
+   "extensions": [
+    {
+     "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      "log/"
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefixes": [
+       "log/"
+      ],
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ]
+     },
+     "proven": [
+      "log/"
+     ]
+    },
+    {
+     "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      "log/",
+      "é",
+      "😀"
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefixes": [
+       "log/",
+       "é",
+       "😀"
+      ],
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ]
+     },
+     "proven": [
+      "log/",
+      "é",
+      "😀"
+     ]
+    },
+    {
+     "base": null,
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      ""
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "base": null,
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefixes": [
+       ""
+      ],
+      "nodes": []
+     },
+     "proven": [
+      ""
+     ]
+    },
+    {
+     "base": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      ""
+     ],
+     "extends": true,
+     "proof": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "base": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefixes": [
+       ""
+      ],
+      "nodes": []
+     },
+     "proven": [
+      ""
+     ]
+    },
+    {
+     "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      ""
+     ],
+     "extends": false,
+     "error": "'109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d' does not extend '9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c' under '': 'state/x' is changed in it"
+    },
+    {
+     "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      "state/"
+     ],
+     "extends": false,
+     "error": "'109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d' does not extend '9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c' under 'state/': 'state/x' is changed in it"
+    },
+    {
+     "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+     "prefixes": [
+      "users/"
+     ],
+     "extends": false,
+     "error": "'109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d' does not extend '9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c' under 'users/': 'users/al' is absent from it"
+    }
+   ],
+   "refused": [
+    {
+     "kind": "key",
+     "name": "a node byte changed",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c00",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+    },
+    {
+     "kind": "key",
+     "name": "the value changed",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a333030"
+     },
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+    },
+    {
+     "kind": "key",
+     "name": "claims absence",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/3",
+      "present": false,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d"
+      ],
+      "value": null
+     },
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+    },
+    {
+     "kind": "key",
+     "name": "about another root",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b7d2c2270223a22222c22746e223a312c2276223a2233356561353538653539653337323231663530366438363263393666356263663261313933306366626135656337393266666636636132363333616337346632227d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "root": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c"
+    },
+    {
+     "kind": "key",
+     "name": "the last node dropped",
+     "envelope": {
+      "format": "recordstore-trie-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "key": "log/3",
+      "present": true,
+      "nodes": [
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d"
+      ],
+      "value": "7b22727376223a312c2276616c223a7b226576656e74223a337d7d"
+     },
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+    },
+    {
+     "kind": "prefix",
+     "name": "a node missing",
+     "envelope": {
+      "format": "recordstore-prefix-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefix": "users/",
+      "nodes": [
+       "7b2263223a7b7d2c2270223a223733363537323733326636313663363936333635222c22746e223a312c2276223a2234643163396361313663656336396339383935343562313836616238303430323133343335393361376330326462333936646361333661626130326334323936227d"
+      ],
+      "values": [
+       "7b22727376223a312c2276616c223a7b226e223a22416c696365227d7d"
+      ]
+     },
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+    },
+    {
+     "kind": "extension",
+     "name": "claims a wider prefix",
+     "envelope": {
+      "format": "recordstore-extension-proof",
+      "version": 1,
+      "addressing": "swarm",
+      "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+      "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d",
+      "prefixes": [
+       ""
+      ],
+      "nodes": [
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662222c223336223a2239616363623766393737646339326631363135303833643736306330386230656133633035663932363937323432353661326661656165613464356262616533227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223331223a2237663632656631636564613536326332386661663064613331633061303838343964656566333533633733336439653838363061356266323136656664306533222c223332223a2237333961343134333633383433306465643639653037393037636634643330343331353362623039303163333739663834613364333839323934373533396431222c223333223a2235393634366331326637663334626239636438336562363136313762613063383837383534333435646561613734326561346434616331636530313138346538222c223334223a2232626630393463353235613735643562306334633638326237343163303333313231666239636239373265323439313832343330636233653039616566353966222c223335223a2265306634663436323364333861343261366234326132626636386535313336386334653334323637623863346262303235373031333461626465663465323662227d2c2270223a22366636373266222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2232373066343366313731393263323263343734336339633134643666643832636537363763626635386338393137666464316661613366313866326336353536222c223733223a2266383461643632313364363366383434343837353631616661313634623562363638633761396631626138383133643933616566396466376662343539306138222c223735223a2237313862343836343739373335646536646634373137663061383839346135663039353461326638313963393239303631616562373439343266323238653430222c223761223a2266333862343663626264383263663333623639383837346533393339313339353639313338383961376263373635356164356136316336626633386532346538222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d",
+       "7b2263223a7b223663223a2238346332366531366531336138313062316132363334313166336663663333663034636438356134383433386635313737373138393138316238343031386530222c223733223a2238666130643438373038343331623463303535316166646431306133653862333566656663363139356363323365643233356665373736393762363637376239222c223735223a2231333766303836616539333261623662373538313036323135326166313762363664663536663963326162653762623238393761313364636365323965353838222c226333223a2233393036353332383561616532343366386134323735306430323736656466333361313335663832656133313838613034636331376638386564353666343039222c226630223a2231316637633663353366633861333839636639366535363735623738633031666364363837356430323230613166333661313633616331343566393236303831227d2c2270223a22222c22746e223a312c2276223a6e756c6c7d"
+      ]
+     },
+     "base": "9718efc41d969461e9fde2bc94929cb02c85e07634c08b38e7f51a9ff2b9a99c",
+     "root": "109c1586e2735395e5f222e50c51708e3a71ab945ce4679eb1e89c08ccd6123d"
+    }
+   ]
+  }
+ ]
+}
diff --git a/js/test/live.test.js b/js/test/live.test.js
index 246a7ea..c4201f9 100644
--- a/js/test/live.test.js
+++ b/js/test/live.test.js
@@ -9,7 +9,10 @@
 
 import { test } from 'node:test';
 import assert from 'node:assert/strict';
-import { BeeBytesStore, RecordStore, SwarmFeedPointer, swarmStore, verifyFeedUpdate } from '../src/index.js';
+import { writeFileSync } from 'node:fs';
+import {
+  BeeBytesStore, RecordStore, SwarmFeedPointer, swarmStore, verifyFeedUpdate, verifyPrefix, verifyProof,
+} from '../src/index.js';
 import { toHex } from '../src/bytes.js';
 
 const url = process.env.RECORDSTORE_BEE_URL;
@@ -71,3 +74,18 @@ test('publishes a store behind a new feed and follows it', { skip: skip || (!bat
   assert.deepEqual(updates.map((u) => verifyFeedUpdate(u, writer._pointer.owner, topic).root), [first, second]);
   console.log(`# wrote feed ${topic} (owner ${writer._pointer.owner}): roots ${first.slice(0, 12)}..., ${second.slice(0, 12)}...`);
 });
+
+test('proves records of a published store, checkable with no node', { skip }, async () => {
+  const store = RecordStore.at(root, new BeeBytesStore(url, { concurrency: 32 }));
+  const started = Date.now();
+  const proof = await store.prove('abbey');
+  assert.deepEqual(await verifyProof(proof, root), await store.get('abbey'));
+  const absent = await store.prove('abbeyz');
+  assert.equal(absent.present, false);
+  const prefix = await store.provePrefix('ab');
+  const records = await verifyPrefix(prefix, root);
+  assert.ok(records.size > 0);
+  if (process.env.RECORDSTORE_PROOF_OUT) writeFileSync(process.env.RECORDSTORE_PROOF_OUT, JSON.stringify({ root, proof, absent, prefix }));
+  const size = (p) => JSON.stringify(p).length;
+  console.log(`# key proof ${proof.nodes.length} nodes / ${size(proof)} B, absence ${size(absent)} B, prefix 'ab' ${records.size} records / ${size(prefix)} B, ${Date.now() - started} ms`);
+});
diff --git a/js/test/proofs.test.js b/js/test/proofs.test.js
new file mode 100644
index 0000000..695039a
--- /dev/null
+++ b/js/test/proofs.test.js
@@ -0,0 +1,90 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import {
+  ABSENT, MemoryBytesStore, ProofError, RecordStore, verifyExtension, verifyPrefix, verifyProof,
+} from '../src/index.js';
+import { apply, fixture } from './helpers.js';
+
+const { cases } = fixture('proofs.json');
+
+async function build(c) {
+  const blobs = new MemoryBytesStore({ addressing: c.addressing });
+  const store = new RecordStore(blobs);
+  await apply(store, c.first);
+  assert.equal(store.root, c.roots.a);
+  await apply(store, c.second);
+  assert.equal(store.root, c.roots.b);
+  return blobs;
+}
+
+for (const c of cases) {
+  const opts = { addressing: c.addressing };
+
+  test(`${c.addressing}: key proofs are identical to Python's and verify to the same records`, async () => {
+    const blobs = await build(c);
+    const empty = new RecordStore(new MemoryBytesStore(opts));
+    assert.deepEqual(await empty.prove(c.empty.key, opts), c.empty.proof);
+    assert.equal(await verifyProof(c.empty.proof, null), ABSENT);
+    for (const k of c.keys) {
+      const store = RecordStore.at(k.proof.root, blobs);
+      assert.deepEqual(await store.prove(k.key, opts), k.proof, `prove(${JSON.stringify(k.key)})`);
+      const verdict = await verifyProof(k.proof, k.proof.root);
+      if (k.present) assert.deepEqual(verdict, k.value);
+      else assert.equal(verdict, ABSENT);
+    }
+  });
+
+  test(`${c.addressing}: prefix proofs are identical and complete`, async () => {
+    const blobs = await build(c);
+    for (const p of c.prefixes) {
+      const store = RecordStore.at(p.proof.root, blobs);
+      assert.deepEqual(await store.provePrefix(p.prefix, opts), p.proof, `provePrefix(${JSON.stringify(p.prefix)})`);
+      assert.deepEqual([...(await verifyPrefix(p.proof, p.proof.root))], p.records);
+    }
+  });
+
+  test(`${c.addressing}: extension proofs are identical, and the same extensions are refused`, async () => {
+    const blobs = await build(c);
+    for (const e of c.extensions) {
+      const store = RecordStore.at(e.root, blobs);
+      assert.equal(await store.extends(e.base, e.prefixes), e.extends);
+      if (e.proof) {
+        assert.deepEqual(await store.proveExtension(e.base, e.prefixes, opts), e.proof);
+        assert.deepEqual(await verifyExtension(e.proof, e.base, e.root), e.proven);
+      } else {
+        const named = e.error.match(/: '(.+)' is (absent from|changed in) it$/);
+        await assert.rejects(store.proveExtension(e.base, e.prefixes, opts), (err) => {
+          assert.ok(err.message.includes(`${JSON.stringify(named[1])} is ${named[2]} it`), err.message);
+          return true;
+        });
+      }
+    }
+  });
+
+  test(`${c.addressing}: every proof Python refuses is refused`, async () => {
+    for (const r of c.refused) {
+      const check = r.kind === 'key' ? verifyProof(r.envelope, r.root)
+        : r.kind === 'prefix' ? verifyPrefix(r.envelope, r.root)
+          : verifyExtension(r.envelope, r.base, r.root);
+      await assert.rejects(check, ProofError, r.name);
+    }
+  });
+}
+
+test('proofs are about committed roots: staged keys are refused', async () => {
+  const store = new RecordStore(new MemoryBytesStore());
+  store.put('a/1', 1);
+  await store.commit();
+  store.put('a/2', 2);
+  await assert.rejects(store.prove('a/2'), /staged/);
+  await assert.rejects(store.provePrefix('a/'), /staged/);
+  await assert.rejects(store.proveExtension(null, ['a/']), /staged/);
+  assert.deepEqual(await verifyProof(await store.prove('a/1'), store.root), 1);
+});
+
+test('a store with its own addressing function must name the scheme', async () => {
+  const blobs = new MemoryBytesStore({ addressing: async (d) => (await crypto.subtle.digest('SHA-256', d), 'x'.repeat(64)) });
+  const store = RecordStore.at(null, blobs);
+  await assert.rejects(store.prove('k'), /addressing/);
+  assert.equal((await store.prove('k', { addressing: 'sha256' })).present, false);
+});
diff --git a/js/tools/make_fixtures.py b/js/tools/make_fixtures.py
index 4fe640f..5f26c0e 100644
--- a/js/tools/make_fixtures.py
+++ b/js/tools/make_fixtures.py
@@ -429,6 +429,114 @@ def feed_cases():
             "refused": bad}
 
 
+# ---------------------------------------------------------------------------
+# Proofs: key inclusion and absence, prefixes, extensions
+# ---------------------------------------------------------------------------
+
+def proof_ops():
+    """A store that changes in two steps: `log/` only grows, other keys
+    change, so the second root extends the first under 'log/' and not
+    under ''."""
+    first = [("put", f"log/{i}", {"event": i}) for i in range(1, 6)] + [
+        ("put", "state/x", "old"), ("put", "state/y", [1, 2]), ("put", "é", "é"),
+        ("put", "😀", None), ("put", "users/alice", {"n": "Alice"}),
+        ("put", "users/al", 0.5), ("commit",)]
+    second = [("put", "log/6", {"event": 6}), ("put", "state/x", "new"),
+              ("delete", "users/al"), ("put", "zz", True), ("commit",)]
+    return first, second
+
+
+def proof_cases():
+    from recordstore import ABSENT, ProofError, verify_extension, verify_prefix, verify_proof
+
+    cases = []
+    first, second = proof_ops()
+    keys = ["log/3", "state/x", "users/alice", "users/al", "users/a", "users/alicex",
+            "é", "😀", "nope", "log/", "zzz"]
+    prefixes = ["", "log/", "users/", "users/al", "state/", "é", "zz", "x"]
+    for addressing in ("sha256", "swarm"):
+        blobs = new_store(addressing)
+        store = RecordStore(blobs)
+        empty = {"key": "anything", "proof": store.prove("anything", addressing=addressing)}
+        for op in first:
+            _apply(store, op)
+        root_a = store.root
+        for op in second:
+            _apply(store, op)
+        root_b = store.root
+        a = RecordStore.at(root_a, blobs)
+        b = RecordStore.at(root_b, blobs)
+
+        def key_proof(s, k):
+            p = s.prove(k, addressing=addressing)
+            v = verify_proof(p, s.root)
+            return {"key": k, "proof": p, "present": v is not ABSENT,
+                    "value": None if v is ABSENT else v}
+
+        def prefix_proof(s, pre):
+            p = s.prove_prefix(pre, addressing=addressing)
+            return {"prefix": pre, "proof": p, "records": sorted(verify_prefix(p, s.root).items())}
+
+        extensions = []
+        for base, later, pres in [(root_a, root_b, ["log/"]), (root_a, root_b, ["log/", "é", "😀"]),
+                                  (None, root_b, [""]), (root_b, root_b, [""]),
+                                  (root_a, root_b, [""]), (root_a, root_b, ["state/"]),
+                                  (root_a, root_b, ["users/"])]:
+            s = RecordStore.at(later, blobs)
+            entry = {"base": base, "root": later, "prefixes": pres,
+                     "extends": s.extends(base, pres)}
+            try:
+                p = s.prove_extension(base, pres, addressing=addressing)
+                entry["proof"] = p
+                entry["proven"] = list(verify_extension(p, base, later))
+            except ValueError as e:
+                entry["error"] = str(e)
+            extensions.append(entry)
+
+        refused = []
+        good = key_proof(b, "log/3")["proof"]
+        node0 = good["nodes"][0]
+        for name, envelope, against in [
+            ("a node byte changed", {**good, "nodes": [node0[:-2] + ("00" if node0[-2:] != "00" else "01")] + good["nodes"][1:]}, root_b),
+            ("the value changed", {**good, "value": good["value"][:-4] + "3030"}, root_b),
+            ("claims absence", {**good, "present": False, "value": None}, root_b),
+            ("about another root", good, root_a),
+            ("the last node dropped", {**good, "nodes": good["nodes"][:-1]}, root_b),
+        ]:
+            try:
+                verify_proof(envelope, against)
+                raise AssertionError(f"Python accepted a tampered proof: {name}")
+            except ProofError:
+                refused.append({"kind": "key", "name": name, "envelope": envelope, "root": against})
+        whole = b.prove_prefix("users/", addressing=addressing)
+        try:
+            verify_prefix({**whole, "nodes": whole["nodes"][1:]}, root_b)
+            raise AssertionError("Python accepted a prefix proof missing a node")
+        except ProofError:
+            refused.append({"kind": "prefix", "name": "a node missing",
+                            "envelope": {**whole, "nodes": whole["nodes"][1:]}, "root": root_b})
+        ext = b.prove_extension(root_a, ["log/"], addressing=addressing)
+        try:
+            verify_extension({**ext, "prefixes": [""]}, root_a, root_b)
+            raise AssertionError("Python accepted an extension proof for a wider prefix")
+        except ProofError:
+            refused.append({"kind": "extension", "name": "claims a wider prefix",
+                            "envelope": {**ext, "prefixes": [""]}, "base": root_a, "root": root_b})
+
+        cases.append({
+            "addressing": addressing,
+            "first": [_op_json(op) for op in first],
+            "second": [_op_json(op) for op in second],
+            "roots": {"a": root_a, "b": root_b},
+            "empty": empty,
+            "keys": [key_proof(s, k) for s in (a, b) for k in keys],
+            "prefixes": [prefix_proof(s, p) for s in (a, b) for p in prefixes],
+            "extensions": extensions,
+            "refused": refused,
+        })
+    return cases
+
+
 # ---------------------------------------------------------------------------
 
 def fixtures():
@@ -444,6 +552,7 @@ def fixtures():
         "merges.json": {"note": note, "resolver": "ours wins: DELETE if ours "
                         "is ABSENT, else ours", "cases": merge_cases()},
         "feeds.json": {"note": note, **feed_cases()},
+        "proofs.json": {"note": note, "cases": proof_cases()},
     }