autobee 2.0.0-rc.8 → 2.0.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/lib/topo.js CHANGED
@@ -1,13 +1,15 @@
1
1
  const b4a = require('b4a')
2
2
  const encoding = require('./encoding.js')
3
- const { createNode } = require('./writers.js')
3
+ const { createNode, finalizeNodeBatch } = require('./writers.js')
4
4
  const asserts = require('./asserts.js')
5
+ const { LEGACY_OPLOG_VERSION } = require('./constants.js')
5
6
 
6
7
  const EMPTY_HEAD = { length: 0, key: null }
7
8
  const INFO_LEGACY_KEY = b4a.concat([b4a.from([0, 0]), b4a.from('info')])
8
9
 
10
+ const DEFAULT_OP_TIMEOUT = 5000
11
+
9
12
  exports.sort = sort
10
- exports.rollback = rollback
11
13
  exports.replay = replay
12
14
  exports.getMostRecentHead = getMostRecentHead
13
15
 
@@ -36,47 +38,34 @@ class Clock {
36
38
  async function getMostRecentHead(auto, system) {
37
39
  const ch = system.createChangesStream()
38
40
  for await (const data of ch) {
39
- const { ref } = getOplog(data)
40
- return { key: ref.oplog.key, length: ref.oplog.length }
41
- }
42
- }
41
+ const refs = getOplogCompat(data)
42
+ if (!refs.length) continue
43
43
 
44
- async function rollback(auto, system, verified) {
45
- const tip = []
46
- const ch = system.createChangesStream()
47
-
48
- for await (const data of ch) {
49
- const { ref, info } = getOplog(data)
50
- if (sameNode(ref.oplog, verified.op)) {
51
- asserts.assert(info.flushes === verified.flushes, 'Expect tip to be continuous')
52
- break
53
- }
54
-
55
- tip.push(ref)
44
+ const { oplog } = refs[0]
45
+ return { key: oplog.key, length: oplog.length }
56
46
  }
57
47
 
58
- const inflated = await inflate(auto, tip)
59
-
60
- // @todo: verify sort
61
-
62
- return {
63
- undo: null,
64
- view: null,
65
- tip: inflated.map(getBatch)
66
- }
48
+ return null
67
49
  }
68
50
 
51
+ // nodes we cannot resolve are returned as null rather than dropped, so callers
52
+ // can tell an incomplete replay from a short one
69
53
  async function replay(auto) {
70
- const ch = auto.system.bee.createChangesStream()
71
54
  const replay = []
72
55
 
73
- for await (const data of ch) {
74
- const { ref } = getOplog(data)
75
- replay.push(ref)
56
+ for await (const entry of oplogIterator(auto.system.bee)) {
57
+ // the walk was cut short, nothing below this point is reachable
58
+ if (entry === null) {
59
+ replay.push(null)
60
+ break
61
+ }
62
+
63
+ for (const ref of entry.refs) replay.push(ref)
76
64
  }
77
65
 
78
- const inflated = await inflate(auto, replay.reverse())
79
- return inflated.flatMap((b) => b.batch)
66
+ // same reasoning as the iterator - never block on what we do not have
67
+ const inflated = await inflate(auto, replay.reverse(), { wait: false })
68
+ return inflated.flatMap((b) => (b === null ? [null] : b.batch))
80
69
  }
81
70
 
82
71
  async function sort(auto, batch) {
@@ -87,7 +76,7 @@ async function sort(auto, batch) {
87
76
  for await (const data of ch) {
88
77
  if (isLegacyNode(data)) break
89
78
 
90
- const { ref, info } = getOplog(data)
79
+ const { ref } = getOplog(data)
91
80
  const link = {
92
81
  key: ref.oplog.key,
93
82
  length: ref.oplog.length,
@@ -129,8 +118,7 @@ function addSorted(list, entry) {
129
118
  clock.add(link)
130
119
  }
131
120
 
132
- // backer MUST precede node
133
- if (node.witness) clock.add(node.witness.backer)
121
+ if (node.witness) clock.add(node.witness.link)
134
122
 
135
123
  const tip = []
136
124
  const linked = []
@@ -193,12 +181,21 @@ function cmp(a, b) {
193
181
  return a.length < b.length ? -1 : 1
194
182
  }
195
183
 
196
- async function inflate(auto, reorder) {
184
+ async function inflate(auto, reorder, { wait = true, timeout = DEFAULT_OP_TIMEOUT } = {}) {
185
+ const readOpts = wait ? { timeout } : { wait: false }
186
+
197
187
  const promises = []
198
188
  const sessions = new Map()
199
189
 
200
190
  for (let i = 0; i < reorder.length; i++) {
201
191
  const ref = reorder[i]
192
+
193
+ // marker for a node we could not resolve, pass it through
194
+ if (ref === null) {
195
+ promises.push(null)
196
+ continue
197
+ }
198
+
202
199
  const id = b4a.toString(ref.oplog.key, 'hex')
203
200
  // only seeds the node - reapplication re-resolves from the witness
204
201
  const weight = ref.oplog.weight
@@ -215,7 +212,7 @@ async function inflate(auto, reorder) {
215
212
  sessions.set(id, core)
216
213
  }
217
214
 
218
- promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i))
215
+ promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, readOpts))
219
216
  }
220
217
 
221
218
  try {
@@ -225,11 +222,27 @@ async function inflate(auto, reorder) {
225
222
  }
226
223
  }
227
224
 
228
- async function getOplogBatch(undo, core, length, weight, index) {
225
+ async function getOplogBatch(undo, core, length, weight, index, readOpts = null) {
229
226
  const seq = length - 1
230
- const block = await core.get(seq)
227
+
228
+ const block = await core.get(seq, readOpts)
229
+ if (block === null) return null
230
+
231
231
  const oplog = encoding.decodeOplog(block)
232
232
  const head = createNode(core, length, weight, oplog)
233
+ const legacy = oplog.version <= LEGACY_OPLOG_VERSION
234
+
235
+ // if this is legacy the start pointer is always 0, fix
236
+ if (legacy) {
237
+ for (let i = length - 1; i > 0; i--) {
238
+ const block = await core.get(i - 1, readOpts)
239
+ if (block === null) return null // wait: false in replay helper
240
+
241
+ const oplog = encoding.decodeOplog(block)
242
+ if (oplog.batch.end === 0) break
243
+ head.batch.start++
244
+ }
245
+ }
233
246
 
234
247
  const batch = []
235
248
  const result = { undo, batch, index }
@@ -239,18 +252,26 @@ async function getOplogBatch(undo, core, length, weight, index) {
239
252
  const end = seq + head.batch.end // skip head, we have it
240
253
 
241
254
  for (let i = start; i < end; i++) {
242
- remaining.push(core.get(i))
255
+ remaining.push(core.get(i, readOpts))
243
256
  }
244
257
 
245
258
  if (remaining.length) {
246
259
  const blocks = await Promise.all(remaining)
247
260
  for (let i = 0; i < blocks.length; i++) {
261
+ // keep the hole so the gap in the batch stays visible
262
+ if (blocks[i] === null) {
263
+ batch.push(null)
264
+ continue
265
+ }
266
+
248
267
  const oplog = encoding.decodeOplog(blocks[i])
249
268
  batch.push(createNode(core, i + start + 1, weight, oplog))
250
269
  }
251
270
  }
252
271
 
253
272
  batch.push(head)
273
+ finalizeNodeBatch(batch)
274
+
254
275
  return result
255
276
  }
256
277
 
@@ -259,7 +280,9 @@ function getLocalBatch(undo, writer, length, weight, index) {
259
280
 
260
281
  if (length > writer.core.length) {
261
282
  if (!writer.pending) {
262
- throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}`)
283
+ throw new Error(
284
+ `No local nodes: looking for ${length}, found ${writer.core.length}@${writer.core.id}`
285
+ )
263
286
  }
264
287
 
265
288
  let i = 0
@@ -269,9 +292,10 @@ function getLocalBatch(undo, writer, length, weight, index) {
269
292
  }
270
293
 
271
294
  const head = writer.pending[i]
295
+ const headBatch = head.batch || { start: 0, end: 0 }
272
296
 
273
- const start = i - head.batch.start
274
- const end = i + head.batch.end + 1 // include head
297
+ const start = i - headBatch.start
298
+ const end = i + headBatch.end + 1 // include head
275
299
 
276
300
  const batch = writer.pending.slice(start, end)
277
301
  return { undo, batch, index }
@@ -280,6 +304,19 @@ function getLocalBatch(undo, writer, length, weight, index) {
280
304
  return getOplogBatch(undo, writer.core, length, weight, index)
281
305
  }
282
306
 
307
+ // tolerates legacy nodes instead of asserting
308
+ async function* oplogIterator(bee) {
309
+ try {
310
+ for await (const node of bee.createChangesStream({ wait: false })) {
311
+ const legacy = isLegacyNode(node)
312
+ const refs = getOplogCompat(node)
313
+ if (refs.length) yield { node, refs, legacy }
314
+ }
315
+ } catch {
316
+ yield null // signal data not available
317
+ }
318
+ }
319
+
283
320
  function getOplog(data) {
284
321
  const result = { ref: null, info: null }
285
322
  for (const { keys } of data.batch) {
@@ -305,6 +342,35 @@ function getOplog(data) {
305
342
  asserts.bail('Bad system node')
306
343
  }
307
344
 
345
+ // legacy nodes carry no isOplog flag so we cannot tell which writer update is
346
+ // the oplog to fetch - yield every candidate until we have a better signal
347
+ function getOplogCompat(data) {
348
+ const undo = data.tail || EMPTY_HEAD
349
+ const oplogs = []
350
+ const candidates = []
351
+
352
+ for (const { keys } of data.batch) {
353
+ for (const k of keys) {
354
+ if (k.key[0] !== 1) continue
355
+
356
+ let value = null
357
+ try {
358
+ value = encoding.decodeSystemWriter(k.key, k.value)
359
+ } catch {
360
+ continue
361
+ }
362
+
363
+ // writer was just added, nothing to fetch
364
+ if (!value.length) continue
365
+
366
+ if (value.isOplog) oplogs.push({ undo, oplog: value })
367
+ else candidates.push({ undo, oplog: value })
368
+ }
369
+ }
370
+
371
+ return oplogs.length ? oplogs : candidates
372
+ }
373
+
308
374
  function isLinking(node, link) {
309
375
  if (node.length >= link.length && b4a.equals(node.key, link.key)) {
310
376
  return true
@@ -335,11 +401,3 @@ function isLegacyNode(data) {
335
401
  }
336
402
  return false
337
403
  }
338
-
339
- function sameNode(a, b) {
340
- return b4a.equals(a.key, b.key) && a.length === b.length
341
- }
342
-
343
- function getBatch(b) {
344
- return b.batch
345
- }
package/lib/trusted.js ADDED
@@ -0,0 +1,44 @@
1
+ const b4a = require('b4a')
2
+
3
+ class TrustedPeers {
4
+ constructor(hooks) {
5
+ this.trusted = new Map()
6
+ this.pending = new Map()
7
+ this.hooks = hooks
8
+ }
9
+
10
+ async isTrusted(key, reference) {
11
+ const hex = b4a.toString(key, 'hex')
12
+ if (this.trusted.get(hex)) return true
13
+
14
+ let promise = this.pending.get(hex)
15
+ if (promise) return promise
16
+
17
+ promise = this.hooks.isTrusted
18
+ ? this.hooks.isTrusted(key, reference)
19
+ : Promise.resolve(!this.hooks.mostRecentTrusted)
20
+ this.pending.set(hex, promise)
21
+
22
+ const trusted = await promise
23
+ if (trusted) this.trusted.set(hex, trusted)
24
+
25
+ if (this.pending.get(hex) === promise) this.pending.delete(hex)
26
+ return trusted
27
+ }
28
+
29
+ read(trusted) {
30
+ return trusted || null
31
+ }
32
+
33
+ mostRecentTrusted(target, reference) {
34
+ if (!this.hooks.mostRecentTrusted) return Promise.resolve(null)
35
+ return this.hooks.mostRecentTrusted(target, reference)
36
+ }
37
+
38
+ clear() {
39
+ this.pending.clear()
40
+ this.trusted.clear()
41
+ }
42
+ }
43
+
44
+ module.exports = TrustedPeers
package/lib/updates.js CHANGED
@@ -1,5 +1,3 @@
1
- const b4a = require('b4a')
2
-
3
1
  module.exports = class UpdateChanges {
4
2
  constructor(auto) {
5
3
  this.auto = auto
@@ -21,57 +19,22 @@ module.exports = class UpdateChanges {
21
19
  flushes: this.auto.system.flushes,
22
20
  from: this.tracking[k].head(),
23
21
  to: null,
24
- shared: null
22
+ incremental: false
25
23
  })
26
24
  }
27
25
  }
28
26
 
29
27
  finalise() {
30
- const floor = this.auto.system.shared
31
-
32
28
  // flush count at `to` - safe to read live, state is frozen for the hook
33
29
  const flushes = this.auto.system.flushes
34
30
 
35
31
  for (const k in this.tracking) {
36
32
  const tracked = this.byName.get(k)
37
- const startFlushes = tracked.flushes
38
33
 
39
34
  tracked.to = this.tracking[k].head()
40
35
  tracked.flushes = flushes
41
-
42
- if (floor && floor.flushes < startFlushes) {
43
- tracked.shared = k === 'view' ? floor.view : floor.system
44
- } else {
45
- tracked.shared = tracked.from
46
- }
47
36
  }
48
37
 
49
38
  return this.byName
50
39
  }
51
-
52
- static from(shared, current) {
53
- if (
54
- shared.flushes === current.flushes &&
55
- shared.system.length === current.system.length &&
56
- (shared.system.key === current.system.key ||
57
- (shared.system.key &&
58
- current.system.key &&
59
- b4a.equals(shared.system.key, current.system.key)))
60
- ) {
61
- return null
62
- }
63
-
64
- const byName = new Map()
65
- const flushes = current.flushes
66
-
67
- byName.set('view', { from: shared.view, shared: shared.view, to: current.view, flushes })
68
- byName.set('system', {
69
- from: shared.system,
70
- shared: shared.system,
71
- to: current.system,
72
- flushes
73
- })
74
-
75
- return byName
76
- }
77
40
  }
package/lib/witness.js CHANGED
@@ -1,94 +1,63 @@
1
1
  const b4a = require('b4a')
2
- const c = require('compact-encoding')
3
- const crypto = require('hypercore-crypto')
4
- const Hypercore = require('hypercore')
5
- const { NS_WITNESS } = require('./constants.js')
2
+ const encoding = require('./encoding.js')
6
3
 
7
4
  exports.currentWeight = currentWeight
8
5
  exports.resolveWeight = resolveWeight
9
- exports.verifyWitness = verifyWitness
10
- exports.attestationMessage = attestationMessage
11
- exports.signAttestation = signAttestation
6
+ exports.verifyClaim = verifyClaim
12
7
 
13
- // resolved current weight of a writer
14
8
  function currentWeight(rec) {
15
9
  if (!rec) return 0
16
10
  if (rec.maxWeight > 0 && rec.isGenesis) return rec.maxWeight
17
11
  return rec.weight
18
12
  }
19
13
 
20
- // resolve witness weight to sort weight according to current system
14
+ function seedWeight(auto, node) {
15
+ if (node.length === 1 && b4a.equals(node.key, auto.key)) return auto.bootstrapWeight
16
+ return 0
17
+ }
18
+
21
19
  async function resolveWeight(auto, node) {
22
20
  const rec = await auto.system.get(node.key)
23
- const prev = currentWeight(rec)
21
+ const prev = rec ? currentWeight(rec) : seedWeight(auto, node)
24
22
 
25
23
  const witness = node.witness
26
- if (!witness || witness.weight <= prev) return prev
24
+ if (!witness) return prev
27
25
 
28
- const limit = rec ? rec.maxWeight : 0
29
- if (witness.weight > limit) return prev
26
+ const { weight, link } = witness
27
+ if (weight <= prev) return prev
28
+ if (!(await verifyClaim(auto, node.key, weight, link))) return prev
30
29
 
31
- // a node never influences its own sort key
32
- if (b4a.equals(witness.backer.key, node.key)) return prev
30
+ return weight
31
+ }
33
32
 
34
- const backer = await auto.system.get(witness.backer.key)
35
- if (!backer || backer.length < witness.backer.length) return prev
33
+ async function verifyClaim(auto, key, weight, link) {
34
+ if (!link || !link.length) return false
36
35
 
37
- if (!verifyWitness(node)) return prev
36
+ const anchor = await readOplogNode(auto, link.key, link.length)
37
+ const entry = anchor.approvals ? anchor.approvals.find((a) => b4a.equals(a.key, key)) : null
38
+ if (!entry || entry.weight < weight) return false
38
39
 
39
- return Math.max(prev, witness.weight)
40
+ return approverQualified(auto, link.key, entry.weight)
40
41
  }
41
42
 
42
- // a pure function of the node's own bytes - no core session, no store
43
- // lookup, no IO at all, so it returns the same verdict on every peer at
44
- // every point in time, including peers that fast-forwarded past the
45
- // backer's history and never ingested its blocks. the inlined manifest is
46
- // bound to the cited backer key (a v1+ core key is the manifest hash), the
47
- // signer public key read out of it, and the signature checked against that
48
- function verifyWitness(node) {
49
- const witness = node.witness
50
- if (!witness || !witness.backer || witness.weight === 0) return false
51
- if (b4a.equals(witness.backer.key, node.key)) return false
52
-
53
- const publicKey = signerFromRawManifest(witness.backer.key, witness.backer.manifest)
54
- if (!publicKey) return false
43
+ // the approval sits in the causal past of any citation of it, so the approver's
44
+ // record is lower-bounded by the standing it held when it approved - no need to
45
+ // walk its own witness chain back to genesis
46
+ async function approverQualified(auto, key, weight) {
47
+ if (b4a.equals(key, auto.key)) return auto.bootstrapWeight >= weight
55
48
 
56
- const message = attestationMessage(
57
- witness.backer.key,
58
- node.key,
59
- witness.backer.length,
60
- witness.weight
61
- )
62
- return crypto.verify(message, witness.backer.signature, publicKey)
49
+ const rec = await auto.system.get(key)
50
+ return currentWeight(rec) >= weight
63
51
  }
64
52
 
65
- function signerFromRawManifest(backerKey, raw) {
66
- if (!raw || !raw.length) return null
53
+ async function readOplogNode(auto, key, length) {
54
+ const core = auto.openCore(key)
67
55
 
68
- let manifest
69
56
  try {
70
- // throws on malformed bytes AND when the manifest does not match the key
71
- manifest = Hypercore.parseManifest(raw, backerKey)
72
- } catch {
73
- return null
57
+ await core.ready()
58
+ const block = await core.get(length - 1)
59
+ return encoding.decodeOplog(block)
60
+ } finally {
61
+ await core.close()
74
62
  }
75
-
76
- // single-signer backers only: an attestation is one writer's testimony
77
- if (!manifest || !manifest.signers || manifest.signers.length !== 1) return null
78
-
79
- return manifest.signers[0].publicKey
80
- }
81
-
82
- function attestationMessage(backerKey, key, length, weight) {
83
- return b4a.concat([
84
- NS_WITNESS,
85
- backerKey,
86
- key,
87
- c.encode(c.uint, length),
88
- c.encode(c.uint, weight)
89
- ])
90
- }
91
-
92
- function signAttestation(secretKey, backerKey, key, length, weight) {
93
- return crypto.sign(attestationMessage(backerKey, key, length, weight), secretKey)
94
63
  }