hypercore 10.31.12 → 10.32.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/index.js +15 -4
- package/lib/batch.js +2 -2
- package/lib/core.js +87 -41
- package/lib/merkle-tree.js +27 -2
- package/lib/messages.js +139 -55
- package/lib/multisig.js +46 -45
- package/lib/replicator.js +13 -0
- package/lib/verifier.js +272 -0
- package/package.json +2 -1
- package/lib/manifest.js +0 -222
package/lib/messages.js
CHANGED
|
@@ -65,76 +65,120 @@ const signer = {
|
|
|
65
65
|
|
|
66
66
|
const signerArray = c.array(signer)
|
|
67
67
|
|
|
68
|
-
const
|
|
69
|
-
preencode (state,
|
|
70
|
-
state.
|
|
71
|
-
c.uint.preencode(state,
|
|
72
|
-
signerArray.preencode(state, m.signers)
|
|
68
|
+
const prologue = {
|
|
69
|
+
preencode (state, p) {
|
|
70
|
+
c.fixed32.preencode(state, p.hash)
|
|
71
|
+
c.uint.preencode(state, p.length)
|
|
73
72
|
},
|
|
74
|
-
encode (state,
|
|
75
|
-
c.
|
|
76
|
-
c.uint.encode(state,
|
|
77
|
-
signerArray.encode(state, m.signers)
|
|
73
|
+
encode (state, p) {
|
|
74
|
+
c.fixed32.encode(state, p.hash)
|
|
75
|
+
c.uint.encode(state, p.length)
|
|
78
76
|
},
|
|
79
77
|
decode (state) {
|
|
80
|
-
const flags = c.uint.decode(state)
|
|
81
78
|
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
signers: signerArray.decode(state)
|
|
79
|
+
hash: c.fixed32.decode(state),
|
|
80
|
+
length: c.uint.decode(state)
|
|
85
81
|
}
|
|
86
82
|
}
|
|
87
83
|
}
|
|
88
84
|
|
|
89
|
-
const
|
|
85
|
+
const manifestv0 = {
|
|
90
86
|
preencode (state, m) {
|
|
91
|
-
c.uint.preencode(state, 0) // version
|
|
92
87
|
hashes.preencode(state, m.hash)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (m.
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (m.multipleSigners) {
|
|
104
|
-
multipleSigners.preencode(state, m.multipleSigners)
|
|
88
|
+
state.end++ // type
|
|
89
|
+
|
|
90
|
+
if (m.quorum === 1 && m.signers.length === 1 && !m.allowPatch) {
|
|
91
|
+
signer.preencode(state, m.signers[0])
|
|
92
|
+
} else {
|
|
93
|
+
state.end++ // flags
|
|
94
|
+
c.uint.preencode(state, m.quorum)
|
|
95
|
+
signerArray.preencode(state, m.signers)
|
|
105
96
|
}
|
|
106
97
|
},
|
|
107
98
|
encode (state, m) {
|
|
108
|
-
c.uint.encode(state, 0) // version
|
|
109
99
|
hashes.encode(state, m.hash)
|
|
110
|
-
c.uint.encode(state, m.signer ? 1 : m.multipleSigners ? 2 : 0)
|
|
111
100
|
|
|
112
|
-
if (m.
|
|
113
|
-
c.
|
|
101
|
+
if (m.quorum === 1 && m.signers.length === 1 && !m.allowPatch) {
|
|
102
|
+
c.uint.encode(state, 1)
|
|
103
|
+
signer.encode(state, m.signers[0])
|
|
104
|
+
} else {
|
|
105
|
+
c.uint.encode(state, 2)
|
|
106
|
+
c.uint.encode(state, m.allowPatched ? 1 : 0)
|
|
107
|
+
c.uint.encode(state, m.quorum)
|
|
108
|
+
signerArray.encode(state, m.signers)
|
|
114
109
|
}
|
|
110
|
+
},
|
|
111
|
+
decode (state) {
|
|
112
|
+
const hash = hashes.decode(state)
|
|
113
|
+
const type = c.uint.decode(state)
|
|
114
|
+
|
|
115
|
+
if (type === 0) throw new Error('Type 0 is deprecated')
|
|
116
|
+
if (type > 2) throw new Error('Unknown type: ' + type)
|
|
115
117
|
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
+
if (type === 1) {
|
|
119
|
+
return {
|
|
120
|
+
version: 0,
|
|
121
|
+
hash,
|
|
122
|
+
allowPatch: false,
|
|
123
|
+
quorum: 1,
|
|
124
|
+
signers: [signer.decode(state)],
|
|
125
|
+
prologue: null
|
|
126
|
+
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
const flags = c.uint.decode(state)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
version: 0,
|
|
133
|
+
hash,
|
|
134
|
+
allowPatch: (flags & 1) !== 0,
|
|
135
|
+
quorum: c.uint.decode(state),
|
|
136
|
+
signers: signerArray.decode(state),
|
|
137
|
+
prologue: null
|
|
122
138
|
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const manifest = exports.manifest = {
|
|
143
|
+
preencode (state, m) {
|
|
144
|
+
state.end++ // version
|
|
145
|
+
if (m.version === 0) return manifestv0.preencode(state, m)
|
|
146
|
+
|
|
147
|
+
state.end++ // flags
|
|
148
|
+
hashes.preencode(state, m.hash)
|
|
149
|
+
|
|
150
|
+
c.uint.preencode(state, m.quorum)
|
|
151
|
+
signerArray.preencode(state, m.signers)
|
|
152
|
+
if (m.prologue) prologue.preencode(state, m.prologue)
|
|
153
|
+
},
|
|
154
|
+
encode (state, m) {
|
|
155
|
+
c.uint.encode(state, m.version)
|
|
156
|
+
if (m.version === 0) return manifestv0.encode(state, m)
|
|
157
|
+
|
|
158
|
+
c.uint.encode(state, (m.allowPatch ? 1 : 0) | (m.prologue ? 2 : 0))
|
|
159
|
+
hashes.encode(state, m.hash)
|
|
160
|
+
|
|
161
|
+
c.uint.encode(state, m.quorum)
|
|
162
|
+
signerArray.encode(state, m.signers)
|
|
163
|
+
if (m.prologue) prologue.encode(state, m.prologue)
|
|
123
164
|
},
|
|
124
165
|
decode (state) {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
166
|
+
const v = c.uint.decode(state)
|
|
167
|
+
if (v === 0) return manifestv0.decode(state)
|
|
168
|
+
if (v !== 1) throw new Error('Unknown version: ' + v)
|
|
127
169
|
|
|
170
|
+
const flags = c.uint.decode(state)
|
|
128
171
|
const hash = hashes.decode(state)
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
if (type > 2) throw new Error('Unknown type: ' + type)
|
|
172
|
+
const quorum = c.uint.decode(state)
|
|
173
|
+
const signers = signerArray.decode(state)
|
|
132
174
|
|
|
133
175
|
return {
|
|
176
|
+
version: 1,
|
|
134
177
|
hash,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
178
|
+
allowPatch: (flags & 1) !== 0,
|
|
179
|
+
quorum,
|
|
180
|
+
signers,
|
|
181
|
+
prologue: (flags & 2) === 0 ? null : prologue.decode(state)
|
|
138
182
|
}
|
|
139
183
|
}
|
|
140
184
|
}
|
|
@@ -831,14 +875,16 @@ oplog.header = {
|
|
|
831
875
|
external: null,
|
|
832
876
|
key: old.signer.publicKey,
|
|
833
877
|
manifest: {
|
|
878
|
+
version: 0,
|
|
834
879
|
hash: old.types.tree,
|
|
835
|
-
|
|
836
|
-
|
|
880
|
+
allowPatch: false,
|
|
881
|
+
quorum: 1,
|
|
882
|
+
signers: [{
|
|
837
883
|
signature: old.types.signer,
|
|
838
884
|
namespace: DEFAULT_NAMESPACE,
|
|
839
885
|
publicKey: old.signer.publicKey
|
|
840
|
-
},
|
|
841
|
-
|
|
886
|
+
}],
|
|
887
|
+
prologue: null
|
|
842
888
|
},
|
|
843
889
|
keyPair: old.signer.secretKey ? old.signer : null,
|
|
844
890
|
userData: old.userData,
|
|
@@ -875,7 +921,27 @@ oplog.header = {
|
|
|
875
921
|
|
|
876
922
|
const uintArray = c.array(c.uint)
|
|
877
923
|
|
|
878
|
-
const
|
|
924
|
+
const multisigInput = {
|
|
925
|
+
preencode (state, inp) {
|
|
926
|
+
c.uint.preencode(state, inp.signer)
|
|
927
|
+
c.fixed64.preencode(state, inp.signature)
|
|
928
|
+
c.uint.preencode(state, inp.patch)
|
|
929
|
+
},
|
|
930
|
+
encode (state, inp) {
|
|
931
|
+
c.uint.encode(state, inp.signer)
|
|
932
|
+
c.fixed64.encode(state, inp.signature)
|
|
933
|
+
c.uint.encode(state, inp.patch)
|
|
934
|
+
},
|
|
935
|
+
decode (state) {
|
|
936
|
+
return {
|
|
937
|
+
signer: c.uint.decode(state),
|
|
938
|
+
signature: c.fixed64.decode(state),
|
|
939
|
+
patch: c.uint.decode(state)
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
const patchEncodingv0 = {
|
|
879
945
|
preencode (state, n) {
|
|
880
946
|
c.uint.preencode(state, n.start)
|
|
881
947
|
c.uint.preencode(state, n.length)
|
|
@@ -895,29 +961,30 @@ const patchEncoding = {
|
|
|
895
961
|
}
|
|
896
962
|
}
|
|
897
963
|
|
|
898
|
-
const
|
|
964
|
+
const multisigInputv0 = {
|
|
899
965
|
preencode (state, n) {
|
|
900
966
|
state.end++
|
|
901
967
|
c.uint.preencode(state, n.signer)
|
|
902
968
|
c.fixed64.preencode(state, n.signature)
|
|
903
|
-
if (n.patch)
|
|
969
|
+
if (n.patch) patchEncodingv0.preencode(state, n.patch)
|
|
904
970
|
},
|
|
905
971
|
encode (state, n) {
|
|
906
972
|
c.uint.encode(state, n.patch ? 1 : 0)
|
|
907
973
|
c.uint.encode(state, n.signer)
|
|
908
974
|
c.fixed64.encode(state, n.signature)
|
|
909
|
-
if (n.patch)
|
|
975
|
+
if (n.patch) patchEncodingv0.encode(state, n.patch)
|
|
910
976
|
},
|
|
911
977
|
decode (state) {
|
|
912
978
|
const flags = c.uint.decode(state)
|
|
913
979
|
return {
|
|
914
980
|
signer: c.uint.decode(state),
|
|
915
981
|
signature: c.fixed64.decode(state),
|
|
916
|
-
patch: (flags & 1) ?
|
|
982
|
+
patch: (flags & 1) ? patchEncodingv0.decode(state) : null
|
|
917
983
|
}
|
|
918
984
|
}
|
|
919
985
|
}
|
|
920
986
|
|
|
987
|
+
const multisigInputArrayv0 = c.array(multisigInputv0)
|
|
921
988
|
const multisigInputArray = c.array(multisigInput)
|
|
922
989
|
|
|
923
990
|
const compactNode = {
|
|
@@ -942,19 +1009,36 @@ const compactNode = {
|
|
|
942
1009
|
|
|
943
1010
|
const compactNodeArray = c.array(compactNode)
|
|
944
1011
|
|
|
1012
|
+
exports.multiSignaturev0 = {
|
|
1013
|
+
preencode (state, s) {
|
|
1014
|
+
multisigInputArrayv0.preencode(state, s.proofs)
|
|
1015
|
+
compactNodeArray.preencode(state, s.patch)
|
|
1016
|
+
},
|
|
1017
|
+
encode (state, s) {
|
|
1018
|
+
multisigInputArrayv0.encode(state, s.proofs)
|
|
1019
|
+
compactNodeArray.encode(state, s.patch)
|
|
1020
|
+
},
|
|
1021
|
+
decode (state) {
|
|
1022
|
+
return {
|
|
1023
|
+
proofs: multisigInputArrayv0.decode(state),
|
|
1024
|
+
patch: compactNodeArray.decode(state)
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
945
1029
|
exports.multiSignature = {
|
|
946
1030
|
preencode (state, s) {
|
|
947
1031
|
multisigInputArray.preencode(state, s.proofs)
|
|
948
|
-
compactNodeArray.preencode(state, s.
|
|
1032
|
+
compactNodeArray.preencode(state, s.patch)
|
|
949
1033
|
},
|
|
950
1034
|
encode (state, s) {
|
|
951
1035
|
multisigInputArray.encode(state, s.proofs)
|
|
952
|
-
compactNodeArray.encode(state, s.
|
|
1036
|
+
compactNodeArray.encode(state, s.patch)
|
|
953
1037
|
},
|
|
954
1038
|
decode (state) {
|
|
955
1039
|
return {
|
|
956
1040
|
proofs: multisigInputArray.decode(state),
|
|
957
|
-
|
|
1041
|
+
patch: compactNodeArray.decode(state)
|
|
958
1042
|
}
|
|
959
1043
|
}
|
|
960
1044
|
}
|
package/lib/multisig.js
CHANGED
|
@@ -1,45 +1,42 @@
|
|
|
1
1
|
const c = require('compact-encoding')
|
|
2
2
|
const b4a = require('b4a')
|
|
3
|
-
const
|
|
3
|
+
const flat = require('flat-tree')
|
|
4
|
+
const { multiSignature, multiSignaturev0 } = require('./messages')
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
7
|
+
assemblev0,
|
|
6
8
|
assemble,
|
|
9
|
+
inflatev0,
|
|
7
10
|
inflate,
|
|
8
11
|
partialSignature,
|
|
9
12
|
signableLength
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
for (const proof of compressedInputs.proofs) {
|
|
17
|
-
inputs.push({
|
|
18
|
-
signer: proof.signer,
|
|
19
|
-
signature: proof.signature,
|
|
20
|
-
patch: inflateUpgrade(proof.patch, compressedInputs.nodes)
|
|
21
|
-
})
|
|
22
|
-
}
|
|
15
|
+
function inflatev0 (data) {
|
|
16
|
+
return c.decode(multiSignaturev0, data)
|
|
17
|
+
}
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
function inflate (data) {
|
|
20
|
+
return c.decode(multiSignature, data)
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
async function partialSignature (tree, signer, from, to = tree.length, signature = tree.signature) {
|
|
28
24
|
if (from > tree.length) return null
|
|
29
|
-
const
|
|
25
|
+
const nodes = to <= from ? null : await upgradeNodes(tree, from, to)
|
|
26
|
+
|
|
27
|
+
if (signature.byteLength !== 64) signature = c.decode(multiSignature, signature).proofs[0].signature
|
|
30
28
|
|
|
31
29
|
return {
|
|
32
30
|
signer,
|
|
33
31
|
signature,
|
|
34
|
-
patch
|
|
32
|
+
patch: nodes ? to - from : 0,
|
|
33
|
+
nodes
|
|
35
34
|
}
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
async function
|
|
37
|
+
async function upgradeNodes (tree, from, to) {
|
|
39
38
|
const p = await tree.proof({ upgrade: { start: from, length: to - from } })
|
|
40
|
-
p.upgrade.
|
|
41
|
-
p.upgrade.signature = null
|
|
42
|
-
return p.upgrade
|
|
39
|
+
return p.upgrade.nodes
|
|
43
40
|
}
|
|
44
41
|
|
|
45
42
|
function signableLength (lengths, quorum) {
|
|
@@ -53,15 +50,39 @@ function cmp (a, b) {
|
|
|
53
50
|
return b - a
|
|
54
51
|
}
|
|
55
52
|
|
|
53
|
+
function assemblev0 (inputs) {
|
|
54
|
+
const proofs = []
|
|
55
|
+
const patch = []
|
|
56
|
+
|
|
57
|
+
for (const u of inputs) {
|
|
58
|
+
proofs.push(compressProof(u, patch))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return c.encode(multiSignaturev0, { proofs, patch })
|
|
62
|
+
}
|
|
63
|
+
|
|
56
64
|
function assemble (inputs) {
|
|
57
65
|
const proofs = []
|
|
58
|
-
const
|
|
66
|
+
const patch = []
|
|
67
|
+
const seen = new Set()
|
|
59
68
|
|
|
60
69
|
for (const u of inputs) {
|
|
61
|
-
|
|
70
|
+
if (u.nodes) {
|
|
71
|
+
for (const node of u.nodes) {
|
|
72
|
+
if (seen.has(node.index)) continue
|
|
73
|
+
seen.add(node.index)
|
|
74
|
+
patch.push(node)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
proofs.push({
|
|
79
|
+
signer: u.signer,
|
|
80
|
+
signature: u.signature,
|
|
81
|
+
patch: u.patch
|
|
82
|
+
})
|
|
62
83
|
}
|
|
63
84
|
|
|
64
|
-
return c.encode(
|
|
85
|
+
return c.encode(multiSignature, { proofs, patch })
|
|
65
86
|
}
|
|
66
87
|
|
|
67
88
|
function compareNode (a, b) {
|
|
@@ -74,16 +95,14 @@ function compressProof (proof, nodes) {
|
|
|
74
95
|
return {
|
|
75
96
|
signer: proof.signer,
|
|
76
97
|
signature: proof.signature,
|
|
77
|
-
patch: compressUpgrade(proof
|
|
98
|
+
patch: proof.patch ? compressUpgrade(proof, nodes) : null
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
function compressUpgrade (p, nodes) {
|
|
82
|
-
if (!p) return null
|
|
83
|
-
|
|
84
103
|
const u = {
|
|
85
|
-
start: p.
|
|
86
|
-
length: p.
|
|
104
|
+
start: flat.rightSpan(p.nodes[p.nodes.length - 1].index) / 2 + 1,
|
|
105
|
+
length: p.patch,
|
|
87
106
|
nodes: []
|
|
88
107
|
}
|
|
89
108
|
|
|
@@ -103,21 +122,3 @@ function compressUpgrade (p, nodes) {
|
|
|
103
122
|
|
|
104
123
|
return u
|
|
105
124
|
}
|
|
106
|
-
|
|
107
|
-
function inflateUpgrade (s, nodes) {
|
|
108
|
-
if (!s) return null
|
|
109
|
-
|
|
110
|
-
const upgrade = {
|
|
111
|
-
start: s.start,
|
|
112
|
-
length: s.length,
|
|
113
|
-
nodes: [],
|
|
114
|
-
additionalNodes: [],
|
|
115
|
-
signature: null
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
for (const i of s.nodes) {
|
|
119
|
-
upgrade.nodes.push(nodes[i])
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return upgrade
|
|
123
|
-
}
|
package/lib/replicator.js
CHANGED
|
@@ -7,6 +7,7 @@ const RemoteBitfield = require('./remote-bitfield')
|
|
|
7
7
|
const { REQUEST_CANCELLED, REQUEST_TIMEOUT, INVALID_CAPABILITY, SNAPSHOT_NOT_AVAILABLE } = require('hypercore-errors')
|
|
8
8
|
const m = require('./messages')
|
|
9
9
|
const caps = require('./caps')
|
|
10
|
+
const { createTracer } = require('hypertrace')
|
|
10
11
|
|
|
11
12
|
const DEFAULT_MAX_INFLIGHT = [32, 512]
|
|
12
13
|
const SCALE_LATENCY = 50
|
|
@@ -268,6 +269,7 @@ class BlockTracker {
|
|
|
268
269
|
|
|
269
270
|
class Peer {
|
|
270
271
|
constructor (replicator, protomux, channel, session) {
|
|
272
|
+
this.tracer = createTracer(this, { parent: replicator.core.tracer })
|
|
271
273
|
this.core = replicator.core
|
|
272
274
|
this.replicator = replicator
|
|
273
275
|
this.stream = protomux.stream
|
|
@@ -429,6 +431,8 @@ class Peer {
|
|
|
429
431
|
}
|
|
430
432
|
|
|
431
433
|
onclose (isRemote) {
|
|
434
|
+
this.tracer.trace('onclose')
|
|
435
|
+
|
|
432
436
|
// we might have signalled to the remote that we are done (ie not downloading) and the remote might agree on that
|
|
433
437
|
// if that happens, the channel might be closed by the remote. if so just renegotiate it.
|
|
434
438
|
// TODO: add a CLOSE_REASON to mux to we can make this cleaner...
|
|
@@ -564,6 +568,8 @@ class Peer {
|
|
|
564
568
|
}
|
|
565
569
|
|
|
566
570
|
async onrequest (msg) {
|
|
571
|
+
this.tracer.trace('onrequest', msg)
|
|
572
|
+
|
|
567
573
|
if (!this.protomux.drained || this.receiverQueue.length) {
|
|
568
574
|
this.receiverQueue.push(msg)
|
|
569
575
|
return
|
|
@@ -661,6 +667,8 @@ class Peer {
|
|
|
661
667
|
}
|
|
662
668
|
|
|
663
669
|
async ondata (data) {
|
|
670
|
+
this.tracer.trace('ondata', data)
|
|
671
|
+
|
|
664
672
|
// always allow a fork conflict proof to be sent
|
|
665
673
|
if (data.request === 0 && data.upgrade && data.upgrade.start === 0) {
|
|
666
674
|
if (await this.core.checkConflict(data, this)) return
|
|
@@ -720,6 +728,8 @@ class Peer {
|
|
|
720
728
|
}
|
|
721
729
|
|
|
722
730
|
onnodata ({ request }) {
|
|
731
|
+
this.tracer.trace('onnodata', { request })
|
|
732
|
+
|
|
723
733
|
const req = request > 0 ? this.replicator._inflight.get(request) : null
|
|
724
734
|
|
|
725
735
|
if (req === null || req.peer !== this) return
|
|
@@ -1190,6 +1200,8 @@ class Peer {
|
|
|
1190
1200
|
return
|
|
1191
1201
|
}
|
|
1192
1202
|
|
|
1203
|
+
this.tracer.trace('send', req)
|
|
1204
|
+
|
|
1193
1205
|
this.wireRequest.send(req)
|
|
1194
1206
|
}
|
|
1195
1207
|
}
|
|
@@ -1203,6 +1215,7 @@ module.exports = class Replicator {
|
|
|
1203
1215
|
onupload = noop,
|
|
1204
1216
|
oninvalid = noop
|
|
1205
1217
|
} = {}) {
|
|
1218
|
+
this.tracer = createTracer(this)
|
|
1206
1219
|
this.key = key
|
|
1207
1220
|
this.discoveryKey = core.crypto.discoveryKey(key)
|
|
1208
1221
|
this.core = core
|