autobee 2.0.0-rc.14 → 2.0.0-rc.16

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/README.md CHANGED
@@ -78,7 +78,10 @@ Options:
78
78
  encryptionKey: Buffer, // 32-byte key to encrypt all data at rest
79
79
  encrypted: false, // set true if using encryptionKey
80
80
  keyPair: { publicKey, secretKey }, // custom signing key pair for the local writer
81
- optimistic: true // allow optimistic writes from unknown writers
81
+ optimistic: true, // allow optimistic writes from unknown writers
82
+ isTrusted (key, reference) {}, // do we trust this writer, see Fast-forward
83
+ mostRecentTrusted (target, reference) {}, // the head we vouch for, see Fast-forward
84
+ fastForward: {} // see Fast-forward
82
85
  }
83
86
  ```
84
87
 
@@ -222,6 +225,50 @@ Create an anchor node. Returns `{ key, length }`. Anchors are used to create a v
222
225
 
223
226
  `true` if the system has not yet processed any nodes. Use this to bootstrap the first writer.
224
227
 
228
+ ### Fast-forward
229
+
230
+ Fast-forward only ever deals in oplog heads — `{ key, length }` of a writer's core, never a system head.
231
+
232
+ Each flush stamps the head you vouch for into your own oplog, and peers read those stamps out of the writers they wake up on, so trust travels with the log.
233
+
234
+ #### `isTrusted(key, reference)`
235
+
236
+ Return whether `key` is a writer you trust, judged against the `reference` view.
237
+
238
+ Positive answers are cached until an undo rewinds the view, and the default is `true` unless you supply `mostRecentTrusted`, in which case it is `false`.
239
+
240
+ #### `mostRecentTrusted(target, reference)`
241
+
242
+ Return the oplog head you most recently vouched for, given the `target` view being considered.
243
+
244
+ Called at flush time to stamp your own oplog (with your view as `target` and a `null` reference), and again per candidate during discovery.
245
+
246
+ #### `fastForward.boot`
247
+
248
+ ```js
249
+ {
250
+ head: { key, length }, // oplog head to boot from
251
+ legacy: { key, length }, // pre-2.0 pointer, see below
252
+ bootCondition (target, reference) {} // optional gate on the view we would land on
253
+ }
254
+ ```
255
+
256
+ Pass one of `head` or `legacy`, not both.
257
+
258
+ Without `bootCondition` this is a single attempt that gives up if the head cannot be read; with one it parks until the condition is satisfied, which today means indefinitely if it never is.
259
+
260
+ `legacy` is for records written before boot heads were oplog heads: the key is a _system_ head and a `0` length is resolved from the core. It boots ungated - `bootCondition` does not apply - and will be removed, so don't reach for it. A bare `{ key, length }` in place of the whole struct means the same thing, older still.
261
+
262
+ #### `fastForward.conservative`
263
+
264
+ Defaults to `true`: only fast-forward onto a head a connected peer can serve whole.
265
+
266
+ The check covers the oplog head only, not the system and view cores the fast-forward then reads.
267
+
268
+ #### `await db.moveTo(head)`
269
+
270
+ Fast-forward onto an oplog head, ignoring the usual distance and conservative checks. Resolves `{ to, from }`.
271
+
225
272
  ### Encryption
226
273
 
227
274
  Pass an `encryptionKey` to encrypt all writer cores and the view at rest.
@@ -856,15 +856,28 @@ const encoding29_7 = c.array(c.frame(encoding28))
856
856
  // @autobee/oplog-message-v3
857
857
  const encoding29 = {
858
858
  preencode(state, m) {
859
+ let flags =
860
+ (m.batch ? 1 : 0) |
861
+ (m.views ? 2 : 0) |
862
+ (m.optimistic ? 8 : 0) |
863
+ (m.value ? 16 : 0) |
864
+ (m.witness ? 32 : 0) |
865
+ (m.attestations ? 64 : 0) |
866
+ (m.trusted ? 128 : 0)
867
+ if (m.views) {
868
+ flags |= m.views.view ? 4 : 0
869
+ }
870
+
859
871
  c.uint.preencode(state, m.timestamp)
860
872
  encoding29_1.preencode(state, m.links)
861
- state.end++ // max flag is 64 so always one byte
873
+ c.uint.preencode(state, flags)
862
874
 
863
875
  if (m.batch) encoding24.preencode(state, m.batch)
864
876
  if (m.views) encoding25_inline.preencode(state, m.views)
865
877
  if (m.value) c.buffer.preencode(state, m.value)
866
878
  if (m.witness) encoding29_6.preencode(state, m.witness)
867
879
  if (m.attestations) encoding29_7.preencode(state, m.attestations)
880
+ if (m.trusted) encoding29_8.preencode(state, m.trusted)
868
881
  },
869
882
  encode(state, m) {
870
883
  let flags =
@@ -873,7 +886,8 @@ const encoding29 = {
873
886
  (m.optimistic ? 8 : 0) |
874
887
  (m.value ? 16 : 0) |
875
888
  (m.witness ? 32 : 0) |
876
- (m.attestations ? 64 : 0)
889
+ (m.attestations ? 64 : 0) |
890
+ (m.trusted ? 128 : 0)
877
891
  if (m.views) {
878
892
  flags |= m.views.view ? 4 : 0
879
893
  }
@@ -887,6 +901,7 @@ const encoding29 = {
887
901
  if (m.value) c.buffer.encode(state, m.value)
888
902
  if (m.witness) encoding29_6.encode(state, m.witness)
889
903
  if (m.attestations) encoding29_7.encode(state, m.attestations)
904
+ if (m.trusted) encoding29_8.encode(state, m.trusted)
890
905
  },
891
906
  decode(state) {
892
907
  const v = c.uint.decode(state)
@@ -903,7 +918,8 @@ const encoding29 = {
903
918
  optimistic: (flags & 8) !== 0,
904
919
  value: (flags & 16) !== 0 ? c.buffer.decode(state) : null,
905
920
  witness: (flags & 32) !== 0 ? encoding29_6.decode(state) : null,
906
- attestations: (flags & 64) !== 0 ? encoding29_7.decode(state) : null
921
+ attestations: (flags & 64) !== 0 ? encoding29_7.decode(state) : null,
922
+ trusted: (flags & 128) !== 0 ? encoding29_8.decode(state) : null
907
923
  }
908
924
  }
909
925
  }
@@ -1007,12 +1023,39 @@ const encoding31 = {
1007
1023
  }
1008
1024
  }
1009
1025
 
1026
+ // @autobee/trusted-head
1027
+ const encoding32 = {
1028
+ preencode(state, m) {
1029
+ c.fixed32.preencode(state, m.key)
1030
+ c.uint.preencode(state, m.length)
1031
+ c.uint.preencode(state, m.flushes)
1032
+ },
1033
+ encode(state, m) {
1034
+ c.fixed32.encode(state, m.key)
1035
+ c.uint.encode(state, m.length)
1036
+ c.uint.encode(state, m.flushes)
1037
+ },
1038
+ decode(state) {
1039
+ const r0 = c.fixed32.decode(state)
1040
+ const r1 = c.uint.decode(state)
1041
+ const r2 = c.uint.decode(state)
1042
+
1043
+ return {
1044
+ key: r0,
1045
+ length: r1,
1046
+ flushes: r2
1047
+ }
1048
+ }
1049
+ }
1050
+
1010
1051
  // @autobee/system-info-v3.heads, deferred due to recusive use
1011
1052
  const encoding18_3 = c.array(encoding22)
1012
1053
  // @autobee/system-info-v3.indexers, deferred due to recusive use
1013
1054
  const encoding18_4 = encoding18_3
1014
1055
  // @autobee/oplog-message-v3.links, deferred due to recusive use
1015
1056
  const encoding29_1 = encoding18_3
1057
+ // @autobee/oplog-message-v3.trusted, deferred due to recusive use
1058
+ const encoding29_8 = c.array(c.frame(encoding32))
1016
1059
 
1017
1060
  function setVersion(v) {
1018
1061
  version = v
@@ -1101,6 +1144,8 @@ function getEncoding(name) {
1101
1144
  return encoding30
1102
1145
  case '@autobee/manifest-data':
1103
1146
  return encoding31
1147
+ case '@autobee/trusted-head':
1148
+ return encoding32
1104
1149
  default:
1105
1150
  throw new Error('Encoder not found ' + name)
1106
1151
  }
@@ -750,6 +750,12 @@
750
750
  "array": true,
751
751
  "type": "@autobee/attestation",
752
752
  "version": 1
753
+ },
754
+ {
755
+ "name": "trusted",
756
+ "array": true,
757
+ "type": "@autobee/trusted-head",
758
+ "version": 1
753
759
  }
754
760
  ]
755
761
  },
@@ -804,6 +810,32 @@
804
810
  "version": 1
805
811
  }
806
812
  ]
813
+ },
814
+ {
815
+ "name": "trusted-head",
816
+ "namespace": "autobee",
817
+ "compact": false,
818
+ "flagsPosition": -1,
819
+ "fields": [
820
+ {
821
+ "name": "key",
822
+ "required": true,
823
+ "type": "fixed32",
824
+ "version": 1
825
+ },
826
+ {
827
+ "name": "length",
828
+ "required": true,
829
+ "type": "uint",
830
+ "version": 1
831
+ },
832
+ {
833
+ "name": "flushes",
834
+ "required": true,
835
+ "type": "uint",
836
+ "version": 1
837
+ }
838
+ ]
807
839
  }
808
840
  ],
809
841
  "namespaces": [
package/index.js CHANGED
@@ -14,16 +14,17 @@ const asserts = require('./lib/asserts.js')
14
14
  const boot = require('./lib/boot.js')
15
15
  const { resolveWeight, currentWeight } = require('./lib/witness.js')
16
16
  const encoding = require('./lib/encoding.js')
17
- const Reboot = require('./lib/reboot.js')
17
+ const FastForward = require('./lib/fast-forward.js')
18
18
  const System = require('./lib/system.js')
19
19
  const ApplyCalls = require('./lib/apply-calls.js')
20
20
  const topo = require('./lib/topo.js')
21
21
  const { ActiveWriters } = require('./lib/writers.js')
22
+ const TrustedPeers = require('./lib/trusted.js')
23
+ const ApplyView = require('./lib/apply-view.js')
22
24
  const UpdateChanges = require('./lib/updates.js')
23
25
 
24
26
  const EMPTY_HEAD = { length: 0, key: null }
25
27
  const INTERRUPT = new Error('Apply interrupted')
26
- const MIN_FF_GAP = 32
27
28
 
28
29
  module.exports = class Autobee extends ReadyResource {
29
30
  constructor(store, key = null, handlers = {}) {
@@ -56,7 +57,7 @@ module.exports = class Autobee extends ReadyResource {
56
57
  this.discoveryKey = null
57
58
  this.id = null
58
59
  this.bootstrap = null
59
- this.handlers = handlers
60
+ this._handlers = handlers
60
61
  this.stats = { undos: 0, fastForwards: 0, drains: 0, applies: 0, appends: 0 }
61
62
 
62
63
  this.system = new System(this.store.namespace('system'), this.name, {
@@ -66,7 +67,7 @@ module.exports = class Autobee extends ReadyResource {
66
67
  this.system.auto = this
67
68
 
68
69
  this.bee = bee.snapshot()
69
- this.view = handlers.open ? handlers.open(this.bee, this) : this.bee
70
+ this.view = ApplyView.open(this.bee, this)
70
71
  this.optimistic = handlers.optimistic !== false // TODO: should default to false instead
71
72
 
72
73
  this.name = name // for debugging
@@ -79,18 +80,20 @@ module.exports = class Autobee extends ReadyResource {
79
80
 
80
81
  const fastForward = handlers.fastForward || {}
81
82
 
82
- // system head to boot from: migrates or fast-forwards depending on its version
83
+ // oplog head to boot from: migrates or fast-forwards depending on its version
83
84
  this.bootFrom = fastForward.boot || null
84
85
 
85
86
  // conservative (default on): only fast-forward onto a head someone can serve whole
86
87
  this._conservativeFF = fastForward.conservative !== false
87
88
 
88
- this.reboot = null
89
- this.rebooting = null
90
- this.rebootTo = null
89
+ this.trusted = new TrustedPeers(handlers)
90
+
91
+ this.ff = null
92
+ this.fastForwarding = null
93
+ this.fastForwardTo = null
91
94
 
92
95
  this._workingBee = bee
93
- this._workingView = handlers.open ? handlers.open(this._workingBee, this) : this._workingBee
96
+ this._workingView = new ApplyView(this._workingBee, this)
94
97
 
95
98
  this._localSystemStart = 0
96
99
  this._localSystemLength = 0
@@ -100,6 +103,7 @@ module.exports = class Autobee extends ReadyResource {
100
103
 
101
104
  this._appending = []
102
105
  this._draining = null
106
+ this._bootWait = null
103
107
 
104
108
  this.legacyViews = handlers.legacyViews || []
105
109
 
@@ -107,7 +111,6 @@ module.exports = class Autobee extends ReadyResource {
107
111
  this._bootingState = null
108
112
  this._bootingAll = null
109
113
 
110
- this._handlers = handlers
111
114
  this._now = handlers.now || Date.now // overridable for clock-drift tests
112
115
  this._hasApply = !!handlers.apply
113
116
  this._hasUpdate = !!handlers.update
@@ -175,6 +178,11 @@ module.exports = class Autobee extends ReadyResource {
175
178
  this.bumpSoon()
176
179
  }
177
180
 
181
+ _requestWakeup() {
182
+ const session = this._wakeup._session
183
+ if (session) session.broadcastLookup()
184
+ }
185
+
178
186
  _registerWakeup() {
179
187
  if (!this.wakeupCapability) return
180
188
  this._wakeup.recouple()
@@ -219,6 +227,7 @@ module.exports = class Autobee extends ReadyResource {
219
227
 
220
228
  async _close() {
221
229
  this._interrupting = true
230
+ if (this._bootWait !== null) this._bootWait.resolve()
222
231
  if (this._notifyHandler) this._notifyHandler.destroy()
223
232
  if (this._draining) {
224
233
  // drain may be waiting on replicated blocks
@@ -231,14 +240,14 @@ module.exports = class Autobee extends ReadyResource {
231
240
  await this._bootingAll
232
241
  } catch {}
233
242
 
234
- if (this._handlers.close) await this._handlers.close(this.view)
243
+ await ApplyView.close(this.view, this)
235
244
 
236
245
  if (this.writers) await this.writers.close()
237
246
  await this.local.close()
238
247
  await this.system.close()
239
248
  await this._wakeup.close()
240
249
  if (this.bootstrap && !this.bootstrap.closed) await this.bootstrap.close()
241
- await this._workingBee.close()
250
+ await this._workingView.close()
242
251
  await this.bee.close()
243
252
  await this.store.close()
244
253
  }
@@ -250,6 +259,8 @@ module.exports = class Autobee extends ReadyResource {
250
259
  session.clearRequests(session.activeRequests, closingError)
251
260
  }
252
261
  }
262
+
263
+ if (this.fastForwarding) this.fastForwarding.clearRequests(closingError)
253
264
  }
254
265
 
255
266
  async changesFrom({ system } = {}) {
@@ -285,6 +296,14 @@ module.exports = class Autobee extends ReadyResource {
285
296
  this._wakeup.hint(wakeup)
286
297
  }
287
298
 
299
+ // the view an oplog head points at, opened and closed through the handlers
300
+ openViewAt(oplog) {
301
+ const v = oplog.op.views.view
302
+ if (!v) return null
303
+
304
+ return new ApplyView(this.bee.checkout({ key: v.key, length: v.start + v.length }), this)
305
+ }
306
+
288
307
  openCore(key) {
289
308
  const encryption = this.encryptionKey ? new WriterEncryption(this) : null
290
309
  return this.store.get({ key, encryption })
@@ -310,7 +329,7 @@ module.exports = class Autobee extends ReadyResource {
310
329
  }
311
330
 
312
331
  if (this.bootFrom) {
313
- this.bootFrom = (await this.bootFrom) || null
332
+ this.bootFrom = getBootOption(await this.bootFrom)
314
333
  }
315
334
  }
316
335
 
@@ -398,8 +417,8 @@ module.exports = class Autobee extends ReadyResource {
398
417
 
399
418
  // @todo migration
400
419
  if (result.migration) {
401
- if (this.handlers.migrate) {
402
- view = (await this.handlers.migrate(result.migration.views)) || EMPTY_HEAD
420
+ if (this._handlers.migrate) {
421
+ view = (await this._handlers.migrate(result.migration.views)) || EMPTY_HEAD
403
422
  this._catchupMigratedNodes = result.migration.catchup
404
423
  await this._storeMigratedView(view)
405
424
  }
@@ -441,7 +460,7 @@ module.exports = class Autobee extends ReadyResource {
441
460
  async _bump() {
442
461
  if (!(await this._bootReady())) return
443
462
 
444
- await this._flushWakeup()
463
+ if (this._bootWait !== null) this._bootWait.resolve()
445
464
 
446
465
  this.bumping++
447
466
 
@@ -502,8 +521,12 @@ module.exports = class Autobee extends ReadyResource {
502
521
  this.stats.drains++
503
522
 
504
523
  if (this.bootFrom) {
505
- await this._initFromHead(this.bootFrom)
524
+ const { head = null, legacy = null, bootCondition = null } = this.bootFrom
525
+
506
526
  this.bootFrom = null
527
+
528
+ if (legacy) await this._bootFromSystem(legacy)
529
+ else if (head) await this._bootFromHead(head, bootCondition)
507
530
  }
508
531
 
509
532
  // tracked across drain iteration
@@ -525,8 +548,11 @@ module.exports = class Autobee extends ReadyResource {
525
548
 
526
549
  try {
527
550
  while (!this._interrupting) {
528
- if (this.rebootTo !== null) {
529
- await this._applyReboot()
551
+ await this._flushWakeup()
552
+ if (this._interrupting) break
553
+
554
+ if (this.fastForwardTo !== null) {
555
+ await this._applyFastForward()
530
556
  break // revaluate conditions...
531
557
  }
532
558
 
@@ -593,50 +619,100 @@ module.exports = class Autobee extends ReadyResource {
593
619
 
594
620
  async _flushWakeup() {
595
621
  const hints = this._wakeup.flush()
622
+ if (!hints.size) return
596
623
 
597
624
  this.previousDrain = Date.now()
598
- this.rebootFromHeads(hints).catch(noop)
599
625
 
600
626
  for (const [hex, length] of hints) {
601
627
  const key = b4a.from(hex, 'hex')
602
628
  if (this.writers.has(hex)) continue
603
629
  await this.writers.wakeup(key, length)
604
630
  }
605
- }
606
631
 
607
- async _getOplog(key, length, opts = null) {
608
- const core = this.openCore(key)
632
+ // a scheduled fast-forward is applied by the drain before we look again
633
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) return
634
+ if (this._interrupting || this.closing || this.bootFrom) return
609
635
 
610
636
  try {
611
- await core.ready()
637
+ const heads = await this._readCandidateHeads(hints, FastForward.DEFAULT_TIMEOUT)
612
638
 
613
- const target = length >= 0 ? length : core.length
614
- if (target === 0) return null
639
+ const ff = await FastForward.fromHeads(this, heads, {
640
+ timeout: FastForward.DEFAULT_TIMEOUT
641
+ })
615
642
 
616
- // conservative: only proceed when a connected peer can serve the head whole
617
- if (opts && opts.conservative && core.remoteContiguousLength < target) return null
643
+ if (ff !== null) await this._runFastForward(ff)
644
+ } catch (err) {
645
+ safetyCatch(err)
646
+ }
647
+ }
618
648
 
619
- const buf = await core.get(target - 1, opts)
620
- if (buf === null) return null
649
+ async _readCandidateHeads(hints, timeout) {
650
+ const promises = []
621
651
 
622
- let op = encoding.decodeOplog(buf)
652
+ for (const [hex, length] of hints) {
653
+ if (length === 0) continue
654
+ const key = b4a.from(hex, 'hex')
655
+ promises.push(this._getOplog(key, length, timeout ? { timeout } : null))
656
+ }
623
657
 
624
- // legacy nodes always inflate, but only indexers carry views - callers
625
- // that need system info must check op.views
626
- if (op.version < 3) {
627
- op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
628
- }
658
+ const ops = await Promise.all(promises)
659
+ const heads = []
629
660
 
630
- return {
631
- key,
632
- length: target,
633
- op
661
+ for (const res of ops) {
662
+ if (res === null) continue
663
+
664
+ // the head we were woken on is a candidate in its own right
665
+ heads.push({ key: res.key, length: res.length })
666
+
667
+ if (!res.op.trusted) continue
668
+
669
+ for (const trusted of res.op.trusted) {
670
+ const head = this.trusted.read(trusted)
671
+ if (head !== null) heads.push(head)
634
672
  }
673
+ }
674
+
675
+ return heads
676
+ }
677
+
678
+ async _getOplog(key, length, opts = null) {
679
+ const core = this.openCore(key)
680
+
681
+ try {
682
+ return await this.readOplog(core, length, opts)
635
683
  } finally {
636
684
  await core.close()
637
685
  }
638
686
  }
639
687
 
688
+ // reads on a core the caller owns, so a walk does not churn a session per step
689
+ async readOplog(core, length, opts = null) {
690
+ await core.ready()
691
+
692
+ const target = length >= 0 ? length : core.length
693
+ if (target === 0) return null
694
+
695
+ // conservative: only proceed when a connected peer can serve the head whole
696
+ if (opts && opts.conservative && core.remoteContiguousLength < target) return null
697
+
698
+ const buf = await core.get(target - 1, opts)
699
+ if (buf === null) return null
700
+
701
+ let op = encoding.decodeOplog(buf)
702
+
703
+ // legacy nodes always inflate, but only indexers carry views - callers
704
+ // that need system info must check op.views
705
+ if (op.version < 3) {
706
+ op = await this._inflateLegacyOplog(buf, core, target - 1, opts)
707
+ }
708
+
709
+ return {
710
+ key: core.key,
711
+ length: target,
712
+ op
713
+ }
714
+ }
715
+
640
716
  async _inflateLegacyOplog(buf, core, seq, opts = null) {
641
717
  const m = encoding.decodeRawOplog(buf)
642
718
 
@@ -734,7 +810,7 @@ module.exports = class Autobee extends ReadyResource {
734
810
  await this.bootstrap.setUserData('autobee/local', this.local.key)
735
811
  await oldLocal.close()
736
812
 
737
- // done, soft reboot
813
+ // done, soft restart
738
814
  this.emit('rotate-local-writer')
739
815
  }
740
816
 
@@ -923,6 +999,7 @@ module.exports = class Autobee extends ReadyResource {
923
999
 
924
1000
  if (t.undo) {
925
1001
  this.stats.undos++
1002
+ this.trusted.clear()
926
1003
  t.view = await this.system.undo(t.undo)
927
1004
  }
928
1005
 
@@ -973,7 +1050,7 @@ module.exports = class Autobee extends ReadyResource {
973
1050
  if (this._hasApply && (await this.system.canApply(batch[0].key, optimistic))) {
974
1051
  this.stats.applies++
975
1052
  this._host.applying = batch
976
- await this._handlers.apply(userBatch, this._workingView, this._host)
1053
+ await this._workingView.apply(userBatch)
977
1054
  this._host.applying = null
978
1055
  }
979
1056
 
@@ -1119,135 +1196,130 @@ module.exports = class Autobee extends ReadyResource {
1119
1196
  this._localViewLength = 0
1120
1197
  }
1121
1198
 
1122
- async rebootFromHeads(hints, { force = false } = {}) {
1123
- if (!this._handlers.onwakeup) return false
1124
- if (this._interrupting) return false
1125
- if (!hints.size || this.rebooting || this.rebootTo || this.bootFrom) {
1126
- return false
1127
- }
1128
-
1129
- const promises = []
1130
- const heads = []
1131
- for (const [hex, length] of hints) {
1132
- if (length === 0) continue
1133
- const key = b4a.from(hex, 'hex')
1134
- heads.push({ key, length })
1135
- promises.push(this._getOplog(key, length))
1136
- }
1199
+ async moveTo(head) {
1200
+ if (!this.opened) await this.ready()
1201
+ if (this.closing) throw new Error('Autobee closed')
1137
1202
 
1138
- const ops = await Promise.all(promises)
1139
- if (this.rebooting || this.rebootTo) return false
1203
+ const ff = await FastForward.fromHead(this, head, null, { force: true })
1204
+ if (ff === null) return null
1140
1205
 
1141
- let best = null
1142
- let bestFlushes = -1
1206
+ if (!(await this._runFastForward(ff))) return null
1143
1207
 
1144
- for (let i = 0; i < ops.length; i++) {
1145
- const res = ops[i]
1146
- if (res === null) continue
1208
+ return this.ff.promise
1209
+ }
1147
1210
 
1148
- if (res.op.views && res.op.views.flushes > bestFlushes) {
1149
- bestFlushes = res.op.views.flushes
1150
- best = res
1151
- }
1211
+ // legacy: ungated boot straight onto a system head
1212
+ async _bootFromSystem(system) {
1213
+ try {
1214
+ const ff = new FastForward(this, system, null, { timeout: FastForward.DEFAULT_TIMEOUT })
1215
+ return await this._runFastForward(ff)
1216
+ } catch (err) {
1217
+ safetyCatch(err)
1218
+ return false
1152
1219
  }
1220
+ }
1153
1221
 
1154
- if (best && force) {
1155
- return this._rebootFromHead(best, null, { force: true })
1222
+ // the boot head seeds the view every trust decision here is made against,
1223
+ // since our own view is still empty
1224
+ async _bootFromHead(head, bootCondition) {
1225
+ // just a head: one attempt, we do not wait around if it cannot be read
1226
+ if (bootCondition === null) {
1227
+ try {
1228
+ const ff = await FastForward.fromHead(this, head, null, {
1229
+ force: true,
1230
+ timeout: FastForward.DEFAULT_TIMEOUT
1231
+ })
1232
+
1233
+ return ff !== null && (await this._runFastForward(ff))
1234
+ } catch (err) {
1235
+ safetyCatch(err)
1236
+ return false
1237
+ }
1156
1238
  }
1157
1239
 
1158
- if (best === null || bestFlushes - this.system.flushes < MIN_FF_GAP) return false
1159
-
1160
- const head = { key: best.key, length: best.length }
1161
-
1162
- const v = best.op.views.view
1163
- const view = this.bee.checkout({ key: v.key, length: v.start + v.length })
1240
+ // a condition needs the view at head to check against, so park until we
1241
+ // can read it and something acceptable turns up
1242
+ let opened = null
1164
1243
 
1165
- let trusted = null
1166
1244
  try {
1167
- trusted = await this._handlers.onwakeup(head, view, this)
1168
- if (!trusted || this.rebooting || this.rebootTo) return false
1169
- } finally {
1170
- view.close()
1171
- }
1172
-
1173
- return this._rebootFromHead(best, trusted)
1174
- }
1175
-
1176
- async _rebootFromHead(head, trusted, { force = false, wait = true } = {}) {
1177
- const conservative = !force && this._conservativeFF
1178
-
1179
- const oplog = await this._getOplog(head.key, head.length, { conservative })
1180
- if (!oplog) return null
1245
+ while (!this._interrupting) {
1246
+ this._bootWait = rrp()
1181
1247
 
1182
- const verified = trusted ? await this._getOplog(trusted.key, trusted.length) : oplog
1183
- if (!verified) return null
1248
+ try {
1249
+ if (opened === null) opened = await this._bootReference(head)
1184
1250
 
1185
- // legacy nodes from non-indexers have no system info to reboot from
1186
- if (!oplog.op.views || !verified.op.views) return null
1251
+ if (opened !== null && (await this._bootAttempt(head, bootCondition, opened.view))) {
1252
+ return true
1253
+ }
1254
+ } catch (err) {
1255
+ safetyCatch(err)
1256
+ }
1187
1257
 
1188
- if (!force && verified.op.views.flushes - this.system.flushes < MIN_FF_GAP) {
1189
- return null
1190
- }
1258
+ if (this._interrupting) break
1191
1259
 
1192
- const tip = {
1193
- system: batchToHead(oplog.op.views.system),
1194
- verified: {
1195
- op: trusted || head,
1196
- flushes: verified.op.views.flushes
1260
+ await this._bootWait.promise
1197
1261
  }
1262
+ } finally {
1263
+ this._bootWait = null
1264
+ if (opened !== null) await opened.close()
1198
1265
  }
1199
1266
 
1200
- const moved = await this._moveTo(batchToHead(verified.op.views.system), tip)
1267
+ return false
1268
+ }
1201
1269
 
1202
- if (moved && wait) return this.reboot.promise
1270
+ async _bootAttempt(head, bootCondition, reference) {
1271
+ // peek, so the hints are still there for the drain once we have booted
1272
+ const candidates = await this._readCandidateHeads(
1273
+ this._wakeup.hints,
1274
+ FastForward.DEFAULT_TIMEOUT
1275
+ )
1203
1276
 
1204
- return null
1277
+ const ff = await FastForward.fromHeads(this, [head, ...candidates], {
1278
+ force: true,
1279
+ timeout: FastForward.DEFAULT_TIMEOUT,
1280
+ condition: bootCondition,
1281
+ reference
1282
+ })
1283
+
1284
+ return ff !== null && (await this._runFastForward(ff))
1205
1285
  }
1206
1286
 
1207
- // same as moveTo except we don't return the final promise
1208
- _initFromHead(head, tip) {
1209
- if (!head.length) {
1210
- // legacy fastForward boot
1211
- return this._runReboot(new Reboot(this, head, tip))
1212
- }
1287
+ async _bootReference(head) {
1288
+ const oplog = await FastForward.flushHead(this, head, {
1289
+ timeout: FastForward.DEFAULT_TIMEOUT
1290
+ })
1213
1291
 
1214
- return this._rebootFromHead(head, null, { force: true, wait: false })
1292
+ return oplog === null ? null : this.openViewAt(oplog)
1215
1293
  }
1216
1294
 
1217
- // head is a system head; reboots onto it, migrating in place if it's a legacy version
1218
- async _moveTo(systemHead, tip) {
1219
- if (this.rebootTo !== null || this.rebooting !== null) return false
1220
-
1221
- if (await this._runReboot(new Reboot(this, systemHead, tip))) {
1222
- return true
1295
+ async _runFastForward(ff) {
1296
+ if (this.fastForwardTo !== null || this.fastForwarding !== null) {
1297
+ await ff.close()
1298
+ return false
1223
1299
  }
1224
1300
 
1225
- return false
1226
- }
1227
-
1228
- async _runReboot(reboot) {
1229
- this.rebooting = reboot
1301
+ this.fastForwarding = ff
1230
1302
 
1231
- const result = await reboot.run()
1232
- await reboot.close()
1303
+ const result = await ff.run()
1304
+ await ff.close()
1233
1305
 
1234
- if (this.rebooting === reboot) this.rebooting = null
1306
+ if (this.fastForwarding === ff) this.fastForwarding = null
1235
1307
 
1236
1308
  if (!result) return false
1237
1309
 
1238
- this.rebootTo = result
1239
- this.reboot = rrp()
1310
+ this.fastForwardTo = result
1311
+ this.ff = rrp()
1240
1312
 
1241
1313
  this.bumpSoon()
1242
1314
 
1243
1315
  return true
1244
1316
  }
1245
1317
 
1246
- async _applyReboot() {
1318
+ async _applyFastForward() {
1247
1319
  const changes = this._hasUpdate ? new UpdateChanges(this) : null
1248
1320
  if (changes) changes.track()
1249
1321
 
1250
- const { head, tip, migrate } = this.rebootTo
1322
+ const { head, tip, migrate } = this.fastForwardTo
1251
1323
 
1252
1324
  const from = this.system.bee.head()
1253
1325
  const to = head
@@ -1255,7 +1327,7 @@ module.exports = class Autobee extends ReadyResource {
1255
1327
  this.system.bee.move(head)
1256
1328
  await this.system.reset()
1257
1329
 
1258
- this.system.shared = this.rebootTo.shared || {
1330
+ this.system.shared = this.fastForwardTo.shared || {
1259
1331
  flushes: -1,
1260
1332
  view: EMPTY_HEAD,
1261
1333
  system: EMPTY_HEAD
@@ -1263,7 +1335,7 @@ module.exports = class Autobee extends ReadyResource {
1263
1335
 
1264
1336
  // migrate is set when fast-forwarding from a legacy head
1265
1337
  if (migrate) {
1266
- const view = (await this.handlers.migrate(migrate)) || EMPTY_HEAD
1338
+ const view = (await this._handlers.migrate(migrate)) || EMPTY_HEAD
1267
1339
  await this._storeMigratedView(view)
1268
1340
  this.bee.move(view)
1269
1341
  this._workingBee.move(view)
@@ -1272,16 +1344,19 @@ module.exports = class Autobee extends ReadyResource {
1272
1344
  this._workingBee.move(this.system.view)
1273
1345
  }
1274
1346
 
1275
- this.rebootTo = null
1347
+ this.fastForwardTo = null
1276
1348
  await this.writers.refresh()
1277
1349
 
1350
+ // we moved, so ask our peers to tell us their heads again
1351
+ this._requestWakeup()
1352
+
1278
1353
  await this._storeBoot()
1279
1354
 
1280
1355
  await this._update(changes)
1281
1356
 
1282
1357
  this.stats.fastForwards++
1283
1358
  this.emit('move-to', to, from)
1284
- this.reboot.resolve({ to, from })
1359
+ this.ff.resolve({ to, from })
1285
1360
 
1286
1361
  // tip is null during boot
1287
1362
  if (!tip) return
@@ -1315,6 +1390,17 @@ function isObject(o) {
1315
1390
  return typeof o === 'object' && o && !b4a.isBuffer(o)
1316
1391
  }
1317
1392
 
1393
+ function getBootOption(boot) {
1394
+ if (!boot) return null
1395
+
1396
+ // oldest style, supported for now but will go away: a bare key is legacy
1397
+ if (boot.key) return { legacy: boot, head: null, bootCondition: null }
1398
+
1399
+ asserts.assert(!(boot.head && boot.legacy), 'Boot from either a head or a legacy pointer')
1400
+
1401
+ return boot
1402
+ }
1403
+
1318
1404
  function noop() {}
1319
1405
 
1320
1406
  function createAnchorCore(store, prologue, manifestData) {
@@ -1343,10 +1429,3 @@ function crashSoon(err) {
1343
1429
  })
1344
1430
  throw err
1345
1431
  }
1346
-
1347
- function batchToHead(b) {
1348
- return {
1349
- key: b.key,
1350
- length: b.start + b.length
1351
- }
1352
- }
@@ -0,0 +1,25 @@
1
+ module.exports = class ApplyView {
2
+ constructor(bee, auto) {
3
+ this.bee = bee
4
+ this.auto = auto
5
+ this.view = ApplyView.open(bee, auto)
6
+ }
7
+
8
+ static open(bee, auto) {
9
+ return auto._handlers.open ? auto._handlers.open(bee, auto) : bee
10
+ }
11
+
12
+ static close(view, auto) {
13
+ if (!auto._handlers.close) return Promise.resolve()
14
+ return auto._handlers.close(view)
15
+ }
16
+
17
+ apply(userBatch) {
18
+ return this.auto._handlers.apply(userBatch, this.view, this.auto._host)
19
+ }
20
+
21
+ async close() {
22
+ await ApplyView.close(this.view, this.auto)
23
+ await this.bee.close()
24
+ }
25
+ }
package/lib/boot.js CHANGED
@@ -242,6 +242,7 @@ async function getCatchupHeads(core) {
242
242
  if (entry.key[0] !== 0x01) continue
243
243
 
244
244
  const { key, length } = encoding.decodeSystemWriter(entry.key, entry.value)
245
+ if (!length) continue
245
246
  nodes.push({ key, length })
246
247
  }
247
248
 
@@ -1,5 +1,6 @@
1
1
  const safetyCatch = require('safety-catch')
2
2
  const b4a = require('b4a')
3
+ const Hypercore = require('hypercore')
3
4
  const c = require('compact-encoding')
4
5
  const crypto = require('hypercore-crypto')
5
6
 
@@ -18,13 +19,24 @@ const [NS_SIGNER_NAMESPACE] = crypto.namespace('autobase', 1)
18
19
 
19
20
  const EMPTY = b4a.alloc(0)
20
21
  const DEFAULT_OP_TIMEOUT = 5_000
22
+ const MIN_FF_GAP = 32
21
23
 
22
- module.exports = class Reboot {
24
+ // views are only stamped on the last node of a flush, so a head may sit above
25
+ // the most recent one - how far back we look for it
26
+ const MAX_FLUSH_SEARCH = 64
27
+
28
+ module.exports = class FastForward {
23
29
  constructor(auto, head, tip, { timeout = DEFAULT_OP_TIMEOUT } = {}) {
24
30
  this.auto = auto
31
+
32
+ this.activeRequests = []
33
+
34
+ // 'reboot' is a storage namespace: renaming it would derive a different
35
+ // scratch core on disk, so it stays as is
25
36
  this.system = new System(auto.store.namespace('reboot'), null, {
26
37
  getEncryptionProvider: this.auto.getSystemEncryption,
27
- encrypted: this.encrypted
38
+ encrypted: this.encrypted,
39
+ activeRequests: this.activeRequests
28
40
  })
29
41
 
30
42
  // length -1 marks an uninitialised head: we know the key but not the length
@@ -37,6 +49,164 @@ module.exports = class Reboot {
37
49
  this.cores = []
38
50
  }
39
51
 
52
+ static DEFAULT_TIMEOUT = DEFAULT_OP_TIMEOUT
53
+
54
+ static async fromHead(
55
+ auto,
56
+ head,
57
+ trusted,
58
+ { force = false, timeout = 0, condition = null, reference = null } = {}
59
+ ) {
60
+ const conservative = !force && auto._conservativeFF
61
+ const timeoutOpts = timeout ? { timeout } : null
62
+
63
+ const oplog = await FastForward.flushHead(auto, head, {
64
+ conservative,
65
+ ...timeoutOpts
66
+ })
67
+ if (!oplog) return null
68
+
69
+ const verified = trusted ? await FastForward.flushHead(auto, trusted, timeoutOpts) : oplog
70
+ if (!verified) return null
71
+
72
+ // legacy nodes from non-indexers have no system info to fast-forward from
73
+ if (!oplog.op.views || !verified.op.views) return null
74
+
75
+ if (!force && verified.op.views.flushes - auto.system.flushes < MIN_FF_GAP) {
76
+ return null
77
+ }
78
+
79
+ if (condition !== null && !(await FastForward.accepts(auto, verified, condition, reference))) {
80
+ return null
81
+ }
82
+
83
+ const tip = {
84
+ system: batchToHead(oplog.op.views.system),
85
+ verified: {
86
+ op: { key: verified.key, length: verified.length },
87
+ flushes: verified.op.views.flushes
88
+ }
89
+ }
90
+
91
+ return new FastForward(auto, batchToHead(verified.op.views.system), tip)
92
+ }
93
+
94
+ static async fromHeads(
95
+ auto,
96
+ heads,
97
+ { force = false, timeout = 0, condition = null, reference = null } = {}
98
+ ) {
99
+ if (reference === null) reference = auto._workingView.view
100
+
101
+ const promises = []
102
+
103
+ for (const head of heads) {
104
+ if (head.length === 0) continue
105
+ promises.push(FastForward.flushHead(auto, head, timeout ? { timeout } : null))
106
+ }
107
+
108
+ const ops = await Promise.all(promises)
109
+ if (auto.fastForwarding || auto.fastForwardTo) return null
110
+
111
+ const trust = await Promise.all(
112
+ ops.map((res) => {
113
+ return res === null ? false : auto.trusted.isTrusted(res.key, reference)
114
+ })
115
+ )
116
+ if (auto.fastForwarding || auto.fastForwardTo) return null
117
+
118
+ const candidates = []
119
+
120
+ let bestTrusted = null
121
+ let bestTrustedFlushes = -1
122
+
123
+ for (let i = 0; i < ops.length; i++) {
124
+ const res = ops[i]
125
+ if (res === null || !res.op.views) continue
126
+ if (!force && res.op.views.flushes - auto.system.flushes < MIN_FF_GAP) continue
127
+
128
+ if (!trust[i]) {
129
+ candidates.push(res)
130
+ continue
131
+ }
132
+
133
+ if (res.op.views.flushes > bestTrustedFlushes) {
134
+ bestTrustedFlushes = res.op.views.flushes
135
+ bestTrusted = res
136
+ }
137
+ }
138
+
139
+ if (bestTrusted !== null) {
140
+ return FastForward.fromHead(auto, bestTrusted, null, {
141
+ force,
142
+ timeout,
143
+ condition,
144
+ reference
145
+ })
146
+ }
147
+
148
+ candidates.sort((a, b) => b.op.views.flushes - a.op.views.flushes)
149
+
150
+ for (const res of candidates) {
151
+ const trusted = await FastForward.mostRecentTrusted(auto, res, reference)
152
+ if (trusted === null) continue
153
+ if (auto.fastForwarding || auto.fastForwardTo) return null
154
+
155
+ const ff = await FastForward.fromHead(auto, res, trusted, {
156
+ force,
157
+ timeout,
158
+ condition,
159
+ reference
160
+ })
161
+ if (ff !== null) return ff
162
+ }
163
+
164
+ return null
165
+ }
166
+
167
+ static async flushHead(auto, head, opts) {
168
+ const core = auto.openCore(head.key)
169
+
170
+ try {
171
+ let length = head.length
172
+
173
+ for (let i = 0; i < MAX_FLUSH_SEARCH; i++) {
174
+ const oplog = await auto.readOplog(core, length, opts)
175
+ if (oplog === null) return null
176
+ if (oplog.op.views) return oplog
177
+
178
+ length = oplog.length - 1
179
+ if (length <= 0) return null
180
+ }
181
+
182
+ return null
183
+ } finally {
184
+ await core.close()
185
+ }
186
+ }
187
+
188
+ static async accepts(auto, oplog, condition, reference) {
189
+ const opened = auto.openViewAt(oplog)
190
+ if (opened === null) return false
191
+
192
+ try {
193
+ return !!(await condition(opened.view, reference))
194
+ } finally {
195
+ await opened.close()
196
+ }
197
+ }
198
+
199
+ static async mostRecentTrusted(auto, head, reference) {
200
+ const opened = auto.openViewAt(head)
201
+ if (opened === null) return null
202
+
203
+ try {
204
+ return (await auto.trusted.mostRecentTrusted(opened.view, reference)) || null
205
+ } finally {
206
+ await opened.close()
207
+ }
208
+ }
209
+
40
210
  async run() {
41
211
  try {
42
212
  if (!this.running) this.running = this._run()
@@ -77,7 +247,12 @@ module.exports = class Reboot {
77
247
  const view = this.auto.store.get({ key: this.system.view.key, active: true })
78
248
  this.cores.push(view)
79
249
 
80
- promises.push(view.get(this.system.view.length - 1, { timeout: this.timeout }))
250
+ promises.push(
251
+ view.get(this.system.view.length - 1, {
252
+ timeout: this.timeout,
253
+ activeRequests: this.activeRequests
254
+ })
255
+ )
81
256
 
82
257
  for (const head of this.system.heads) {
83
258
  promises.push(this.system.get(head.key, { timeout: this.timeout }))
@@ -118,7 +293,12 @@ module.exports = class Reboot {
118
293
  await core.setEncryption(this.auto.getSystemEncryption())
119
294
 
120
295
  // Decode hyperbee block using hyperbee2 compat
121
- const node = decodeBlock(await core.get(this.head.length - 1, { timeout: this.timeout }))
296
+ const node = decodeBlock(
297
+ await core.get(this.head.length - 1, {
298
+ timeout: this.timeout,
299
+ activeRequests: this.activeRequests
300
+ })
301
+ )
122
302
  assert(node.keys.length > 0, 'bad system block')
123
303
 
124
304
  // Decode system info from hyperbee block using autobee compat
@@ -163,7 +343,12 @@ module.exports = class Reboot {
163
343
  await core.ready()
164
344
 
165
345
  try {
166
- if (!core.manifest) await core.get(length - 1, { timeout: this.timeout })
346
+ if (!core.manifest) {
347
+ await core.get(length - 1, {
348
+ timeout: this.timeout,
349
+ activeRequests: this.activeRequests
350
+ })
351
+ }
167
352
  } catch {
168
353
  return null
169
354
  }
@@ -191,13 +376,26 @@ module.exports = class Reboot {
191
376
  }
192
377
  }
193
378
 
379
+ // cancels every read in flight and fails any that come after
380
+ clearRequests(err = null) {
381
+ Hypercore.destroyRequests(this.activeRequests, err)
382
+ }
383
+
194
384
  async close() {
195
385
  this.destroyed = true
386
+ this.clearRequests()
196
387
  if (this.system) await this.system.close()
197
388
  for (const core of this.cores) await core.close()
198
389
  }
199
390
  }
200
391
 
392
+ function batchToHead(b) {
393
+ return {
394
+ key: b.key,
395
+ length: b.start + b.length
396
+ }
397
+ }
398
+
201
399
  function decodeLegacySystemInfo(buffer) {
202
400
  const state = { start: 0, end: buffer.length, buffer }
203
401
  const version = c.uint.decode(state)
package/lib/topo.js CHANGED
@@ -308,7 +308,7 @@ function getLocalBatch(undo, writer, length, weight, index) {
308
308
 
309
309
  if (length > writer.core.length) {
310
310
  if (!writer.pending) {
311
- throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}`)
311
+ throw new Error(`No local nodes: looking for ${length}, found ${writer.core.length}@${writer.core.id}`)
312
312
  }
313
313
 
314
314
  let i = 0
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/writers.js CHANGED
@@ -64,6 +64,9 @@ class ActiveWriters {
64
64
  w = await this.add(key)
65
65
  }
66
66
 
67
+ // prefetch the oplog head for the fast-forward that runs right after us
68
+ if (w && length > 0) w.core.get(length - 1).catch(noop)
69
+
67
70
  // TODO: signal that we've seen length on w for gc
68
71
 
69
72
  this.bump()
@@ -95,16 +98,18 @@ class ActiveWriters {
95
98
  return this.localWriter.clear()
96
99
  }
97
100
 
98
- flushLocal(views) {
101
+ async flushLocal(views) {
99
102
  const w = this.localWriter
100
103
 
101
104
  // only consume the queue when a flush will actually write
102
- if (w.pending === null || w.processed === 0) return Promise.resolve()
105
+ if (w.pending === null || w.processed === 0) return
103
106
 
104
107
  const queued = this.attestations
105
108
  this.attestations = []
106
109
 
107
- return w.flush(views, queued)
110
+ const trusted = await this.auto.trusted.mostRecentTrusted(this.auto._workingView.view, null)
111
+
112
+ return w.flush(views, queued, trusted)
108
113
  }
109
114
 
110
115
  async refresh() {
@@ -623,7 +628,8 @@ class Writer {
623
628
  batch,
624
629
  views: null,
625
630
  optimistic: optimistic && !this.writers.writable,
626
- value
631
+ value,
632
+ trusted: null
627
633
  }
628
634
 
629
635
  const node = createNode(this.core, this.appendLength++, -1, oplog)
@@ -646,12 +652,16 @@ class Writer {
646
652
  this.processed = 0
647
653
  }
648
654
 
649
- flush(views, queued) {
655
+ flush(views, queued, trusted) {
650
656
  if (this.pending === null || this.processed === 0) return Promise.resolve()
651
657
 
652
658
  const head = this.pending[this.processed - 1]
653
659
  head.views = views
654
660
 
661
+ head.trusted = trusted
662
+ ? [{ key: trusted.key, length: trusted.length, flushes: views.flushes }]
663
+ : null
664
+
655
665
  if (queued && queued.length) {
656
666
  const attestations = []
657
667
  for (const { key, weight } of queued) {
@@ -695,7 +705,8 @@ function createNode(core, length, weight, oplog) {
695
705
  batch: oplog.batch,
696
706
  views: oplog.views,
697
707
  optimistic: oplog.optimistic,
698
- value: oplog.value
708
+ value: oplog.value,
709
+ trusted: oplog.trusted || []
699
710
  }
700
711
  }
701
712
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autobee",
3
- "version": "2.0.0-rc.14",
3
+ "version": "2.0.0-rc.16",
4
4
  "description": "Bee based autobase",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Holepunch Inc.",