+2
mise.toml
+2
mise.toml
+4
packages/utilities/mst/.gitignore
+4
packages/utilities/mst/.gitignore
-12
packages/utilities/mst/lib/errors.ts
-12
packages/utilities/mst/lib/errors.ts
···
18
18
super(`missing block in store; cid=${cid}` + (def ? `; type=${def}` : ``));
19
19
}
20
20
}
21
-
22
-
/**
23
-
* thrown when a block's decoded object doesn't match the expected type
24
-
*/
25
-
export class UnexpectedObjectError extends Error {
26
-
constructor(
27
-
public cid: string,
28
-
public def: string,
29
-
) {
30
-
super(`unexpected object in store; cid=${cid}; expected=${def}`);
31
-
}
32
-
}
+8
-8
packages/utilities/mst/lib/node-wrangler.ts
+8
-8
packages/utilities/mst/lib/node-wrangler.ts
···
4
4
import { NodeStore } from './node-store.js';
5
5
6
6
/**
7
-
* array helper: replaces element at index with a new value
7
+
* replaces element at index with a new value
8
8
*/
9
9
const replaceAt = <T>(arr: readonly T[], index: number, value: T): readonly T[] => {
10
-
return [...arr.slice(0, index), value, ...arr.slice(index + 1)];
10
+
return arr.with(index, value);
11
11
};
12
12
13
13
/**
14
-
* array helper: inserts element at index
14
+
* inserts element at index
15
15
*/
16
16
const insertAt = <T>(arr: readonly T[], index: number, value: T): readonly T[] => {
17
-
return [...arr.slice(0, index), value, ...arr.slice(index)];
17
+
return arr.toSpliced(index, 0, value);
18
18
};
19
19
20
20
/**
21
-
* array helper: removes element at index
21
+
* removes element at index
22
22
*/
23
23
const removeAt = <T>(arr: readonly T[], index: number): readonly T[] => {
24
-
return [...arr.slice(0, index), ...arr.slice(index + 1)];
24
+
return arr.toSpliced(index, 1);
25
25
};
26
26
27
27
/**
···
31
31
* the external APIs take a CID (the MST root) and return a CID (the new root),
32
32
* while storing any newly created nodes in the NodeStore.
33
33
*
34
-
* neither method should ever fail - deleting a node that doesn't exist is a nop,
35
-
* and adding the same node twice with the same value is also a nop. Callers
34
+
* neither method should ever fail - deleting a node that doesn't exist is a noop,
35
+
* and adding the same node twice with the same value is also a nop. callers
36
36
* can detect these cases by seeing if the initial and final CIDs changed.
37
37
*/
38
38
export class NodeWrangler {
+2
packages/utilities/mst/lib/node.ts
+2
packages/utilities/mst/lib/node.ts
+30
-45
packages/utilities/mst/lib/stores.ts
+30
-45
packages/utilities/mst/lib/stores.ts
···
1
-
import * as CBOR from '@atcute/cbor';
2
-
3
1
import { deleteMany, setMany, type BlockMap } from './blockmap.js';
4
-
import { MissingBlockError, UnexpectedObjectError } from './errors.js';
5
2
6
3
/**
7
4
* a read-only interface for retrieving blocks by their CID
···
132
129
* all writes go to the upper store only
133
130
*/
134
131
export class OverlayBlockStore implements BlockStore {
135
-
/** the writable upper layer store */
132
+
/** writable upper layer store */
136
133
upper: BlockStore;
137
-
/** the read-only lower layer store */
134
+
/** read-only lower layer store */
138
135
lower: ReadonlyBlockStore;
139
136
140
137
/**
···
195
192
}
196
193
197
194
/**
198
-
* reads and decodes a block, validating it matches the expected type
199
-
* @param store block store to read from
200
-
* @param cid CID of the block to read
201
-
* @param def schema definition with name and validation function
202
-
* @returns the decoded and validated object
203
-
* @throws {MissingBlockError} if block is not found
204
-
* @throws {UnexpectedObjectError} if block doesn't match expected type
195
+
* a read-only block store wrapper that tracks all get() accesses
196
+
* useful for collecting proof nodes during MST operations
205
197
*/
206
-
export const readObject = async <T>(store: ReadonlyBlockStore, cid: string, def: CheckDef<T>): Promise<T> => {
207
-
const bytes = await store.get(cid);
208
-
if (bytes === null) {
209
-
throw new MissingBlockError(cid, def.name);
210
-
}
198
+
export class LoggingBlockStore implements ReadonlyBlockStore {
199
+
/** block store being proxied */
200
+
readonly wrapped: ReadonlyBlockStore;
201
+
/** set of CIDs that were accessed via get() or getMany() */
202
+
readonly accessed = new Set<string>();
211
203
212
-
const decoded = CBOR.decode(bytes);
213
-
if (!def.check(decoded)) {
214
-
throw new UnexpectedObjectError(cid, def.name);
204
+
/**
205
+
* creates a new logging block store wrapper
206
+
* @param store the block store to wrap
207
+
*/
208
+
constructor(store: ReadonlyBlockStore) {
209
+
this.wrapped = store;
215
210
}
216
211
217
-
return decoded;
218
-
};
212
+
async get(cid: string): Promise<Uint8Array<ArrayBuffer> | null> {
213
+
this.accessed.add(cid);
219
214
220
-
/**
221
-
* reads and decodes a block without type validation
222
-
* @param store block store to read from
223
-
* @param cid CID of the block to read
224
-
* @returns the decoded object
225
-
* @throws {MissingBlockError} if block is not found
226
-
*/
227
-
export const readRecord = async (store: ReadonlyBlockStore, cid: string): Promise<unknown> => {
228
-
const bytes = await store.get(cid);
229
-
if (bytes === null) {
230
-
throw new MissingBlockError(cid, undefined);
215
+
return this.wrapped.get(cid);
231
216
}
232
217
233
-
const decoded = CBOR.decode(bytes);
218
+
async getMany(cids: string[]): Promise<{ found: BlockMap; missing: string[] }> {
219
+
const accessed = this.accessed;
234
220
235
-
return decoded;
236
-
};
221
+
for (const cid of cids) {
222
+
accessed.add(cid);
223
+
}
237
224
238
-
/**
239
-
* defines a type validator for use with readObject
240
-
* combines a human-readable type name with a type guard function
241
-
*/
242
-
export interface CheckDef<T> {
243
-
/** human-readable name of the expected type */
244
-
name: string;
245
-
/** type guard function to validate the decoded value */
246
-
check: (value: unknown) => value is T;
225
+
return this.wrapped.getMany(cids);
226
+
}
227
+
228
+
async has(cid: string): Promise<boolean> {
229
+
// has() doesn't count as an access for proof purposes
230
+
return this.wrapped.has(cid);
231
+
}
247
232
}
+157
-114
packages/utilities/mst/lib/test-suite.test.ts
+157
-114
packages/utilities/mst/lib/test-suite.test.ts
···
1
-
import { readFileSync, readdirSync, statSync } from 'node:fs';
2
-
import { join } from 'node:path';
3
-
import { describe, expect, it } from 'vitest';
1
+
import { beforeAll, describe, expect, it } from 'vitest';
2
+
import * as v from 'valibot';
3
+
4
+
import * as fs from 'node:fs/promises';
5
+
import * as path from 'node:path';
4
6
5
7
import { fromUint8Array } from '@atcute/car/v4/car-reader';
6
8
import * as CID from '@atcute/cid';
7
9
10
+
import { setMany } from './blockmap.js';
8
11
import { DeltaType, mstDiff, recordDiff } from './diff.js';
9
12
import { NodeStore } from './node-store.js';
10
-
import { MemoryBlockStore } from './stores.js';
13
+
import { NodeWrangler } from './node-wrangler.js';
14
+
import { buildExclusionProof, buildInclusionProof } from './proof.js';
15
+
import {
16
+
LoggingBlockStore,
17
+
MemoryBlockStore,
18
+
OverlayBlockStore,
19
+
ReadonlyMemoryBlockStore,
20
+
} from './stores.js';
11
21
12
-
interface MstDiffTestCase {
13
-
$type: 'mst-diff';
14
-
description: string;
15
-
inputs: {
16
-
mst_a: string;
17
-
mst_b: string;
18
-
};
19
-
results: {
20
-
created_nodes: string[];
21
-
deleted_nodes: string[];
22
-
record_ops: Array<{
23
-
rpath: string;
24
-
old_value: string | null;
25
-
new_value: string | null;
26
-
}>;
27
-
proof_nodes: string[];
28
-
inductive_proof_nodes: string[];
29
-
firehose_cids: string | string[];
30
-
};
31
-
}
22
+
const mstDiffTestCaseSchema = v.object({
23
+
$type: v.literal('mst-diff'),
24
+
description: v.string(),
25
+
inputs: v.object({
26
+
mst_a: v.string(),
27
+
mst_b: v.string(),
28
+
}),
29
+
results: v.object({
30
+
created_nodes: v.array(v.string()),
31
+
deleted_nodes: v.array(v.string()),
32
+
record_ops: v.array(
33
+
v.object({
34
+
rpath: v.string(),
35
+
old_value: v.nullable(v.string()),
36
+
new_value: v.nullable(v.string()),
37
+
}),
38
+
),
39
+
proof_nodes: v.array(v.string()),
40
+
inductive_proof_nodes: v.array(v.string()),
41
+
}),
42
+
});
43
+
44
+
type MstDiffTestCase = v.InferOutput<typeof mstDiffTestCaseSchema>;
45
+
46
+
const testSuiteRoot = path.join(__dirname, '../mst-test-suite');
32
47
33
48
/**
34
49
* Load a CAR file into a MemoryBlockStore and extract the root CID
35
50
*/
36
-
const loadCar = (carPath: string): { store: MemoryBlockStore; root: string } => {
37
-
const testSuiteRoot = join(__dirname, '..', '.research', 'mst-test-suite');
38
-
const fullPath = join(testSuiteRoot, carPath);
39
-
const carBytes = readFileSync(fullPath);
51
+
const loadCar = async (relname: string): Promise<{ store: ReadonlyMemoryBlockStore; root: string }> => {
52
+
const filename = path.join(testSuiteRoot, relname);
53
+
const bytes = await fs.readFile(filename);
40
54
41
-
const car = fromUint8Array(carBytes);
55
+
const car = fromUint8Array(bytes);
42
56
const store = new MemoryBlockStore();
43
57
44
-
// Load all blocks from CAR into the store
45
58
for (const entry of car) {
46
59
const cidStr = CID.toCidLink(entry.cid).$link;
47
-
store.blocks.set(cidStr, entry.bytes);
60
+
store.blocks.set(cidStr, entry.bytes as Uint8Array<ArrayBuffer>);
48
61
}
49
62
50
-
// Extract root CID from CAR header
51
63
if (car.roots.length !== 1) {
52
-
throw new Error(`Expected exactly 1 root in CAR, got ${car.roots.length}`);
64
+
throw new Error(`expected exactly 1 root in CAR, got ${car.roots.length}`);
53
65
}
54
66
55
67
const root = car.roots[0].$link;
56
68
return { store, root };
57
69
};
58
70
59
-
/**
60
-
* Recursively find all .json test files in a directory
61
-
*/
62
-
const findTestFiles = (dir: string): string[] => {
63
-
const results: string[] = [];
64
-
const entries = readdirSync(dir);
65
-
66
-
for (const entry of entries) {
67
-
const fullPath = join(dir, entry);
68
-
const stat = statSync(fullPath);
71
+
const testCases = await (async () => {
72
+
const testsDir = path.join(testSuiteRoot, 'tests');
69
73
70
-
if (stat.isDirectory()) {
71
-
results.push(...findTestFiles(fullPath));
72
-
} else if (entry.endsWith('.json')) {
73
-
results.push(fullPath);
74
-
}
75
-
}
74
+
const testCases: Array<{ path: string; description: string; testCase: MstDiffTestCase }> = [];
76
75
77
-
return results;
78
-
};
79
-
80
-
/**
81
-
* Load all test cases from the test suite
82
-
*/
83
-
const loadTestCases = (): Array<{ path: string; testCase: MstDiffTestCase }> => {
84
-
const testSuiteRoot = join(__dirname, '..', '.research', 'mst-test-suite');
85
-
const testsDir = join(testSuiteRoot, 'tests');
86
-
const testFiles = findTestFiles(testsDir);
76
+
for await (const name of fs.glob('**/*.json', { cwd: testsDir })) {
77
+
const filename = path.join(testsDir, name);
87
78
88
-
const testCases: Array<{ path: string; testCase: MstDiffTestCase }> = [];
79
+
const raw = await fs.readFile(filename, 'utf-8');
80
+
const json = JSON.parse(raw);
89
81
90
-
for (const filePath of testFiles) {
91
-
const content = readFileSync(filePath, 'utf-8');
92
-
const testCase = JSON.parse(content) as MstDiffTestCase;
82
+
const testCase = v.parse(mstDiffTestCaseSchema, json);
93
83
94
-
if (testCase.$type === 'mst-diff') {
95
-
testCases.push({ path: filePath, testCase });
96
-
}
84
+
testCases.push({
85
+
path: filename,
86
+
description: testCase.description.replace(`procedurally generated MST diff test case `, ``),
87
+
testCase,
88
+
});
97
89
}
98
90
99
91
return testCases;
100
-
};
92
+
})();
101
93
102
94
describe('MST Test Suite', () => {
103
-
const allTestCases = loadTestCases();
95
+
describe.each(testCases)('$description', ({ testCase }) => {
96
+
let storeA: ReadonlyMemoryBlockStore;
97
+
let rootA: string;
104
98
105
-
// Run all test cases
106
-
const testCases = allTestCases;
99
+
let storeB: ReadonlyMemoryBlockStore;
100
+
let rootB: string;
107
101
108
-
it(`should have loaded test cases (${testCases.length} total)`, () => {
109
-
expect(testCases.length).toBeGreaterThan(1000); // Should have 16k+ tests
110
-
});
102
+
beforeAll(async () => {
103
+
({ store: storeA, root: rootA } = await loadCar(testCase.inputs.mst_a));
104
+
({ store: storeB, root: rootB } = await loadCar(testCase.inputs.mst_b));
105
+
});
111
106
112
-
describe.each(testCases)('$testCase.description', ({ testCase }) => {
113
-
it('should compute correct mstDiff', async () => {
114
-
// Load both CARs
115
-
const { store: storeA, root: rootA } = loadCar(testCase.inputs.mst_a);
116
-
const { store: storeB, root: rootB } = loadCar(testCase.inputs.mst_b);
117
-
118
-
// Create NodeStores (combine both block stores for access to all blocks)
119
-
// We need an overlay approach since diff needs to read from both trees
107
+
it('computes the correct mstDiff', async () => {
120
108
const combinedStore = new MemoryBlockStore();
121
-
for (const [cid, bytes] of storeA.blocks) {
122
-
combinedStore.blocks.set(cid, bytes);
123
-
}
124
-
for (const [cid, bytes] of storeB.blocks) {
125
-
combinedStore.blocks.set(cid, bytes);
126
-
}
109
+
setMany(combinedStore.blocks, storeA.blocks);
110
+
setMany(combinedStore.blocks, storeB.blocks);
127
111
128
112
const nodeStore = new NodeStore(combinedStore);
129
113
130
-
// Run mstDiff
131
114
const [createdNodes, deletedNodes] = await mstDiff(nodeStore, rootA, rootB);
132
115
133
-
// Compare created_nodes (as sets, order doesn't matter)
134
116
const expectedCreated = new Set(testCase.results.created_nodes);
135
117
expect(createdNodes).toEqual(expectedCreated);
136
118
137
-
// Compare deleted_nodes (as sets, order doesn't matter)
138
119
const expectedDeleted = new Set(testCase.results.deleted_nodes);
139
120
expect(deletedNodes).toEqual(expectedDeleted);
140
121
});
141
122
142
-
it('should compute correct recordDiff', async () => {
143
-
// Load both CARs
144
-
const { store: storeA, root: rootA } = loadCar(testCase.inputs.mst_a);
145
-
const { store: storeB, root: rootB } = loadCar(testCase.inputs.mst_b);
146
-
147
-
// Create combined NodeStore
123
+
it('computes the correct recordDiff', async () => {
148
124
const combinedStore = new MemoryBlockStore();
149
-
for (const [cid, bytes] of storeA.blocks) {
150
-
combinedStore.blocks.set(cid, bytes);
151
-
}
152
-
for (const [cid, bytes] of storeB.blocks) {
153
-
combinedStore.blocks.set(cid, bytes);
154
-
}
125
+
setMany(combinedStore.blocks, storeA.blocks);
126
+
setMany(combinedStore.blocks, storeB.blocks);
155
127
156
128
const nodeStore = new NodeStore(combinedStore);
157
129
158
-
// Run mstDiff and recordDiff
159
130
const [createdNodes, deletedNodes] = await mstDiff(nodeStore, rootA, rootB);
160
131
161
-
const deltas = [];
162
-
for await (const delta of recordDiff(nodeStore, createdNodes, deletedNodes)) {
163
-
deltas.push(delta);
164
-
}
132
+
const deltas = await Array.fromAsync(recordDiff(nodeStore, createdNodes, deletedNodes));
133
+
deltas.sort((a, b) => +(a.path > b.path) - +(a.path < b.path));
165
134
166
-
// Sort both actual and expected by rpath for comparison
167
-
const sortedDeltas = deltas.sort((a, b) => a.path.localeCompare(b.path));
168
-
const sortedExpected = [...testCase.results.record_ops].sort((a, b) => a.rpath.localeCompare(b.rpath));
135
+
const expectance = testCase.results.record_ops.toSorted(
136
+
(a, b) => +(a.rpath > b.rpath) - +(a.rpath < b.rpath),
137
+
);
169
138
170
-
expect(sortedDeltas.length).toBe(sortedExpected.length);
139
+
expect(deltas.length).toBe(expectance.length);
171
140
172
-
for (let i = 0; i < sortedDeltas.length; i++) {
173
-
const actual = sortedDeltas[i];
174
-
const expected = sortedExpected[i];
141
+
for (let idx = 0, len = deltas.length; idx < len; idx++) {
142
+
const actual = deltas[idx];
143
+
const expected = expectance[idx];
175
144
176
145
expect(actual.path).toBe(expected.rpath);
177
146
expect(actual.priorValue?.$link ?? null).toBe(expected.old_value);
···
186
155
expect(actual.deltaType).toBe(DeltaType.UPDATED);
187
156
}
188
157
}
158
+
});
159
+
160
+
it('computes the correct proof_nodes', async () => {
161
+
// create combined store
162
+
const combinedStore = new MemoryBlockStore();
163
+
setMany(combinedStore.blocks, storeA.blocks);
164
+
setMany(combinedStore.blocks, storeB.blocks);
165
+
166
+
const nodeStore = new NodeStore(combinedStore);
167
+
168
+
// collect proof nodes for all record operations
169
+
const proofNodes = new Set<string>();
170
+
171
+
for (const op of testCase.results.record_ops) {
172
+
let proof: Set<string>;
173
+
174
+
if (op.old_value === null) {
175
+
// CREATED: inclusion proof for new record in rootB
176
+
proof = await buildInclusionProof(nodeStore, rootB, op.rpath);
177
+
} else if (op.new_value === null) {
178
+
// DELETED: exclusion proof in rootB
179
+
proof = await buildExclusionProof(nodeStore, rootB, op.rpath);
180
+
} else {
181
+
// UPDATED: inclusion proof for updated record in rootB
182
+
proof = await buildInclusionProof(nodeStore, rootB, op.rpath);
183
+
}
184
+
185
+
// add all proof nodes to the set
186
+
for (const cid of proof) {
187
+
proofNodes.add(cid);
188
+
}
189
+
}
190
+
191
+
// compare against expected proof_nodes (as sets, order doesn't matter)
192
+
const expectedProofNodes = new Set(testCase.results.proof_nodes);
193
+
expect(proofNodes).toEqual(expectedProofNodes);
194
+
});
195
+
196
+
it('computes the correct inductive_proof_nodes', async () => {
197
+
// create combined store
198
+
const combinedStore = new MemoryBlockStore();
199
+
setMany(combinedStore.blocks, storeA.blocks);
200
+
setMany(combinedStore.blocks, storeB.blocks);
201
+
202
+
// inductive proofs: nodes that get READ when applying ops in REVERSE order
203
+
// this is used for MST operation inversion (verifying B→A instead of A→B)
204
+
205
+
const loggingStore = new LoggingBlockStore(combinedStore);
206
+
207
+
const overlayStore = new OverlayBlockStore(new MemoryBlockStore(), loggingStore);
208
+
const nodeStore = new NodeStore(overlayStore);
209
+
const wrangler = new NodeWrangler(nodeStore);
210
+
211
+
// start from rootB and apply operations in REVERSE order
212
+
let currentRoot = rootB;
213
+
const reversedOps = testCase.results.record_ops.toReversed();
214
+
215
+
for (const op of reversedOps) {
216
+
if (op.old_value === null) {
217
+
// was CREATE, reverse it with DELETE
218
+
currentRoot = await wrangler.deleteRecord(currentRoot, op.rpath);
219
+
} else {
220
+
// was UPDATE or DELETE, reverse with PUT of old value
221
+
currentRoot = await wrangler.putRecord(currentRoot, op.rpath, { $link: op.old_value });
222
+
}
223
+
}
224
+
225
+
// after reversing all operations, we should end up back at rootA
226
+
expect(currentRoot).toBe(rootA);
227
+
228
+
// the blocks that were accessed (read) are the inductive proof nodes
229
+
const inductiveProofNodes = loggingStore.accessed;
230
+
const expectedInductiveProofNodes = new Set(testCase.results.inductive_proof_nodes);
231
+
expect(inductiveProofNodes).toEqual(expectedInductiveProofNodes);
189
232
});
190
233
});
191
234
});
packages/utilities/mst/mst-test-suite/cars/exhaustive/.gitkeep
packages/utilities/mst/mst-test-suite/cars/exhaustive/.gitkeep
This is a binary file and will not be displayed.
+7
packages/utilities/mst/mst-test-suite/pyproject.toml
+7
packages/utilities/mst/mst-test-suite/pyproject.toml
+228
packages/utilities/mst/mst-test-suite/scripts/generate_exhaustive_cars.py
+228
packages/utilities/mst/mst-test-suite/scripts/generate_exhaustive_cars.py
···
1
+
from typing import BinaryIO, Optional
2
+
import json
3
+
4
+
from atmst.mst.node import MSTNode
5
+
from atmst.mst.node_store import NodeStore
6
+
from atmst.mst.node_wrangler import NodeWrangler
7
+
from atmst.mst.node_walker import NodeWalker
8
+
from atmst.mst.diff import very_slow_mst_diff, record_diff
9
+
from atmst.blockstore import MemoryBlockStore, OverlayBlockStore, BlockStore
10
+
from atmst.blockstore.car_file import encode_varint
11
+
from atmst.mst import proof
12
+
import cbrrr
13
+
from cbrrr import CID
14
+
15
+
16
+
class LoggingBlockStoreWrapper(BlockStore):
17
+
def __init__(self, bs: BlockStore):
18
+
self.bs = bs
19
+
self.gets = set()
20
+
21
+
def put_block(self, key: bytes, value: bytes) -> None:
22
+
self.bs.put_block(key, value)
23
+
24
+
def get_block(self, key: bytes) -> bytes:
25
+
self.gets.add(key)
26
+
return self.bs.get_block(key)
27
+
28
+
def del_block(self, key: bytes) -> None:
29
+
self.bs.del_block(key)
30
+
31
+
32
+
"""
33
+
class LoggingNodeStore(NodeStore):
34
+
def __init__(self, bs):
35
+
self.read_cids = set()
36
+
self.stored_cids = set()
37
+
super().__init__(bs)
38
+
39
+
def get_node(self, cid: Optional[CID]) -> MSTNode:
40
+
if cid is None:
41
+
self.read_cids.add(MSTNode.empty_root().cid)
42
+
else:
43
+
self.read_cids.add(cid)
44
+
return super().get_node(cid)
45
+
46
+
def stored_node(self, node: MSTNode) -> MSTNode:
47
+
self.stored_cids.add(node.cid)
48
+
return super().stored_node(node)
49
+
"""
50
+
51
+
52
+
class CarWriter:
53
+
def __init__(self, stream: BinaryIO, root: cbrrr.CID) -> None:
54
+
self.stream = stream
55
+
header_bytes = cbrrr.encode_dag_cbor({"version": 1, "roots": [root]})
56
+
stream.write(encode_varint(len(header_bytes)))
57
+
stream.write(header_bytes)
58
+
59
+
def write_block(self, cid: cbrrr.CID, value: bytes):
60
+
cid_bytes = bytes(cid)
61
+
self.stream.write(encode_varint(len(cid_bytes) + len(value)))
62
+
self.stream.write(cid_bytes)
63
+
self.stream.write(value)
64
+
65
+
66
+
keys = []
67
+
key_heights = [
68
+
0,
69
+
1,
70
+
0,
71
+
2,
72
+
0,
73
+
1,
74
+
0,
75
+
] # if all these keys are added to a MST, it'll form a perfect binary tree.
76
+
i = 0
77
+
for height in key_heights:
78
+
while True:
79
+
key = f"k/{i:02d}"
80
+
i += 1
81
+
if MSTNode.key_height(key) == height:
82
+
keys.append(key)
83
+
break
84
+
85
+
vals = [
86
+
CID.cidv1_dag_cbor_sha256_32_from(
87
+
cbrrr.encode_dag_cbor({"$type": "mst-test-data", "value_for": k})
88
+
)
89
+
for k in keys
90
+
]
91
+
92
+
val_for_key = dict(zip(keys, vals))
93
+
94
+
print(keys)
95
+
print(vals)
96
+
97
+
# we can reuse these
98
+
bs = MemoryBlockStore()
99
+
ns = NodeStore(bs)
100
+
wrangler = NodeWrangler(ns)
101
+
102
+
roots = []
103
+
104
+
for i in range(2 ** len(keys)):
105
+
filename = f"./cars/exhaustive/exhaustive_{i:03d}.car"
106
+
root = ns.get_node(None).cid
107
+
for j in range(len(keys)):
108
+
if (i >> j) & 1:
109
+
# filename += f"_{keys[j]}h{key_heights[j]}"
110
+
root = wrangler.put_record(root, keys[j], vals[j])
111
+
# filename += ".car"
112
+
print(i, filename)
113
+
114
+
car_blocks = []
115
+
for node in NodeWalker(ns, root).iter_nodes():
116
+
car_blocks.append((node.cid, node.serialised))
117
+
118
+
assert len(set(cid for cid, val in car_blocks)) == len(car_blocks) # no dupes
119
+
120
+
with open(filename, "wb") as carfile:
121
+
car = CarWriter(carfile, root)
122
+
for cid, val in sorted(car_blocks, key=lambda x: bytes(x[0])):
123
+
car.write_block(cid, val)
124
+
125
+
roots.append(root)
126
+
127
+
# collecting these stats just for the sake of curiosity
128
+
# identical_proof_and_creation_count = 0
129
+
# proof_superset_of_creation_count = 0
130
+
# creation_superset_of_proof_count = 0
131
+
inversion_needs_extra_blocks = 0
132
+
clusion_proof_nodes_not_in_inversion_proof = 0
133
+
134
+
# generate exhaustive test cases
135
+
for ai, root_a in enumerate(roots):
136
+
for bi, root_b in enumerate(roots):
137
+
filename = f"./tests/diff/exhaustive/exhaustive_{ai:03d}_{bi:03d}.json"
138
+
print(filename)
139
+
car_a = f"./cars/exhaustive/exhaustive_{ai:03d}.car"
140
+
car_b = f"./cars/exhaustive/exhaustive_{bi:03d}.car"
141
+
created_nodes, deleted_nodes = very_slow_mst_diff(ns, root_a, root_b)
142
+
record_ops = []
143
+
proof_nodes = set()
144
+
no_deletions = True
145
+
for delta in record_diff(ns, created_nodes, deleted_nodes):
146
+
record_ops.append(
147
+
{
148
+
"rpath": delta.path,
149
+
"old_value": None
150
+
if delta.prior_value is None
151
+
else delta.prior_value.encode(),
152
+
"new_value": None
153
+
if delta.later_value is None
154
+
else delta.later_value.encode(),
155
+
}
156
+
)
157
+
if delta.later_value is None: # deletion
158
+
proof_nodes.update(proof.build_exclusion_proof(ns, root_b, delta.path))
159
+
no_deletions = False
160
+
else: # update or create
161
+
proof_nodes.update(proof.build_inclusion_proof(ns, root_b, delta.path))
162
+
163
+
if no_deletions: # commits with no deletions are more well-behaved
164
+
assert proof_nodes.issubset(created_nodes)
165
+
166
+
# my inductive-proof-generation logic is ops order sensitive, so we do the sort beforehand
167
+
# TODO: maybe "deletes first" or similar produces smaller proofs on average?
168
+
record_ops.sort(key=lambda x: x["rpath"])
169
+
170
+
# figure out which blocks are required for inductive proofs.
171
+
# the idea here is that we use an overlay blockstore and log every "get" that has to fall thru to the lower layer.
172
+
# those gets are therefore the blocks required for a stateless consumer to verify the proof.
173
+
upper = MemoryBlockStore()
174
+
lbs = LoggingBlockStoreWrapper(bs)
175
+
lns = NodeStore(OverlayBlockStore(upper, lbs))
176
+
lnw = NodeWrangler(lns)
177
+
proof_root = root_b
178
+
for op in record_ops[
179
+
::-1
180
+
]: # while the order does not effect the final root CID, it does affect the set of CIDs that fall thru
181
+
if op["old_value"] is None:
182
+
proof_root = lnw.del_record(proof_root, op["rpath"])
183
+
else:
184
+
proof_root = lnw.put_record(
185
+
proof_root, op["rpath"], val_for_key[op["rpath"]]
186
+
)
187
+
assert proof_root == root_a # we're back to where we started
188
+
inductive_proof_nodes = set(CID(cid) for cid in lbs.gets)
189
+
190
+
if inductive_proof_nodes - (created_nodes | proof_nodes):
191
+
# print(delta)
192
+
inversion_needs_extra_blocks += 1
193
+
194
+
if proof_nodes - inductive_proof_nodes:
195
+
clusion_proof_nodes_not_in_inversion_proof += 1
196
+
197
+
# if proof_nodes == created_nodes:
198
+
# identical_proof_and_creation_count += 1
199
+
# if proof_nodes.issuperset(created_nodes):
200
+
# proof_superset_of_creation_count += 1
201
+
# if created_nodes.issuperset(proof_nodes):
202
+
# creation_superset_of_proof_count += 1
203
+
204
+
testcase = {
205
+
"$type": "mst-diff",
206
+
"description": f"procedurally generated MST diff test case between MST {ai} and {bi}",
207
+
"inputs": {"mst_a": car_a, "mst_b": car_b},
208
+
"results": {
209
+
"created_nodes": sorted([cid.encode() for cid in created_nodes]),
210
+
"deleted_nodes": sorted([cid.encode() for cid in deleted_nodes]),
211
+
"record_ops": record_ops, # these were sorted earlier
212
+
"proof_nodes": sorted([cid.encode() for cid in proof_nodes]),
213
+
"inductive_proof_nodes": sorted(
214
+
[cid.encode() for cid in inductive_proof_nodes]
215
+
),
216
+
"firehose_cids": "TODO",
217
+
},
218
+
}
219
+
with open(filename, "w") as jsonfile:
220
+
json.dump(testcase, jsonfile, indent="\t")
221
+
222
+
# print("identical_proof_and_creation_count", identical_proof_and_creation_count / (len(roots)**2)) # 0.75
223
+
# print("proof_superset_of_creation_count", proof_superset_of_creation_count / (len(roots)**2)) # 0.84
224
+
# print("creation_superset_of_proof_count", creation_superset_of_proof_count / (len(roots)**2)) # 0.91
225
+
print(
226
+
"inversion_needs_extra_blocks", inversion_needs_extra_blocks / (len(roots) ** 2)
227
+
) # 0.04
228
+
print(clusion_proof_nodes_not_in_inversion_proof)
packages/utilities/mst/mst-test-suite/tests/diff/exhaustive/.gitkeep
packages/utilities/mst/mst-test-suite/tests/diff/exhaustive/.gitkeep
This is a binary file and will not be displayed.
+81
packages/utilities/mst/mst-test-suite/uv.lock
+81
packages/utilities/mst/mst-test-suite/uv.lock
···
1
+
version = 1
2
+
revision = 3
3
+
requires-python = ">=3.11"
4
+
5
+
[[package]]
6
+
name = "atmst"
7
+
version = "0.0.6"
8
+
source = { registry = "https://pypi.org/simple" }
9
+
dependencies = [
10
+
{ name = "cbrrr" },
11
+
{ name = "lru-dict" },
12
+
{ name = "more-itertools" },
13
+
]
14
+
sdist = { url = "https://files.pythonhosted.org/packages/47/7a/2cca04368b664d372473504615e37150466ecc796dff018504d8daf5de6d/atmst-0.0.6.tar.gz", hash = "sha256:bdc3ada3f234e28dada73f50cd40359534f99208436e831fe035f6fc7c7b188e", size = 18577, upload-time = "2024-12-21T11:37:20.15Z" }
15
+
wheels = [
16
+
{ url = "https://files.pythonhosted.org/packages/f1/ec/d743d809cadfaae230ebed08c00f63a68f1bfe82042f74ea98c51965ed8f/atmst-0.0.6-py3-none-any.whl", hash = "sha256:e63801225f31b602a3aacfe73360561fa5f488adb1a56d64e0746d462981ecff", size = 19744, upload-time = "2024-12-21T11:37:17.804Z" },
17
+
]
18
+
19
+
[[package]]
20
+
name = "cbrrr"
21
+
version = "1.0.1"
22
+
source = { registry = "https://pypi.org/simple" }
23
+
sdist = { url = "https://files.pythonhosted.org/packages/a3/2e/321b68b2b12c99864f0872feb8ce80f03483b59c113730f7cb4465a49e0e/cbrrr-1.0.1.tar.gz", hash = "sha256:2dc5f78a71b67849e1b364053819053f03cd2032797ce6adbe1d02e8d602698c", size = 17503, upload-time = "2024-11-16T22:59:41.164Z" }
24
+
25
+
[[package]]
26
+
name = "lru-dict"
27
+
version = "1.3.0"
28
+
source = { registry = "https://pypi.org/simple" }
29
+
sdist = { url = "https://files.pythonhosted.org/packages/96/e3/42c87871920602a3c8300915bd0292f76eccc66c38f782397acbf8a62088/lru-dict-1.3.0.tar.gz", hash = "sha256:54fd1966d6bd1fcde781596cb86068214edeebff1db13a2cea11079e3fd07b6b", size = 13123, upload-time = "2023-11-06T01:40:12.951Z" }
30
+
wheels = [
31
+
{ url = "https://files.pythonhosted.org/packages/a8/c9/6fac0cb67160f0efa3cc76a6a7d04d5e21a516eeb991ebba08f4f8f01ec5/lru_dict-1.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:20c595764695d20bdc3ab9b582e0cc99814da183544afb83783a36d6741a0dac", size = 17750, upload-time = "2023-11-06T01:38:52.667Z" },
32
+
{ url = "https://files.pythonhosted.org/packages/61/14/f90dee4bc547ae266dbeffd4e11611234bb6af511dea48f3bc8dac1de478/lru_dict-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d9b30a8f50c3fa72a494eca6be5810a1b5c89e4f0fda89374f0d1c5ad8d37d51", size = 11055, upload-time = "2023-11-06T01:38:53.798Z" },
33
+
{ url = "https://files.pythonhosted.org/packages/4e/63/a0ae20525f9d52f62ac0def47935f8a2b3b6fcd2c145218b9a27fc1fb910/lru_dict-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9710737584650a4251b9a566cbb1a86f83437adb209c9ba43a4e756d12faf0d7", size = 11330, upload-time = "2023-11-06T01:38:54.847Z" },
34
+
{ url = "https://files.pythonhosted.org/packages/e9/c6/8c2b81b61e5206910c81b712500736227289aefe4ccfb36137aa21807003/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b84c321ae34f2f40aae80e18b6fa08b31c90095792ab64bb99d2e385143effaa", size = 31793, upload-time = "2023-11-06T01:38:56.163Z" },
35
+
{ url = "https://files.pythonhosted.org/packages/f9/d7/af9733f94df67a2e9e31ef47d4c41aff1836024f135cdbda4743eb628452/lru_dict-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eed24272b4121b7c22f234daed99899817d81d671b3ed030c876ac88bc9dc890", size = 33090, upload-time = "2023-11-06T01:38:57.091Z" },
36
+
{ url = "https://files.pythonhosted.org/packages/5b/6e/5b09b069a70028bcf05dbdc57a301fbe8b3bafecf916f2ed5a3065c79a71/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bd13af06dab7c6ee92284fd02ed9a5613a07d5c1b41948dc8886e7207f86dfd", size = 29795, upload-time = "2023-11-06T01:38:58.278Z" },
37
+
{ url = "https://files.pythonhosted.org/packages/21/92/4690daefc2602f7c3429ecf54572d37a9e3c372d370344d2185daa4d5ecc/lru_dict-1.3.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1efc59bfba6aac33684d87b9e02813b0e2445b2f1c444dae2a0b396ad0ed60c", size = 31586, upload-time = "2023-11-06T01:38:59.363Z" },
38
+
{ url = "https://files.pythonhosted.org/packages/3c/67/0a29a91087196b02f278d8765120ee4e7486f1f72a4c505fd1cd3109e627/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cfaf75ac574447afcf8ad998789071af11d2bcf6f947643231f692948839bd98", size = 36662, upload-time = "2023-11-06T01:39:00.795Z" },
39
+
{ url = "https://files.pythonhosted.org/packages/36/54/8d56c514cd2333b652bd44c8f1962ab986cbe68e8ad7258c9e0f360cddb6/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c95f8751e2abd6f778da0399c8e0239321d560dbc58cb063827123137d213242", size = 35118, upload-time = "2023-11-06T01:39:01.883Z" },
40
+
{ url = "https://files.pythonhosted.org/packages/f5/9a/c7a175d10d503b86974cb07141ca175947145dd1c7370fcda86fbbcaf326/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:abd0c284b26b5c4ee806ca4f33ab5e16b4bf4d5ec9e093e75a6f6287acdde78e", size = 38198, upload-time = "2023-11-06T01:39:03.306Z" },
41
+
{ url = "https://files.pythonhosted.org/packages/fd/59/2e5086c8e8a05a7282a824a2a37e3c45cd5714e7b83d8bc0267cb3bb5b4f/lru_dict-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2a47740652b25900ac5ce52667b2eade28d8b5fdca0ccd3323459df710e8210a", size = 36542, upload-time = "2023-11-06T01:39:04.751Z" },
42
+
{ url = "https://files.pythonhosted.org/packages/12/52/80d0a06e5f45fe7c278dd662da6ea5b39f2ff003248f448189932f6b71c2/lru_dict-1.3.0-cp311-cp311-win32.whl", hash = "sha256:a690c23fc353681ed8042d9fe8f48f0fb79a57b9a45daea2f0be1eef8a1a4aa4", size = 12533, upload-time = "2023-11-06T01:39:05.838Z" },
43
+
{ url = "https://files.pythonhosted.org/packages/ce/fe/1f12f33513310860ec6d722709ec4ad8256d9dcc3385f6ae2a244e6e66f5/lru_dict-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:efd3f4e0385d18f20f7ea6b08af2574c1bfaa5cb590102ef1bee781bdfba84bc", size = 13651, upload-time = "2023-11-06T01:39:06.871Z" },
44
+
{ url = "https://files.pythonhosted.org/packages/fc/5c/385f080747eb3083af87d8e4c9068f3c4cab89035f6982134889940dafd8/lru_dict-1.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:c279068f68af3b46a5d649855e1fb87f5705fe1f744a529d82b2885c0e1fc69d", size = 17174, upload-time = "2023-11-06T01:39:07.923Z" },
45
+
{ url = "https://files.pythonhosted.org/packages/3c/de/5ef2ed75ce55d7059d1b96177ba04fa7ee1f35564f97bdfcd28fccfbe9d2/lru_dict-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:350e2233cfee9f326a0d7a08e309372d87186565e43a691b120006285a0ac549", size = 10742, upload-time = "2023-11-06T01:39:08.871Z" },
46
+
{ url = "https://files.pythonhosted.org/packages/ca/05/f69a6abb0062d2cf2ce0aaf0284b105b97d1da024ca6d3d0730e6151242e/lru_dict-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4eafb188a84483b3231259bf19030859f070321b00326dcb8e8c6cbf7db4b12f", size = 11079, upload-time = "2023-11-06T01:39:09.766Z" },
47
+
{ url = "https://files.pythonhosted.org/packages/ea/59/cf891143abe58a455b8eaa9175f0e80f624a146a2bf9a1ca842ee0ef930a/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73593791047e36b37fdc0b67b76aeed439fcea80959c7d46201240f9ec3b2563", size = 32469, upload-time = "2023-11-06T01:39:11.091Z" },
48
+
{ url = "https://files.pythonhosted.org/packages/59/88/d5976e9f70107ce11e45d93c6f0c2d5eaa1fc30bb3c8f57525eda4510dff/lru_dict-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1958cb70b9542773d6241974646e5410e41ef32e5c9e437d44040d59bd80daf2", size = 33496, upload-time = "2023-11-06T01:39:12.463Z" },
49
+
{ url = "https://files.pythonhosted.org/packages/6c/f8/94d6e910d54fc1fa05c0ee1cd608c39401866a18cf5e5aff238449b33c11/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bc1cd3ed2cee78a47f11f3b70be053903bda197a873fd146e25c60c8e5a32cd6", size = 29914, upload-time = "2023-11-06T01:39:13.395Z" },
50
+
{ url = "https://files.pythonhosted.org/packages/ca/b9/9db79780c8a3cfd66bba6847773061e5cf8a3746950273b9985d47bbfe53/lru_dict-1.3.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82eb230d48eaebd6977a92ddaa6d788f14cf4f4bcf5bbffa4ddfd60d051aa9d4", size = 32241, upload-time = "2023-11-06T01:39:14.612Z" },
51
+
{ url = "https://files.pythonhosted.org/packages/9b/b6/08a623019daec22a40c4d6d2c40851dfa3d129a53b2f9469db8eb13666c1/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5ad659cbc349d0c9ba8e536b5f40f96a70c360f43323c29f4257f340d891531c", size = 37320, upload-time = "2023-11-06T01:39:15.875Z" },
52
+
{ url = "https://files.pythonhosted.org/packages/70/0b/d3717159c26155ff77679cee1b077d22e1008bf45f19921e193319cd8e46/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ba490b8972531d153ac0d4e421f60d793d71a2f4adbe2f7740b3c55dce0a12f1", size = 35054, upload-time = "2023-11-06T01:39:17.063Z" },
53
+
{ url = "https://files.pythonhosted.org/packages/04/74/f2ae00de7c27984a19b88d2b09ac877031c525b01199d7841ec8fa657fd6/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:c0131351b8a7226c69f1eba5814cbc9d1d8daaf0fdec1ae3f30508e3de5262d4", size = 38613, upload-time = "2023-11-06T01:39:18.136Z" },
54
+
{ url = "https://files.pythonhosted.org/packages/5a/0b/e30236aafe31b4247aa9ae61ba8aac6dde75c3ea0e47a8fb7eef53f6d5ce/lru_dict-1.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0e88dba16695f17f41701269fa046197a3fd7b34a8dba744c8749303ddaa18df", size = 37143, upload-time = "2023-11-06T01:39:19.571Z" },
55
+
{ url = "https://files.pythonhosted.org/packages/1c/28/b59bcebb8d76ba8147a784a8be7eab6a4ad3395b9236e73740ff675a5a52/lru_dict-1.3.0-cp312-cp312-win32.whl", hash = "sha256:6ffaf595e625b388babc8e7d79b40f26c7485f61f16efe76764e32dce9ea17fc", size = 12653, upload-time = "2023-11-06T01:39:20.574Z" },
56
+
{ url = "https://files.pythonhosted.org/packages/bd/18/06d9710cb0a0d3634f8501e4bdcc07abe64a32e404d82895a6a36fab97f6/lru_dict-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf9da32ef2582434842ab6ba6e67290debfae72771255a8e8ab16f3e006de0aa", size = 13811, upload-time = "2023-11-06T01:39:21.599Z" },
57
+
]
58
+
59
+
[[package]]
60
+
name = "more-itertools"
61
+
version = "10.8.0"
62
+
source = { registry = "https://pypi.org/simple" }
63
+
sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
64
+
wheels = [
65
+
{ url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
66
+
]
67
+
68
+
[[package]]
69
+
name = "mst-test-suite"
70
+
version = "0.1.0"
71
+
source = { virtual = "." }
72
+
dependencies = [
73
+
{ name = "atmst" },
74
+
{ name = "cbrrr" },
75
+
]
76
+
77
+
[package.metadata]
78
+
requires-dist = [
79
+
{ name = "atmst", specifier = ">=0.0.6" },
80
+
{ name = "cbrrr", specifier = ">=1.0.1" },
81
+
]
+5
-6
packages/utilities/mst/package.json
+5
-6
packages/utilities/mst/package.json
···
2
2
"type": "module",
3
3
"name": "@atcute/mst",
4
4
"version": "0.1.0",
5
-
"description": "atproto mst manipulation utilities",
5
+
"description": "atproto MST manipulation utilities",
6
6
"keywords": [
7
7
"atproto",
8
8
"repo",
9
-
"mst",
10
-
"dasl",
11
-
"car"
9
+
"mst"
12
10
],
13
11
"license": "0BSD",
14
12
"repository": {
···
26
24
},
27
25
"sideEffects": false,
28
26
"scripts": {
27
+
"generate-tests": "cd mst-test-suite && mise exec -- uv run python scripts/generate_exhaustive_cars.py",
29
28
"build": "tsc --project tsconfig.build.json",
30
29
"test": "vitest run",
31
30
"prepublish": "rm -rf dist; pnpm run build"
···
33
32
"devDependencies": {
34
33
"@atcute/car": "workspace:^",
35
34
"@vitest/coverage-v8": "^3.2.4",
35
+
"valibot": "^1.1.0",
36
36
"vitest": "^3.2.4"
37
37
},
38
38
"dependencies": {
39
39
"@atcute/cbor": "workspace:^",
40
40
"@atcute/cid": "workspace:^",
41
41
"@atcute/uint8array": "workspace:^",
42
-
"@atcute/varint": "workspace:^",
43
-
"@badrap/valita": "^0.4.6"
42
+
"@atcute/varint": "workspace:^"
44
43
}
45
44
}
+3
-3
pnpm-lock.yaml
+3
-3
pnpm-lock.yaml
···
747
747
'@atcute/varint':
748
748
specifier: workspace:^
749
749
version: link:../varint
750
-
'@badrap/valita':
751
-
specifier: ^0.4.6
752
-
version: 0.4.6
753
750
devDependencies:
754
751
'@atcute/car':
755
752
specifier: workspace:^
···
757
754
'@vitest/coverage-v8':
758
755
specifier: ^3.2.4
759
756
version: 3.2.4(@vitest/browser@3.2.4)(vitest@3.2.4)
757
+
valibot:
758
+
specifier: ^1.1.0
759
+
version: 1.1.0(typescript@5.9.2)
760
760
vitest:
761
761
specifier: ^3.2.4
762
762
version: 3.2.4(@types/node@24.3.0)(@vitest/browser@3.2.4)(tsx@4.20.6)(yaml@2.8.0)