@atproto/repo 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/block-map.d.ts +2 -0
- package/dist/data-diff.d.ts +12 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +12388 -4431
- package/dist/index.js.map +4 -4
- package/dist/mst/mst.d.ts +19 -16
- package/dist/readable-repo.d.ts +4 -3
- package/dist/repo.d.ts +3 -2
- package/dist/storage/index.d.ts +0 -1
- package/dist/storage/memory-blockstore.d.ts +6 -10
- package/dist/storage/types.d.ts +29 -0
- package/dist/sync/consumer.d.ts +13 -16
- package/dist/sync/provider.d.ts +2 -6
- package/dist/types.d.ts +236 -48
- package/dist/util.d.ts +9 -7
- package/jest.bench.config.js +2 -1
- package/package.json +12 -7
- package/src/block-map.ts +8 -0
- package/src/data-diff.ts +47 -49
- package/src/index.ts +1 -1
- package/src/mst/diff.ts +14 -36
- package/src/mst/mst.ts +15 -14
- package/src/readable-repo.ts +5 -5
- package/src/repo.ts +50 -40
- package/src/storage/index.ts +0 -1
- package/src/storage/memory-blockstore.ts +19 -59
- package/src/storage/types.ts +30 -0
- package/src/sync/consumer.ts +170 -113
- package/src/sync/provider.ts +6 -44
- package/src/types.ts +49 -25
- package/src/util.ts +57 -91
- package/tests/_util.ts +38 -67
- package/tests/mst.test.ts +4 -1
- package/tests/{sync/narrow.test.ts → proofs.test.ts} +14 -21
- package/tests/repo.test.ts +5 -4
- package/tests/sync.test.ts +97 -0
- package/tests/util.test.ts +21 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/tsconfig.json +1 -1
- package/dist/blockstore/index.d.ts +0 -2
- package/dist/blockstore/ipld-store.d.ts +0 -27
- package/dist/blockstore/memory-blockstore.d.ts +0 -13
- package/dist/storage/repo-storage.d.ts +0 -18
- package/dist/sync.d.ts +0 -9
- package/dist/verify.d.ts +0 -27
- package/src/storage/repo-storage.ts +0 -42
- package/src/verify.ts +0 -227
- package/tests/sync/checkout.test.ts +0 -57
- package/tests/sync/diff.test.ts +0 -87
package/src/util.ts
CHANGED
|
@@ -4,36 +4,34 @@ import { CarReader } from '@ipld/car/reader'
|
|
|
4
4
|
import { BlockWriter, CarWriter } from '@ipld/car/writer'
|
|
5
5
|
import { Block as CarBlock } from '@ipld/car/api'
|
|
6
6
|
import {
|
|
7
|
-
|
|
7
|
+
streamToBuffer,
|
|
8
8
|
verifyCidForBytes,
|
|
9
9
|
cborDecode,
|
|
10
10
|
check,
|
|
11
11
|
schema,
|
|
12
12
|
cidForCbor,
|
|
13
|
+
byteIterableToStream,
|
|
14
|
+
TID,
|
|
13
15
|
} from '@atproto/common'
|
|
14
16
|
import { ipldToLex, lexToIpld, LexValue, RepoRecord } from '@atproto/lexicon'
|
|
15
17
|
|
|
16
18
|
import * as crypto from '@atproto/crypto'
|
|
17
|
-
import Repo from './repo'
|
|
18
|
-
import { MST } from './mst'
|
|
19
19
|
import DataDiff from './data-diff'
|
|
20
|
-
import { RepoStorage } from './storage'
|
|
21
20
|
import {
|
|
22
21
|
Commit,
|
|
23
|
-
|
|
22
|
+
LegacyV2Commit,
|
|
24
23
|
RecordCreateDescript,
|
|
25
24
|
RecordDeleteDescript,
|
|
26
25
|
RecordPath,
|
|
27
26
|
RecordUpdateDescript,
|
|
28
27
|
RecordWriteDescript,
|
|
29
28
|
UnsignedCommit,
|
|
30
|
-
WriteLog,
|
|
31
29
|
WriteOpAction,
|
|
32
30
|
} from './types'
|
|
33
31
|
import BlockMap from './block-map'
|
|
34
|
-
import { MissingBlocksError } from './error'
|
|
35
32
|
import * as parse from './parse'
|
|
36
33
|
import { Keypair } from '@atproto/crypto'
|
|
34
|
+
import { Readable } from 'stream'
|
|
37
35
|
|
|
38
36
|
export async function* verifyIncomingCarBlocks(
|
|
39
37
|
car: AsyncIterable<CarBlock>,
|
|
@@ -44,25 +42,37 @@ export async function* verifyIncomingCarBlocks(
|
|
|
44
42
|
}
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
|
|
45
|
+
// we have to turn the car writer output into a stream in order to properly handle errors
|
|
46
|
+
export function writeCarStream(
|
|
48
47
|
root: CID | null,
|
|
49
48
|
fn: (car: BlockWriter) => Promise<void>,
|
|
50
|
-
):
|
|
49
|
+
): Readable {
|
|
51
50
|
const { writer, out } =
|
|
52
51
|
root !== null ? CarWriter.create(root) : CarWriter.create()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
52
|
+
|
|
53
|
+
const stream = byteIterableToStream(out)
|
|
54
|
+
fn(writer)
|
|
55
|
+
.catch((err) => {
|
|
56
|
+
stream.destroy(err)
|
|
57
|
+
})
|
|
58
|
+
.finally(() => writer.close())
|
|
59
|
+
return stream
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function* writeCar(
|
|
63
|
+
root: CID | null,
|
|
64
|
+
fn: (car: BlockWriter) => Promise<void>,
|
|
65
|
+
): AsyncIterable<Uint8Array> {
|
|
66
|
+
const stream = writeCarStream(root, fn)
|
|
67
|
+
for await (const chunk of stream) {
|
|
68
|
+
yield chunk
|
|
58
69
|
}
|
|
59
|
-
return bytes
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
export const
|
|
72
|
+
export const blocksToCarStream = (
|
|
63
73
|
root: CID | null,
|
|
64
74
|
blocks: BlockMap,
|
|
65
|
-
):
|
|
75
|
+
): AsyncIterable<Uint8Array> => {
|
|
66
76
|
return writeCar(root, async (writer) => {
|
|
67
77
|
for (const entry of blocks.entries()) {
|
|
68
78
|
await writer.put(entry)
|
|
@@ -70,6 +80,14 @@ export const blocksToCar = async (
|
|
|
70
80
|
})
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
export const blocksToCarFile = (
|
|
84
|
+
root: CID | null,
|
|
85
|
+
blocks: BlockMap,
|
|
86
|
+
): Promise<Uint8Array> => {
|
|
87
|
+
const carStream = blocksToCarStream(root, blocks)
|
|
88
|
+
return streamToBuffer(carStream)
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
export const readCar = async (
|
|
74
92
|
bytes: Uint8Array,
|
|
75
93
|
): Promise<{ roots: CID[]; blocks: BlockMap }> => {
|
|
@@ -99,33 +117,6 @@ export const readCarWithRoot = async (
|
|
|
99
117
|
}
|
|
100
118
|
}
|
|
101
119
|
|
|
102
|
-
export const getWriteLog = async (
|
|
103
|
-
storage: RepoStorage,
|
|
104
|
-
latest: CID,
|
|
105
|
-
earliest: CID | null,
|
|
106
|
-
): Promise<WriteLog> => {
|
|
107
|
-
const commits = await storage.getCommitPath(latest, earliest)
|
|
108
|
-
if (!commits) throw new Error('Could not find shared history')
|
|
109
|
-
const heads = await Promise.all(commits.map((c) => Repo.load(storage, c)))
|
|
110
|
-
// Turn commit path into list of diffs
|
|
111
|
-
let prev: DataStore = await MST.create(storage) // Empty
|
|
112
|
-
const msts = heads.map((h) => h.data)
|
|
113
|
-
const diffs: DataDiff[] = []
|
|
114
|
-
for (const mst of msts) {
|
|
115
|
-
diffs.push(await DataDiff.of(mst, prev))
|
|
116
|
-
prev = mst
|
|
117
|
-
}
|
|
118
|
-
const fullDiff = collapseDiffs(diffs)
|
|
119
|
-
const diffBlocks = await storage.getBlocks(fullDiff.newCidList())
|
|
120
|
-
if (diffBlocks.missing.length > 0) {
|
|
121
|
-
throw new MissingBlocksError('write op log', diffBlocks.missing)
|
|
122
|
-
}
|
|
123
|
-
// Map MST diffs to write ops
|
|
124
|
-
return Promise.all(
|
|
125
|
-
diffs.map((diff) => diffToWriteDescripts(diff, diffBlocks.blocks)),
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
120
|
export const diffToWriteDescripts = (
|
|
130
121
|
diff: DataDiff,
|
|
131
122
|
blocks: BlockMap,
|
|
@@ -166,55 +157,18 @@ export const diffToWriteDescripts = (
|
|
|
166
157
|
])
|
|
167
158
|
}
|
|
168
159
|
|
|
169
|
-
export const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
for (const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (del) {
|
|
179
|
-
if (del.cid !== op.cid) {
|
|
180
|
-
updates[key] = {
|
|
181
|
-
...op,
|
|
182
|
-
action: WriteOpAction.Update,
|
|
183
|
-
prev: del.cid,
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
delete deletes[key]
|
|
187
|
-
} else {
|
|
188
|
-
creates[key] = op
|
|
189
|
-
}
|
|
190
|
-
} else if (op.action === WriteOpAction.Update) {
|
|
191
|
-
updates[key] = op
|
|
192
|
-
delete creates[key]
|
|
193
|
-
delete deletes[key]
|
|
194
|
-
} else if (op.action === WriteOpAction.Delete) {
|
|
195
|
-
if (creates[key]) {
|
|
196
|
-
delete creates[key]
|
|
197
|
-
} else {
|
|
198
|
-
delete updates[key]
|
|
199
|
-
deletes[key] = op
|
|
200
|
-
}
|
|
201
|
-
} else {
|
|
202
|
-
throw new Error(`unknown action: ${op}`)
|
|
203
|
-
}
|
|
160
|
+
export const ensureCreates = (
|
|
161
|
+
descripts: RecordWriteDescript[],
|
|
162
|
+
): RecordCreateDescript[] => {
|
|
163
|
+
const creates: RecordCreateDescript[] = []
|
|
164
|
+
for (const descript of descripts) {
|
|
165
|
+
if (descript.action !== WriteOpAction.Create) {
|
|
166
|
+
throw new Error(`Unexpected action: ${descript.action}`)
|
|
167
|
+
} else {
|
|
168
|
+
creates.push(descript)
|
|
204
169
|
}
|
|
205
170
|
}
|
|
206
|
-
return
|
|
207
|
-
...Object.values(creates),
|
|
208
|
-
...Object.values(updates),
|
|
209
|
-
...Object.values(deletes),
|
|
210
|
-
]
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export const collapseDiffs = (diffs: DataDiff[]): DataDiff => {
|
|
214
|
-
return diffs.reduce((acc, cur) => {
|
|
215
|
-
acc.addDiff(cur)
|
|
216
|
-
return acc
|
|
217
|
-
}, new DataDiff())
|
|
171
|
+
return creates
|
|
218
172
|
}
|
|
219
173
|
|
|
220
174
|
export const parseDataKey = (key: string): RecordPath => {
|
|
@@ -267,3 +221,15 @@ export const cborToLexRecord = (val: Uint8Array): RepoRecord => {
|
|
|
267
221
|
export const cidForRecord = async (val: LexValue) => {
|
|
268
222
|
return cidForCbor(lexToIpld(val))
|
|
269
223
|
}
|
|
224
|
+
|
|
225
|
+
export const ensureV3Commit = (commit: LegacyV2Commit | Commit): Commit => {
|
|
226
|
+
if (commit.version === 3) {
|
|
227
|
+
return commit
|
|
228
|
+
} else {
|
|
229
|
+
return {
|
|
230
|
+
...commit,
|
|
231
|
+
version: 3,
|
|
232
|
+
rev: commit.rev ?? TID.nextStr(),
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/tests/_util.ts
CHANGED
|
@@ -7,15 +7,15 @@ import { RepoStorage } from '../src/storage'
|
|
|
7
7
|
import { MST } from '../src/mst'
|
|
8
8
|
import {
|
|
9
9
|
BlockMap,
|
|
10
|
-
collapseWriteLog,
|
|
11
10
|
CollectionContents,
|
|
12
11
|
RecordWriteOp,
|
|
13
12
|
RepoContents,
|
|
14
13
|
RecordPath,
|
|
15
|
-
WriteLog,
|
|
16
14
|
WriteOpAction,
|
|
17
15
|
RecordClaim,
|
|
18
16
|
Commit,
|
|
17
|
+
DataDiff,
|
|
18
|
+
CommitData,
|
|
19
19
|
} from '../src'
|
|
20
20
|
import { Keypair, randomBytes } from '@atproto/crypto'
|
|
21
21
|
|
|
@@ -109,7 +109,7 @@ export const fillRepo = async (
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
export const
|
|
112
|
+
export const formatEdit = async (
|
|
113
113
|
repo: Repo,
|
|
114
114
|
prevData: RepoContents,
|
|
115
115
|
keypair: crypto.Keypair,
|
|
@@ -118,91 +118,58 @@ export const editRepo = async (
|
|
|
118
118
|
updates?: number
|
|
119
119
|
deletes?: number
|
|
120
120
|
},
|
|
121
|
-
): Promise<{
|
|
121
|
+
): Promise<{ commit: CommitData; data: RepoContents }> => {
|
|
122
122
|
const { adds = 0, updates = 0, deletes = 0 } = params
|
|
123
123
|
const repoData: RepoContents = {}
|
|
124
|
+
const writes: RecordWriteOp[] = []
|
|
124
125
|
for (const collName of testCollections) {
|
|
125
|
-
const collData = prevData[collName]
|
|
126
|
+
const collData = { ...(prevData[collName] ?? {}) }
|
|
126
127
|
const shuffled = shuffle(Object.entries(collData))
|
|
127
128
|
|
|
128
129
|
for (let i = 0; i < adds; i++) {
|
|
129
130
|
const object = generateObject()
|
|
130
131
|
const rkey = TID.nextStr()
|
|
131
132
|
collData[rkey] = object
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
},
|
|
139
|
-
keypair,
|
|
140
|
-
)
|
|
133
|
+
writes.push({
|
|
134
|
+
action: WriteOpAction.Create,
|
|
135
|
+
collection: collName,
|
|
136
|
+
rkey,
|
|
137
|
+
record: object,
|
|
138
|
+
})
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
const toUpdate = shuffled.slice(0, updates)
|
|
144
142
|
for (let i = 0; i < toUpdate.length; i++) {
|
|
145
143
|
const object = generateObject()
|
|
146
144
|
const rkey = toUpdate[i][0]
|
|
147
|
-
repo = await repo.applyWrites(
|
|
148
|
-
{
|
|
149
|
-
action: WriteOpAction.Update,
|
|
150
|
-
collection: collName,
|
|
151
|
-
rkey,
|
|
152
|
-
record: object,
|
|
153
|
-
},
|
|
154
|
-
keypair,
|
|
155
|
-
)
|
|
156
145
|
collData[rkey] = object
|
|
146
|
+
writes.push({
|
|
147
|
+
action: WriteOpAction.Update,
|
|
148
|
+
collection: collName,
|
|
149
|
+
rkey,
|
|
150
|
+
record: object,
|
|
151
|
+
})
|
|
157
152
|
}
|
|
158
153
|
|
|
159
154
|
const toDelete = shuffled.slice(updates, deletes)
|
|
160
155
|
for (let i = 0; i < toDelete.length; i++) {
|
|
161
156
|
const rkey = toDelete[i][0]
|
|
162
|
-
repo = await repo.applyWrites(
|
|
163
|
-
{
|
|
164
|
-
action: WriteOpAction.Delete,
|
|
165
|
-
collection: collName,
|
|
166
|
-
rkey,
|
|
167
|
-
},
|
|
168
|
-
keypair,
|
|
169
|
-
)
|
|
170
157
|
delete collData[rkey]
|
|
158
|
+
writes.push({
|
|
159
|
+
action: WriteOpAction.Delete,
|
|
160
|
+
collection: collName,
|
|
161
|
+
rkey,
|
|
162
|
+
})
|
|
171
163
|
}
|
|
172
164
|
repoData[collName] = collData
|
|
173
165
|
}
|
|
166
|
+
const commit = await repo.formatCommit(writes, keypair)
|
|
174
167
|
return {
|
|
175
|
-
|
|
168
|
+
commit,
|
|
176
169
|
data: repoData,
|
|
177
170
|
}
|
|
178
171
|
}
|
|
179
172
|
|
|
180
|
-
export const verifyRepoDiff = async (
|
|
181
|
-
writeLog: WriteLog,
|
|
182
|
-
before: RepoContents,
|
|
183
|
-
after: RepoContents,
|
|
184
|
-
): Promise<void> => {
|
|
185
|
-
const getVal = (op: RecordWriteOp, data: RepoContents) => {
|
|
186
|
-
return (data[op.collection] || {})[op.rkey]
|
|
187
|
-
}
|
|
188
|
-
const ops = await collapseWriteLog(writeLog)
|
|
189
|
-
|
|
190
|
-
for (const op of ops) {
|
|
191
|
-
if (op.action === WriteOpAction.Create) {
|
|
192
|
-
expect(getVal(op, before)).toBeUndefined()
|
|
193
|
-
expect(getVal(op, after)).toEqual(op.record)
|
|
194
|
-
} else if (op.action === WriteOpAction.Update) {
|
|
195
|
-
expect(getVal(op, before)).toBeDefined()
|
|
196
|
-
expect(getVal(op, after)).toEqual(op.record)
|
|
197
|
-
} else if (op.action === WriteOpAction.Delete) {
|
|
198
|
-
expect(getVal(op, before)).toBeDefined()
|
|
199
|
-
expect(getVal(op, after)).toBeUndefined()
|
|
200
|
-
} else {
|
|
201
|
-
throw new Error('unexpected op type')
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
173
|
export const contentsToClaims = (contents: RepoContents): RecordClaim[] => {
|
|
207
174
|
const claims: RecordClaim[] = []
|
|
208
175
|
for (const coll of Object.keys(contents)) {
|
|
@@ -233,23 +200,27 @@ export const addBadCommit = async (
|
|
|
233
200
|
keypair: Keypair,
|
|
234
201
|
): Promise<Repo> => {
|
|
235
202
|
const obj = generateObject()
|
|
236
|
-
const
|
|
237
|
-
const cid = await
|
|
203
|
+
const newBlocks = new BlockMap()
|
|
204
|
+
const cid = await newBlocks.add(obj)
|
|
238
205
|
const updatedData = await repo.data.add(`com.example.test/${TID.next()}`, cid)
|
|
239
|
-
const
|
|
240
|
-
|
|
206
|
+
const dataCid = await updatedData.getPointer()
|
|
207
|
+
const diff = await DataDiff.of(updatedData, repo.data)
|
|
208
|
+
newBlocks.addMap(diff.newMstBlocks)
|
|
241
209
|
// we generate a bad sig by signing some other data
|
|
210
|
+
const rev = TID.nextStr(repo.commit.rev)
|
|
242
211
|
const commit: Commit = {
|
|
243
212
|
...repo.commit,
|
|
244
|
-
|
|
245
|
-
data:
|
|
213
|
+
rev,
|
|
214
|
+
data: dataCid,
|
|
246
215
|
sig: await keypair.sign(randomBytes(256)),
|
|
247
216
|
}
|
|
248
|
-
const commitCid = await
|
|
217
|
+
const commitCid = await newBlocks.add(commit)
|
|
249
218
|
await repo.storage.applyCommit({
|
|
250
|
-
|
|
219
|
+
cid: commitCid,
|
|
220
|
+
rev,
|
|
251
221
|
prev: repo.cid,
|
|
252
|
-
|
|
222
|
+
newBlocks,
|
|
223
|
+
removedCids: diff.removedCids,
|
|
253
224
|
})
|
|
254
225
|
return await Repo.load(repo.storage, commitCid)
|
|
255
226
|
}
|
package/tests/mst.test.ts
CHANGED
|
@@ -150,7 +150,10 @@ describe('Merkle Search Tree', () => {
|
|
|
150
150
|
} else {
|
|
151
151
|
cid = entry.value
|
|
152
152
|
}
|
|
153
|
-
const found =
|
|
153
|
+
const found =
|
|
154
|
+
(await blockstore.has(cid)) ||
|
|
155
|
+
diff.newMstBlocks.has(cid) ||
|
|
156
|
+
diff.newLeafCids.has(cid)
|
|
154
157
|
expect(found).toBeTruthy()
|
|
155
158
|
}
|
|
156
159
|
})
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { TID } from '@atproto/common'
|
|
1
|
+
import { TID, streamToBuffer } from '@atproto/common'
|
|
2
2
|
import * as crypto from '@atproto/crypto'
|
|
3
|
-
import { RecordClaim, Repo, RepoContents } from '
|
|
4
|
-
import { MemoryBlockstore } from '
|
|
5
|
-
import * as
|
|
6
|
-
import * as sync from '../../src/sync'
|
|
3
|
+
import { RecordClaim, Repo, RepoContents } from '../src'
|
|
4
|
+
import { MemoryBlockstore } from '../src/storage'
|
|
5
|
+
import * as sync from '../src/sync'
|
|
7
6
|
|
|
8
|
-
import * as util from '
|
|
7
|
+
import * as util from './_util'
|
|
9
8
|
|
|
10
|
-
describe('
|
|
9
|
+
describe('Repo Proofs', () => {
|
|
11
10
|
let storage: MemoryBlockstore
|
|
12
11
|
let repo: Repo
|
|
13
12
|
let keypair: crypto.Keypair
|
|
@@ -25,11 +24,11 @@ describe('Narrow Sync', () => {
|
|
|
25
24
|
})
|
|
26
25
|
|
|
27
26
|
const getProofs = async (claims: RecordClaim[]) => {
|
|
28
|
-
return sync.getRecords(storage, repo.cid, claims)
|
|
27
|
+
return streamToBuffer(sync.getRecords(storage, repo.cid, claims))
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
const doVerify = (proofs: Uint8Array, claims: RecordClaim[]) => {
|
|
32
|
-
return
|
|
31
|
+
return sync.verifyProofs(proofs, claims, repoDid, keypair.did())
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
it('verifies valid records', async () => {
|
|
@@ -112,7 +111,7 @@ describe('Narrow Sync', () => {
|
|
|
112
111
|
possible[8],
|
|
113
112
|
]
|
|
114
113
|
const proofs = await getProofs(claims)
|
|
115
|
-
const records = await
|
|
114
|
+
const records = await sync.verifyRecords(proofs, repoDid, keypair.did())
|
|
116
115
|
for (const record of records) {
|
|
117
116
|
const foundClaim = claims.find(
|
|
118
117
|
(claim) =>
|
|
@@ -127,19 +126,13 @@ describe('Narrow Sync', () => {
|
|
|
127
126
|
}
|
|
128
127
|
})
|
|
129
128
|
|
|
130
|
-
it('verifyRecords throws on a bad signature', async () => {
|
|
131
|
-
const badRepo = await util.addBadCommit(repo, keypair)
|
|
132
|
-
const claims = util.contentsToClaims(repoData)
|
|
133
|
-
const proofs = await sync.getRecords(storage, badRepo.cid, claims)
|
|
134
|
-
const fn = verify.verifyRecords(proofs, repoDid, keypair.did())
|
|
135
|
-
await expect(fn).rejects.toThrow(verify.RepoVerificationError)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
129
|
it('verifyProofs throws on a bad signature', async () => {
|
|
139
130
|
const badRepo = await util.addBadCommit(repo, keypair)
|
|
140
131
|
const claims = util.contentsToClaims(repoData)
|
|
141
|
-
const proofs = await
|
|
142
|
-
|
|
143
|
-
|
|
132
|
+
const proofs = await streamToBuffer(
|
|
133
|
+
sync.getRecords(storage, badRepo.cid, claims),
|
|
134
|
+
)
|
|
135
|
+
const fn = sync.verifyProofs(proofs, claims, repoDid, keypair.did())
|
|
136
|
+
await expect(fn).rejects.toThrow(sync.RepoVerificationError)
|
|
144
137
|
})
|
|
145
138
|
})
|
package/tests/repo.test.ts
CHANGED
|
@@ -22,7 +22,7 @@ describe('Repo', () => {
|
|
|
22
22
|
|
|
23
23
|
it('has proper metadata', async () => {
|
|
24
24
|
expect(repo.did).toEqual(keypair.did())
|
|
25
|
-
expect(repo.version).toBe(
|
|
25
|
+
expect(repo.version).toBe(3)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
it('does basic operations', async () => {
|
|
@@ -75,12 +75,13 @@ describe('Repo', () => {
|
|
|
75
75
|
})
|
|
76
76
|
|
|
77
77
|
it('edits and deletes content', async () => {
|
|
78
|
-
const
|
|
78
|
+
const edit = await util.formatEdit(repo, repoData, keypair, {
|
|
79
79
|
adds: 20,
|
|
80
80
|
updates: 20,
|
|
81
81
|
deletes: 20,
|
|
82
82
|
})
|
|
83
|
-
repo =
|
|
83
|
+
repo = await repo.applyCommit(edit.commit)
|
|
84
|
+
repoData = edit.data
|
|
84
85
|
const contents = await repo.getContents()
|
|
85
86
|
expect(contents).toEqual(repoData)
|
|
86
87
|
})
|
|
@@ -100,6 +101,6 @@ describe('Repo', () => {
|
|
|
100
101
|
const contents = await reloadedRepo.getContents()
|
|
101
102
|
expect(contents).toEqual(repoData)
|
|
102
103
|
expect(repo.did).toEqual(keypair.did())
|
|
103
|
-
expect(repo.version).toBe(
|
|
104
|
+
expect(repo.version).toBe(3)
|
|
104
105
|
})
|
|
105
106
|
})
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import * as crypto from '@atproto/crypto'
|
|
2
|
+
import {
|
|
3
|
+
CidSet,
|
|
4
|
+
Repo,
|
|
5
|
+
RepoContents,
|
|
6
|
+
RepoVerificationError,
|
|
7
|
+
readCarWithRoot,
|
|
8
|
+
} from '../src'
|
|
9
|
+
import { MemoryBlockstore } from '../src/storage'
|
|
10
|
+
import * as sync from '../src/sync'
|
|
11
|
+
|
|
12
|
+
import * as util from './_util'
|
|
13
|
+
import { streamToBuffer } from '@atproto/common'
|
|
14
|
+
import { CarReader } from '@ipld/car/reader'
|
|
15
|
+
|
|
16
|
+
describe('Repo Sync', () => {
|
|
17
|
+
let storage: MemoryBlockstore
|
|
18
|
+
let repo: Repo
|
|
19
|
+
let keypair: crypto.Keypair
|
|
20
|
+
let repoData: RepoContents
|
|
21
|
+
|
|
22
|
+
const repoDid = 'did:example:test'
|
|
23
|
+
|
|
24
|
+
beforeAll(async () => {
|
|
25
|
+
storage = new MemoryBlockstore()
|
|
26
|
+
keypair = await crypto.Secp256k1Keypair.create()
|
|
27
|
+
repo = await Repo.create(storage, repoDid, keypair)
|
|
28
|
+
const filled = await util.fillRepo(repo, keypair, 20)
|
|
29
|
+
repo = filled.repo
|
|
30
|
+
repoData = filled.data
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('sync a full repo', async () => {
|
|
34
|
+
const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))
|
|
35
|
+
const car = await readCarWithRoot(carBytes)
|
|
36
|
+
const verified = await sync.verifyRepo(
|
|
37
|
+
car.blocks,
|
|
38
|
+
car.root,
|
|
39
|
+
repoDid,
|
|
40
|
+
keypair.did(),
|
|
41
|
+
)
|
|
42
|
+
const syncStorage = new MemoryBlockstore()
|
|
43
|
+
await syncStorage.applyCommit(verified.commit)
|
|
44
|
+
const loadedRepo = await Repo.load(syncStorage, car.root)
|
|
45
|
+
const contents = await loadedRepo.getContents()
|
|
46
|
+
expect(contents).toEqual(repoData)
|
|
47
|
+
const contentsFromOps: RepoContents = {}
|
|
48
|
+
for (const write of verified.creates) {
|
|
49
|
+
contentsFromOps[write.collection] ??= {}
|
|
50
|
+
contentsFromOps[write.collection][write.rkey] = write.record
|
|
51
|
+
}
|
|
52
|
+
expect(contentsFromOps).toEqual(repoData)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('does not sync duplicate blocks', async () => {
|
|
56
|
+
const carBytes = await streamToBuffer(sync.getFullRepo(storage, repo.cid))
|
|
57
|
+
const car = await CarReader.fromBytes(carBytes)
|
|
58
|
+
const cids = new CidSet()
|
|
59
|
+
for await (const block of car.blocks()) {
|
|
60
|
+
if (cids.has(block.cid)) {
|
|
61
|
+
throw new Error(`duplicate block: :${block.cid.toString()}`)
|
|
62
|
+
}
|
|
63
|
+
cids.add(block.cid)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('syncs a repo that is behind', async () => {
|
|
68
|
+
// add more to providers's repo & have consumer catch up
|
|
69
|
+
const edit = await util.formatEdit(repo, repoData, keypair, {
|
|
70
|
+
adds: 10,
|
|
71
|
+
updates: 10,
|
|
72
|
+
deletes: 10,
|
|
73
|
+
})
|
|
74
|
+
const verified = await sync.verifyDiff(
|
|
75
|
+
repo,
|
|
76
|
+
edit.commit.newBlocks,
|
|
77
|
+
edit.commit.cid,
|
|
78
|
+
repoDid,
|
|
79
|
+
keypair.did(),
|
|
80
|
+
)
|
|
81
|
+
await storage.applyCommit(verified.commit)
|
|
82
|
+
repo = await Repo.load(storage, verified.commit.cid)
|
|
83
|
+
const contents = await repo.getContents()
|
|
84
|
+
expect(contents).toEqual(edit.data)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('throws on a bad signature', async () => {
|
|
88
|
+
const badRepo = await util.addBadCommit(repo, keypair)
|
|
89
|
+
const carBytes = await streamToBuffer(
|
|
90
|
+
sync.getFullRepo(storage, badRepo.cid),
|
|
91
|
+
)
|
|
92
|
+
const car = await readCarWithRoot(carBytes)
|
|
93
|
+
await expect(
|
|
94
|
+
sync.verifyRepo(car.blocks, car.root, repoDid, keypair.did()),
|
|
95
|
+
).rejects.toThrow(RepoVerificationError)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { dataToCborBlock, wait } from '@atproto/common'
|
|
2
|
+
import { writeCar } from '../src'
|
|
3
|
+
|
|
4
|
+
describe('Utils', () => {
|
|
5
|
+
describe('writeCar()', () => {
|
|
6
|
+
it('propagates errors', async () => {
|
|
7
|
+
const iterate = async () => {
|
|
8
|
+
const iter = writeCar(null, async (car) => {
|
|
9
|
+
await wait(1)
|
|
10
|
+
const block = await dataToCborBlock({ test: 1 })
|
|
11
|
+
await car.put(block)
|
|
12
|
+
throw new Error('Oops!')
|
|
13
|
+
})
|
|
14
|
+
for await (const bytes of iter) {
|
|
15
|
+
// no-op
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
await expect(iterate).rejects.toThrow('Oops!')
|
|
19
|
+
})
|
|
20
|
+
})
|
|
21
|
+
})
|