@bjornpagen/bumbledb-log 0.19.2 → 0.20.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/package.json +2 -2
- package/src/braids.ts +8 -6
- package/src/bytes.ts +5 -194
- package/src/chain.ts +67 -111
- package/src/codec.ts +150 -233
- package/src/descriptor.ts +67 -612
- package/src/errors.ts +91 -62
- package/src/index.ts +25 -21
- package/src/keys.ts +49 -62
- package/src/manifest.ts +81 -129
- package/src/replica.ts +82 -128
- package/src/store-s3.ts +1 -1
- package/src/store.ts +245 -229
- package/src/tenants.ts +19 -41
- package/src/vector.ts +5 -75
- package/src/writer.ts +275 -168
- package/src/value.ts +0 -336
package/src/codec.ts
CHANGED
|
@@ -1,32 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The command codec (20): one
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The command codec seat (20): one implementation reads and writes the
|
|
3
|
+
* batch wire — `crates/bumbledb-log`, reached through the sealed
|
|
4
|
+
* per-theory `LogCodec` handle the descriptor parse mints
|
|
5
|
+
* (`descriptor.codec`). This module is typed payload construction only:
|
|
6
|
+
* raw rows tag by the descriptor's layout on the way in, decoded rows
|
|
7
|
+
* cross exactly as the engine's `ValueOut` walk, and every grammar
|
|
8
|
+
* refusal carries the log core's own identity kind, minted through the
|
|
9
|
+
* bridge's `log-identities.json` table. The chain discipline
|
|
10
|
+
* (`verifyChain`) is pure slot algebra over decoded headers and stays
|
|
11
|
+
* host-side; no byte grammar lives here.
|
|
8
12
|
*/
|
|
9
13
|
|
|
14
|
+
import type {
|
|
15
|
+
FactValue,
|
|
16
|
+
LogBatchDecodeKind,
|
|
17
|
+
LogBatchEncodeKind,
|
|
18
|
+
LogOpIn,
|
|
19
|
+
ValueSpec,
|
|
20
|
+
ValueTypeSpec
|
|
21
|
+
} from "@bjornpagen/bumbledb"
|
|
22
|
+
import { internalLogDecodeBatch, internalLogEncodeBatch } from "@bjornpagen/bumbledb"
|
|
10
23
|
import * as errors from "@superbuilders/errors"
|
|
11
24
|
import type { Digest32 } from "#bytes.ts"
|
|
12
|
-
import {
|
|
13
|
-
import type {
|
|
25
|
+
import { bytesEqual, digest32 } from "#bytes.ts"
|
|
26
|
+
import type { ChainEntry } from "#chain.ts"
|
|
27
|
+
import type { Braid, Descriptor, Theory } from "#descriptor.ts"
|
|
14
28
|
import { braidHex, descriptorOf } from "#descriptor.ts"
|
|
15
29
|
import { refuse, refuseChain } from "#errors.ts"
|
|
16
30
|
import type { Generation } from "#keys.ts"
|
|
17
31
|
import { generation } from "#keys.ts"
|
|
18
|
-
import type { TaggedRefusal, Value } from "#value.ts"
|
|
19
|
-
import { checkAgainst, readTagged, writeTagged } from "#value.ts"
|
|
20
|
-
|
|
21
|
-
const MAGIC = utf8Encoder.encode("BDBL")
|
|
22
|
-
const VERSION = 3
|
|
23
|
-
const OP_KIND = { insert: 1, delete: 2 } as const
|
|
24
|
-
const U32_MAX = 0xffffffffn
|
|
25
32
|
|
|
26
33
|
interface Op {
|
|
27
34
|
readonly op: "insert" | "delete"
|
|
28
35
|
readonly relation: string
|
|
29
|
-
readonly rows: ReadonlyArray<readonly
|
|
36
|
+
readonly rows: ReadonlyArray<readonly FactValue[]>
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
interface BatchHeader {
|
|
@@ -39,13 +46,13 @@ interface BatchHeader {
|
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/**
|
|
42
|
-
* Encode input.
|
|
43
|
-
*
|
|
44
|
-
* `
|
|
45
|
-
* `
|
|
49
|
+
* Encode input. The handle is the fingerprint authority, so no
|
|
50
|
+
* fingerprint field exists here — encode fills the wire's from the
|
|
51
|
+
* sealed codec. `prev` is raw bytes so a short digest reaches the named
|
|
52
|
+
* `DigestWidth` refuse at this gate instead of dying at `Digest32`
|
|
53
|
+
* construction. Decode still produces a branded `BatchHeader`.
|
|
46
54
|
*/
|
|
47
55
|
interface EncodeHeader {
|
|
48
|
-
readonly fingerprint: Uint8Array
|
|
49
56
|
readonly braid: Braid
|
|
50
57
|
readonly braidGen: Generation
|
|
51
58
|
readonly prev: Uint8Array
|
|
@@ -62,246 +69,156 @@ function braidIdOf(id: Braid): number {
|
|
|
62
69
|
return Number.parseInt(id.slice(1), 16)
|
|
63
70
|
}
|
|
64
71
|
|
|
72
|
+
function asDigest(bytes: Uint8Array, at: string): Digest32 {
|
|
73
|
+
if (bytes.length !== 32) {
|
|
74
|
+
refuse({ kind: "DigestWidth" }, `${at} is not 32 bytes`)
|
|
75
|
+
}
|
|
76
|
+
return digest32(bytes)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A bridge refusal row surfaces as `ErrRefused` carrying the core's identity kind. */
|
|
80
|
+
function refuseBridge(kind: LogBatchDecodeKind | LogBatchEncodeKind, message: string): never {
|
|
81
|
+
refuse({ kind }, message)
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* unencodable.
|
|
85
|
+
* Tags one raw cell by the field's declared layout — the bridge's
|
|
86
|
+
* inbound spelling. Only the JS shape is judged here (the tag must be
|
|
87
|
+
* constructible); range, width, and interval emptiness are the core's
|
|
88
|
+
* own `Value` refusal.
|
|
71
89
|
*/
|
|
72
|
-
function
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const relation = descriptor.relationByName.get(op.relation)
|
|
86
|
-
if (relation === undefined) {
|
|
87
|
-
throw errors.new(`op cites unknown relation ${op.relation}`)
|
|
90
|
+
function taggedCell(where: string, type: ValueTypeSpec, value: FactValue): ValueSpec {
|
|
91
|
+
switch (type.kind) {
|
|
92
|
+
case "bool": {
|
|
93
|
+
if (typeof value !== "boolean") {
|
|
94
|
+
throw errors.new(`${where}: expected boolean`)
|
|
95
|
+
}
|
|
96
|
+
return { kind: "bool", value }
|
|
97
|
+
}
|
|
98
|
+
case "u64": {
|
|
99
|
+
if (typeof value !== "bigint") {
|
|
100
|
+
throw errors.new(`${where}: expected u64 bigint`)
|
|
101
|
+
}
|
|
102
|
+
return { kind: "u64", value }
|
|
88
103
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
104
|
+
case "i64": {
|
|
105
|
+
if (typeof value !== "bigint") {
|
|
106
|
+
throw errors.new(`${where}: expected i64 bigint`)
|
|
107
|
+
}
|
|
108
|
+
return { kind: "i64", value }
|
|
94
109
|
}
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
case "string": {
|
|
111
|
+
if (typeof value !== "string") {
|
|
112
|
+
throw errors.new(`${where}: expected well-formed string`)
|
|
113
|
+
}
|
|
114
|
+
if (!value.isWellFormed()) {
|
|
115
|
+
throw errors.new(`${where}: string cell is not well-formed UTF-8`)
|
|
116
|
+
}
|
|
117
|
+
return { kind: "string", value }
|
|
97
118
|
}
|
|
98
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
{ kind: "Arity", op: opIndex, relation: relation.name, row: rowIndex },
|
|
102
|
-
`op ${opIndex} relation ${relation.name} row ${rowIndex} arity ${row.length} ≠ ${relation.fields.length}`
|
|
103
|
-
)
|
|
119
|
+
case "fixedBytes": {
|
|
120
|
+
if (!(value instanceof Uint8Array)) {
|
|
121
|
+
throw errors.new(`${where}: expected ${type.len}-byte Uint8Array`)
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
})
|
|
123
|
+
return { kind: "fixedBytes", value }
|
|
124
|
+
}
|
|
125
|
+
case "interval": {
|
|
126
|
+
if (typeof value !== "object" || value instanceof Uint8Array) {
|
|
127
|
+
throw errors.new(`${where}: expected interval value`)
|
|
128
|
+
}
|
|
129
|
+
return type.element === "u64"
|
|
130
|
+
? { kind: "intervalU64", start: value.start, end: value.end }
|
|
131
|
+
: { kind: "intervalI64", start: value.start, end: value.end }
|
|
115
132
|
}
|
|
116
133
|
}
|
|
134
|
+
}
|
|
117
135
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
out.u16le(0)
|
|
127
|
-
out.bytes(fingerprint)
|
|
128
|
-
out.u32le(braidId)
|
|
129
|
-
out.u64le(header.braidGen)
|
|
130
|
-
out.bytes(prev)
|
|
131
|
-
out.u64le(header.writer)
|
|
132
|
-
out.u64le(header.timestamp)
|
|
133
|
-
out.u32le(Number(opCount))
|
|
134
|
-
for (const op of ops) {
|
|
136
|
+
/**
|
|
137
|
+
* Ops to the bridge's spelling: relation names resolve through the
|
|
138
|
+
* descriptor's vocabulary (the core never sees a name), cells tag by
|
|
139
|
+
* the layout. A cell past the layout's width has no type to tag by and
|
|
140
|
+
* refuses `Arity` here; every other judgment is the core's.
|
|
141
|
+
*/
|
|
142
|
+
function opsIn(descriptor: Descriptor, ops: readonly Op[]): LogOpIn[] {
|
|
143
|
+
return ops.map(function opIn(op, opIndex) {
|
|
135
144
|
const relation = descriptor.relationByName.get(op.relation)
|
|
136
145
|
if (relation === undefined) {
|
|
137
146
|
throw errors.new(`op cites unknown relation ${op.relation}`)
|
|
138
147
|
}
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
relation.fields.forEach(function writeCell(field, ordinal) {
|
|
148
|
-
const value = row[ordinal]
|
|
149
|
-
if (value === undefined) {
|
|
150
|
-
throw errors.new(`relation ${relation.name}: row cell ${ordinal} absent`)
|
|
148
|
+
const rows = op.rows.map(function rowIn(row, rowIndex) {
|
|
149
|
+
return row.map(function cellIn(value, ordinal) {
|
|
150
|
+
const field = relation.fields[ordinal]
|
|
151
|
+
if (field === undefined) {
|
|
152
|
+
refuse(
|
|
153
|
+
{ kind: "Arity", op: opIndex, relation: relation.name, row: rowIndex },
|
|
154
|
+
`op ${opIndex} relation ${relation.name} row ${rowIndex} cell ${ordinal} is outside the ${relation.fields.length}-field layout`
|
|
155
|
+
)
|
|
151
156
|
}
|
|
152
|
-
|
|
157
|
+
return taggedCell(`relation ${relation.name} field ${field.name}`, field.type, value)
|
|
153
158
|
})
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function asDigest(bytes: Uint8Array, at: string): Digest32 {
|
|
160
|
-
if (bytes.length !== 32) {
|
|
161
|
-
refuse({ kind: "DigestWidth" }, `${at} is not 32 bytes`)
|
|
162
|
-
}
|
|
163
|
-
return digest32(bytes)
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function readU32(reader: ByteReader, what: string): bigint {
|
|
167
|
-
return BigInt(reader.u32le(what))
|
|
159
|
+
})
|
|
160
|
+
return { kind: op.op, relation: relation.id, rows }
|
|
161
|
+
})
|
|
168
162
|
}
|
|
169
163
|
|
|
170
|
-
/**
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
|
|
178
|
-
function
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Encodes one batch through the sealed codec. The handle is the
|
|
166
|
+
* fingerprint authority: encode fills the wire's fingerprint from the
|
|
167
|
+
* sealed codec, so none rides the bridge (a short `prev` is
|
|
168
|
+
* `DigestWidth` at this gate). Braid membership, closedness, arity, and
|
|
169
|
+
* value validity are the core's refusals, crossing with their identity
|
|
170
|
+
* kinds.
|
|
171
|
+
*/
|
|
172
|
+
function encodeBatch(theory: Theory, header: EncodeHeader, ops: readonly Op[]): Uint8Array {
|
|
173
|
+
const descriptor = descriptorOf(theory)
|
|
174
|
+
const prev = asDigest(header.prev, "prev")
|
|
175
|
+
const outcome = internalLogEncodeBatch(
|
|
176
|
+
descriptor.codec,
|
|
177
|
+
{
|
|
178
|
+
braid: braidIdOf(header.braid),
|
|
179
|
+
braidGen: header.braidGen,
|
|
180
|
+
prev,
|
|
181
|
+
writer: header.writer,
|
|
182
|
+
timestamp: header.timestamp
|
|
183
|
+
},
|
|
184
|
+
opsIn(descriptor, ops)
|
|
185
|
+
)
|
|
186
|
+
if (!outcome.ok) {
|
|
187
|
+
refuseBridge(outcome.kind, outcome.message)
|
|
188
|
+
}
|
|
189
|
+
return outcome.value
|
|
185
190
|
}
|
|
186
191
|
|
|
187
|
-
/** Full parse of a batch object; refusals
|
|
192
|
+
/** Full parse of a batch object by the one grammar; refusals cross typed, never partial reads. */
|
|
188
193
|
function decodeBatch(theory: Theory, bytes: Uint8Array): DecodedBatch {
|
|
189
194
|
const descriptor = descriptorOf(theory)
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
const magic = reader.bytes(4, "magic")
|
|
197
|
-
if (!bytesEqual(magic, MAGIC)) {
|
|
198
|
-
refuse({ kind: "BadMagic" }, "batch magic is not BDBL")
|
|
195
|
+
const outcome = internalLogDecodeBatch(descriptor.codec, bytes)
|
|
196
|
+
if (!outcome.ok) {
|
|
197
|
+
refuseBridge(outcome.kind, outcome.message)
|
|
199
198
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
const flags = reader.u16le("flags")
|
|
205
|
-
if (flags !== 0) {
|
|
206
|
-
refuse({ kind: "Flags", flags }, `batch flags ${flags} must be 0`)
|
|
207
|
-
}
|
|
208
|
-
const fingerprint = digest32(reader.bytes(32, "fingerprint"))
|
|
209
|
-
if (!bytesEqual(fingerprint, descriptor.fingerprintBytes)) {
|
|
210
|
-
refuse(
|
|
211
|
-
{ kind: "FingerprintMismatch", carried: hex32(fingerprint), expected: descriptor.fingerprint },
|
|
212
|
-
"batch fingerprint does not match the descriptor"
|
|
213
|
-
)
|
|
214
|
-
}
|
|
215
|
-
const braidId = reader.u32le("braid")
|
|
216
|
-
const braid = braidHex(braidId)
|
|
217
|
-
const members = descriptor.braidMembers.get(braid)
|
|
218
|
-
if (members === undefined) {
|
|
219
|
-
refuse({ kind: "UnknownBraid", braid: braidId }, `batch braid ${braid} is not derived from this descriptor`)
|
|
220
|
-
}
|
|
221
|
-
const braidGen = generation(reader.u64le("braid generation"))
|
|
222
|
-
const prev = digest32(reader.bytes(32, "prev"))
|
|
223
|
-
const writer = reader.u64le("writer")
|
|
224
|
-
const timestamp = reader.u64le("timestamp")
|
|
225
|
-
|
|
226
|
-
const opCount = readU32(reader, "op count")
|
|
227
|
-
refuseUnbacked(opCount, reader.remaining(), MIN_OP_BYTES, "op count")
|
|
228
|
-
const ops: Op[] = []
|
|
229
|
-
for (let opIndex = 0n; opIndex < opCount; opIndex++) {
|
|
230
|
-
const op = Number(opIndex)
|
|
231
|
-
const kind = reader.u8("op kind")
|
|
232
|
-
if (kind !== OP_KIND.insert && kind !== OP_KIND.delete) {
|
|
233
|
-
refuse(
|
|
234
|
-
{ kind: "UnknownOpKind", op, opKind: kind },
|
|
235
|
-
`op ${op} kind ${kind} is unknown (3 was deleted with floor bumps)`
|
|
236
|
-
)
|
|
237
|
-
}
|
|
238
|
-
const relationId = reader.u32le("op relation")
|
|
239
|
-
const relation = descriptor.relations[relationId]
|
|
199
|
+
const batch = outcome.value
|
|
200
|
+
const ops = batch.ops.map(function opOut(op) {
|
|
201
|
+
const relation = descriptor.relations[op.relation]
|
|
240
202
|
if (relation === undefined) {
|
|
241
|
-
|
|
242
|
-
}
|
|
243
|
-
if (relation.closed) {
|
|
244
|
-
refuse({ kind: "ClosedRelation", op, relation: relationId }, `op ${op} writes closed relation ${relation.name}`)
|
|
245
|
-
}
|
|
246
|
-
if (!members.includes(relationId)) {
|
|
247
|
-
refuse(
|
|
248
|
-
{ kind: "OpRelationOutsideBraid", op, relation: relationId, braid },
|
|
249
|
-
`op ${op} relation ${relation.name} is outside braid ${braid}`
|
|
250
|
-
)
|
|
203
|
+
throw errors.new(`decoded op cites relation ${op.relation} outside the descriptor`)
|
|
251
204
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
refuseUnbacked(rowCount, reader.remaining(), minRow, "row count")
|
|
255
|
-
const rows: Value[][] = []
|
|
256
|
-
for (let rowIndex = 0n; rowIndex < rowCount; rowIndex++) {
|
|
257
|
-
const rowAt = Number(rowIndex)
|
|
258
|
-
const row: Value[] = []
|
|
259
|
-
relation.fields.forEach(function readCell(field) {
|
|
260
|
-
const at = { relation: relation.name, row: rowAt, field: field.name }
|
|
261
|
-
const where = `relation ${relation.name} row ${rowAt} field ${field.name}`
|
|
262
|
-
const refusal: TaggedRefusal = {
|
|
263
|
-
badTag(): never {
|
|
264
|
-
refuse({ kind: "TagMismatch", ...at }, `${where}: tag does not match the layout`)
|
|
265
|
-
},
|
|
266
|
-
boolByte(byte: number): never {
|
|
267
|
-
refuse({ kind: "BoolByte", ...at }, `${where}: bool byte ${byte}`)
|
|
268
|
-
},
|
|
269
|
-
invalidUtf8(): never {
|
|
270
|
-
refuse({ kind: "InvalidUtf8", ...at }, `${where}: string payload is not UTF-8`)
|
|
271
|
-
},
|
|
272
|
-
emptyInterval(): never {
|
|
273
|
-
refuse({ kind: "EmptyInterval", ...at }, `${where}: interval start does not precede its end`)
|
|
274
|
-
},
|
|
275
|
-
intervalOverflow(): never {
|
|
276
|
-
refuse({ kind: "IntervalOverflow", ...at }, `${where}: fixed interval end leaves the element domain`)
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
row.push(readTagged(reader, field.type, refusal))
|
|
280
|
-
})
|
|
281
|
-
rows.push(row)
|
|
282
|
-
}
|
|
283
|
-
ops.push({ op: kind === OP_KIND.insert ? "insert" : "delete", relation: relation.name, rows })
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (reader.remaining() !== 0) {
|
|
287
|
-
refuse(
|
|
288
|
-
{ kind: "TrailingBytes", bytes: reader.remaining() },
|
|
289
|
-
`${reader.remaining()} trailing bytes after the last op`
|
|
290
|
-
)
|
|
291
|
-
}
|
|
292
|
-
|
|
205
|
+
return { op: op.kind, relation: relation.name, rows: op.rows }
|
|
206
|
+
})
|
|
293
207
|
return {
|
|
294
|
-
header: {
|
|
208
|
+
header: {
|
|
209
|
+
// Decode already refused any batch whose fingerprint is not
|
|
210
|
+
// the handle's own, so the descriptor's is the batch's.
|
|
211
|
+
fingerprint: digest32(descriptor.fingerprintBytes),
|
|
212
|
+
braid: braidHex(batch.header.braid),
|
|
213
|
+
braidGen: generation(batch.header.braidGen),
|
|
214
|
+
prev: digest32(batch.header.prev),
|
|
215
|
+
writer: batch.header.writer,
|
|
216
|
+
timestamp: batch.header.timestamp
|
|
217
|
+
},
|
|
295
218
|
ops
|
|
296
219
|
}
|
|
297
220
|
}
|
|
298
221
|
|
|
299
|
-
interface ChainEntry {
|
|
300
|
-
readonly g: Generation
|
|
301
|
-
readonly prev: Digest32
|
|
302
|
-
readonly ts: bigint
|
|
303
|
-
}
|
|
304
|
-
|
|
305
222
|
/**
|
|
306
223
|
* The chain discipline (20 apply, step 1): one identity, three proved
|
|
307
224
|
* causes — the header's slot identity (braid and generation, both
|
|
@@ -331,5 +248,5 @@ function verifyChain(header: BatchHeader, braid: Braid, slot: Generation, chain:
|
|
|
331
248
|
}
|
|
332
249
|
}
|
|
333
250
|
|
|
334
|
-
export type { BatchHeader, ChainEntry, DecodedBatch,
|
|
251
|
+
export type { BatchHeader, ChainEntry, DecodedBatch, EncodeHeader, Op }
|
|
335
252
|
export { decodeBatch, encodeBatch, verifyChain }
|