autobee 2.0.0-rc.9 → 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,34 +38,14 @@ 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
- }
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
- }
41
+ const refs = getOplogCompat(data)
42
+ if (!refs.length) continue
54
43
 
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
 
69
51
  // nodes we cannot resolve are returned as null rather than dropped, so callers
@@ -86,48 +68,6 @@ async function replay(auto) {
86
68
  return inflated.flatMap((b) => (b === null ? [null] : b.batch))
87
69
  }
88
70
 
89
- // tolerates legacy nodes instead of asserting
90
- async function* oplogIterator(bee) {
91
- try {
92
- for await (const node of bee.createChangesStream({ wait: false })) {
93
- const legacy = isLegacyNode(node)
94
- const refs = getOplogs(node)
95
- if (refs.length) yield { node, refs, legacy }
96
- }
97
- } catch {
98
- yield null // signal data not available
99
- }
100
- }
101
-
102
- // legacy nodes carry no isOplog flag so we cannot tell which writer update is
103
- // the oplog to fetch - yield every candidate until we have a better signal
104
- function getOplogs(data) {
105
- const undo = data.tail || EMPTY_HEAD
106
- const oplogs = []
107
- const candidates = []
108
-
109
- for (const { keys } of data.batch) {
110
- for (const k of keys) {
111
- if (k.key[0] !== 1) continue
112
-
113
- let value = null
114
- try {
115
- value = encoding.decodeSystemWriter(k.key, k.value)
116
- } catch {
117
- continue
118
- }
119
-
120
- // writer was just added, nothing to fetch
121
- if (!value.length) continue
122
-
123
- if (value.isOplog) oplogs.push({ undo, oplog: value })
124
- else candidates.push({ undo, oplog: value })
125
- }
126
- }
127
-
128
- return oplogs.length ? oplogs : candidates
129
- }
130
-
131
71
  async function sort(auto, batch) {
132
72
  const node = batch[0]
133
73
  const ch = auto.system.bee.createChangesStream()
@@ -136,7 +76,7 @@ async function sort(auto, batch) {
136
76
  for await (const data of ch) {
137
77
  if (isLegacyNode(data)) break
138
78
 
139
- const { ref, info } = getOplog(data)
79
+ const { ref } = getOplog(data)
140
80
  const link = {
141
81
  key: ref.oplog.key,
142
82
  length: ref.oplog.length,
@@ -178,8 +118,7 @@ function addSorted(list, entry) {
178
118
  clock.add(link)
179
119
  }
180
120
 
181
- // backer MUST precede node
182
- if (node.witness) clock.add(node.witness.backer)
121
+ if (node.witness) clock.add(node.witness.link)
183
122
 
184
123
  const tip = []
185
124
  const linked = []
@@ -242,7 +181,9 @@ function cmp(a, b) {
242
181
  return a.length < b.length ? -1 : 1
243
182
  }
244
183
 
245
- async function inflate(auto, reorder, opts = null) {
184
+ async function inflate(auto, reorder, { wait = true, timeout = DEFAULT_OP_TIMEOUT } = {}) {
185
+ const readOpts = wait ? { timeout } : { wait: false }
186
+
246
187
  const promises = []
247
188
  const sessions = new Map()
248
189
 
@@ -271,7 +212,7 @@ async function inflate(auto, reorder, opts = null) {
271
212
  sessions.set(id, core)
272
213
  }
273
214
 
274
- promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, opts))
215
+ promises.push(getOplogBatch(ref.undo, core, ref.oplog.length, weight, i, readOpts))
275
216
  }
276
217
 
277
218
  try {
@@ -281,12 +222,27 @@ async function inflate(auto, reorder, opts = null) {
281
222
  }
282
223
  }
283
224
 
284
- async function getOplogBatch(undo, core, length, weight, index, opts = null) {
225
+ async function getOplogBatch(undo, core, length, weight, index, readOpts = null) {
285
226
  const seq = length - 1
286
- const block = await core.get(seq, opts)
227
+
228
+ const block = await core.get(seq, readOpts)
287
229
  if (block === null) return null
230
+
288
231
  const oplog = encoding.decodeOplog(block)
289
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
+ }
290
246
 
291
247
  const batch = []
292
248
  const result = { undo, batch, index }
@@ -296,7 +252,7 @@ async function getOplogBatch(undo, core, length, weight, index, opts = null) {
296
252
  const end = seq + head.batch.end // skip head, we have it
297
253
 
298
254
  for (let i = start; i < end; i++) {
299
- remaining.push(core.get(i, opts))
255
+ remaining.push(core.get(i, readOpts))
300
256
  }
301
257
 
302
258
  if (remaining.length) {
@@ -314,6 +270,8 @@ async function getOplogBatch(undo, core, length, weight, index, opts = null) {
314
270
  }
315
271
 
316
272
  batch.push(head)
273
+ finalizeNodeBatch(batch)
274
+
317
275
  return result
318
276
  }
319
277
 
@@ -322,7 +280,9 @@ function getLocalBatch(undo, writer, length, weight, index) {
322
280
 
323
281
  if (length > writer.core.length) {
324
282
  if (!writer.pending) {
325
- 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
+ )
326
286
  }
327
287
 
328
288
  let i = 0
@@ -332,9 +292,10 @@ function getLocalBatch(undo, writer, length, weight, index) {
332
292
  }
333
293
 
334
294
  const head = writer.pending[i]
295
+ const headBatch = head.batch || { start: 0, end: 0 }
335
296
 
336
- const start = i - head.batch.start
337
- const end = i + head.batch.end + 1 // include head
297
+ const start = i - headBatch.start
298
+ const end = i + headBatch.end + 1 // include head
338
299
 
339
300
  const batch = writer.pending.slice(start, end)
340
301
  return { undo, batch, index }
@@ -343,6 +304,19 @@ function getLocalBatch(undo, writer, length, weight, index) {
343
304
  return getOplogBatch(undo, writer.core, length, weight, index)
344
305
  }
345
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
+
346
320
  function getOplog(data) {
347
321
  const result = { ref: null, info: null }
348
322
  for (const { keys } of data.batch) {
@@ -368,6 +342,35 @@ function getOplog(data) {
368
342
  asserts.bail('Bad system node')
369
343
  }
370
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
+
371
374
  function isLinking(node, link) {
372
375
  if (node.length >= link.length && b4a.equals(node.key, link.key)) {
373
376
  return true
@@ -398,11 +401,3 @@ function isLegacyNode(data) {
398
401
  }
399
402
  return false
400
403
  }
401
-
402
- function sameNode(a, b) {
403
- return b4a.equals(a.key, b.key) && a.length === b.length
404
- }
405
-
406
- function getBatch(b) {
407
- return b.batch
408
- }
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
  }