@ocap/merkle-tree 1.6.10
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/LICENSE +13 -0
- package/README.md +15 -0
- package/lib/abi.js +227 -0
- package/lib/index.js +194 -0
- package/package.json +29 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2018-2019 ArcBlock
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|
package/README.md
ADDED
package/lib/abi.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
/* eslint-disable func-names */
|
|
3
|
+
/* eslint-disable no-prototype-builtins */
|
|
4
|
+
/* eslint-disable prefer-rest-params */
|
|
5
|
+
// Ported from: https://github.com/ChainSafe/web3.js/blob/1.x/packages/web3-utils/src/soliditySha3.js
|
|
6
|
+
|
|
7
|
+
const Mcrypto = require('@ocap/mcrypto');
|
|
8
|
+
const { BN, isHexStrict, isBigNumber, isBN, utf8ToHex, leftPad, rightPad } = require('@ocap/util');
|
|
9
|
+
|
|
10
|
+
const keccak256 = Mcrypto.Hasher.Keccak.hash256;
|
|
11
|
+
|
|
12
|
+
const isAddress = (address) => {
|
|
13
|
+
if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (/^(0x|0X)?[0-9a-f]{40}$/.test(address) || /^(0x|0X)?[0-9A-F]{40}$/.test(address)) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return checkAddressChecksum(address);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const checkAddressChecksum = (address) => {
|
|
23
|
+
const origin = address.replace(/^0x/i, '');
|
|
24
|
+
const addressHash = keccak256(origin.toLowerCase()).replace(/^0x/i, '');
|
|
25
|
+
for (let i = 0; i < 40; i++) {
|
|
26
|
+
if (
|
|
27
|
+
(parseInt(addressHash[i], 16) > 7 && origin[i].toUpperCase() !== origin[i]) ||
|
|
28
|
+
(parseInt(addressHash[i], 16) <= 7 && origin[i].toLowerCase() !== origin[i])
|
|
29
|
+
) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const _elementaryName = function (name) {
|
|
37
|
+
if (name.startsWith('int[')) {
|
|
38
|
+
return `int256${name.slice(3)}`;
|
|
39
|
+
}
|
|
40
|
+
if (name === 'int') {
|
|
41
|
+
return 'int256';
|
|
42
|
+
}
|
|
43
|
+
if (name.startsWith('uint[')) {
|
|
44
|
+
return `uint256${name.slice(4)}`;
|
|
45
|
+
}
|
|
46
|
+
if (name === 'uint') {
|
|
47
|
+
return 'uint256';
|
|
48
|
+
}
|
|
49
|
+
if (name.startsWith('fixed[')) {
|
|
50
|
+
return `fixed128x128${name.slice(5)}`;
|
|
51
|
+
}
|
|
52
|
+
if (name === 'fixed') {
|
|
53
|
+
return 'fixed128x128';
|
|
54
|
+
}
|
|
55
|
+
if (name.startsWith('ufixed[')) {
|
|
56
|
+
return `ufixed128x128${name.slice(6)}`;
|
|
57
|
+
}
|
|
58
|
+
if (name === 'ufixed') {
|
|
59
|
+
return 'ufixed128x128';
|
|
60
|
+
}
|
|
61
|
+
return name;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Parse N from type<N>
|
|
65
|
+
const _parseTypeN = function (type) {
|
|
66
|
+
const size = /^\D+(\d+).*$/.exec(type);
|
|
67
|
+
return size ? parseInt(size[1], 10) : null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Parse N from type[<N>]
|
|
71
|
+
const _parseTypeNArray = function (type) {
|
|
72
|
+
const arraySize = /^\D+\d*\[(\d+)\]$/.exec(type);
|
|
73
|
+
return arraySize ? parseInt(arraySize[1], 10) : null;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const _parseNumber = function (arg) {
|
|
77
|
+
const type = typeof arg;
|
|
78
|
+
if (type === 'string') {
|
|
79
|
+
if (isHexStrict(arg)) {
|
|
80
|
+
return new BN(arg.replace(/0x/i, ''), 16);
|
|
81
|
+
}
|
|
82
|
+
return new BN(arg, 10);
|
|
83
|
+
}
|
|
84
|
+
if (type === 'number') {
|
|
85
|
+
return new BN(arg);
|
|
86
|
+
}
|
|
87
|
+
if (isBigNumber(arg)) {
|
|
88
|
+
return new BN(arg.toString(10));
|
|
89
|
+
}
|
|
90
|
+
if (isBN(arg)) {
|
|
91
|
+
return arg;
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`${arg} is not a number`);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const _solidityPack = function (type, value, arraySize) {
|
|
97
|
+
let size;
|
|
98
|
+
let num;
|
|
99
|
+
type = _elementaryName(type);
|
|
100
|
+
|
|
101
|
+
if (type === 'bytes') {
|
|
102
|
+
if (value.replace(/^0x/i, '').length % 2 !== 0) {
|
|
103
|
+
throw new Error(`Invalid bytes characters ${value.length}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
if (type === 'string') {
|
|
109
|
+
return utf8ToHex(value);
|
|
110
|
+
}
|
|
111
|
+
if (type === 'bool') {
|
|
112
|
+
return value ? '01' : '00';
|
|
113
|
+
}
|
|
114
|
+
if (type.startsWith('address')) {
|
|
115
|
+
if (arraySize) {
|
|
116
|
+
size = 64;
|
|
117
|
+
} else {
|
|
118
|
+
size = 40;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!isAddress(value)) {
|
|
122
|
+
throw new Error(`${value} is not a valid address, or the checksum is invalid.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return leftPad(value.toLowerCase(), size, '0');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
size = _parseTypeN(type);
|
|
129
|
+
|
|
130
|
+
if (type.startsWith('bytes')) {
|
|
131
|
+
if (!size) {
|
|
132
|
+
throw new Error('bytes[] not yet supported in solidity');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// must be 32 byte slices when in an array
|
|
136
|
+
if (arraySize) {
|
|
137
|
+
size = 32;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (size < 1 || size > 32 || size < value.replace(/^0x/i, '').length / 2) {
|
|
141
|
+
throw new Error(`Invalid bytes${size} for ${value}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return rightPad(value, size * 2, '0');
|
|
145
|
+
}
|
|
146
|
+
if (type.startsWith('uint')) {
|
|
147
|
+
if (size % 8 || size < 8 || size > 256) {
|
|
148
|
+
throw new Error(`Invalid uint${size} size`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
num = _parseNumber(value);
|
|
152
|
+
if (num.bitLength() > size) {
|
|
153
|
+
throw new Error(`Supplied uint exceeds width: ${size} vs ${num.bitLength()}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (num.lt(new BN(0))) {
|
|
157
|
+
throw new Error(`Supplied uint ${num.toString()} is negative`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return size ? leftPad(num.toString('hex'), (size / 8) * 2, '0') : num;
|
|
161
|
+
}
|
|
162
|
+
if (type.startsWith('int')) {
|
|
163
|
+
if (size % 8 || size < 8 || size > 256) {
|
|
164
|
+
throw new Error(`Invalid int${size} size`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
num = _parseNumber(value);
|
|
168
|
+
if (num.bitLength() > size) {
|
|
169
|
+
throw new Error(`Supplied int exceeds width: ${size} vs ${num.bitLength()}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (num.lt(new BN(0))) {
|
|
173
|
+
return num.toTwos(size).toString('hex');
|
|
174
|
+
}
|
|
175
|
+
return size ? leftPad(num.toString('hex'), (size / 8) * 2, '0') : num;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// FIXME: support all other types
|
|
179
|
+
throw new Error(`Unsupported or invalid type: ${type}`);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const _processSolidityEncodePackedArgs = function (arg) {
|
|
183
|
+
if (!arg || typeof arg !== 'object') {
|
|
184
|
+
throw new Error('arg must be an object');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let arraySize;
|
|
188
|
+
|
|
189
|
+
const type = arg.t || arg.type;
|
|
190
|
+
let value = arg.v || arg.value;
|
|
191
|
+
|
|
192
|
+
if ((type.startsWith('int') || type.startsWith('uint')) && typeof value === 'string' && !/^(-)?0x/i.test(value)) {
|
|
193
|
+
value = new BN(value);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// get the array size
|
|
197
|
+
if (Array.isArray(value)) {
|
|
198
|
+
arraySize = _parseTypeNArray(type);
|
|
199
|
+
if (arraySize && value.length !== arraySize) {
|
|
200
|
+
throw new Error(`${type} is not matching the given array ${JSON.stringify(value)}`);
|
|
201
|
+
} else {
|
|
202
|
+
arraySize = value.length;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (Array.isArray(value)) {
|
|
207
|
+
return value.map((val) => _solidityPack(type, val, arraySize).toString('hex').replace('0x', '')).join('');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return _solidityPack(type, value, arraySize).toString('hex').replace('0x', '');
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Encode packed args to hex
|
|
215
|
+
*
|
|
216
|
+
* @method encodePacked
|
|
217
|
+
* @return {String} the hex encoded arguments
|
|
218
|
+
*/
|
|
219
|
+
const encodePacked = function () {
|
|
220
|
+
const args = Array.prototype.slice.call(arguments);
|
|
221
|
+
const hexArgs = args.map(_processSolidityEncodePackedArgs);
|
|
222
|
+
return `0x${hexArgs.join('').toLowerCase()}`;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
encodePacked,
|
|
227
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const Mcrypto = require('@ocap/mcrypto');
|
|
2
|
+
const { toHex, toBuffer } = require('@ocap/util');
|
|
3
|
+
|
|
4
|
+
const { encodePacked } = require('./abi');
|
|
5
|
+
|
|
6
|
+
const keccak256 = Mcrypto.Hasher.Keccak.hash256;
|
|
7
|
+
const keccakFromString = (str) => toBuffer(keccak256(str));
|
|
8
|
+
|
|
9
|
+
class MerkleTree {
|
|
10
|
+
constructor(elements) {
|
|
11
|
+
// Filter empty strings and hash elements
|
|
12
|
+
this.elements = elements.filter(Boolean).map((x) => keccakFromString(x));
|
|
13
|
+
|
|
14
|
+
// Sort elements
|
|
15
|
+
this.elements.sort(Buffer.compare);
|
|
16
|
+
|
|
17
|
+
// Deduplicate elements
|
|
18
|
+
this.elements = this.bufDeduplicate(this.elements);
|
|
19
|
+
|
|
20
|
+
// Create layers
|
|
21
|
+
this.layers = this.getLayers(this.elements);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
getLayers(elements) {
|
|
25
|
+
if (elements.length === 0) {
|
|
26
|
+
return [['']];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const layers = [];
|
|
30
|
+
layers.push(elements);
|
|
31
|
+
|
|
32
|
+
// Get next layer until we reach the root
|
|
33
|
+
while (layers[layers.length - 1].length > 1) {
|
|
34
|
+
layers.push(this.getNextLayer(layers[layers.length - 1]));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return layers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getNextLayer(elements) {
|
|
41
|
+
return elements.reduce((layer, el, idx, arr) => {
|
|
42
|
+
if (idx % 2 === 0) {
|
|
43
|
+
// Hash the current element with its pair element
|
|
44
|
+
layer.push(this.combinedHash(el, arr[idx + 1]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return layer;
|
|
48
|
+
}, []);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
combinedHash(first, second) {
|
|
52
|
+
if (!first) {
|
|
53
|
+
return second;
|
|
54
|
+
}
|
|
55
|
+
if (!second) {
|
|
56
|
+
return first;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return toBuffer(keccak256(this.sortAndConcat(first, second)));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getRoot() {
|
|
63
|
+
return this.layers[this.layers.length - 1][0];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getHexRoot() {
|
|
67
|
+
return toHex(this.getRoot());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
getProof(el) {
|
|
71
|
+
let idx = this.bufIndexOf(el, this.elements);
|
|
72
|
+
|
|
73
|
+
if (idx === -1) {
|
|
74
|
+
throw new Error('Element does not exist in Merkle tree');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.layers.reduce((proof, layer) => {
|
|
78
|
+
const pairElement = this.getPairElement(idx, layer);
|
|
79
|
+
|
|
80
|
+
if (pairElement) {
|
|
81
|
+
proof.push(pairElement);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
idx = Math.floor(idx / 2);
|
|
85
|
+
|
|
86
|
+
return proof;
|
|
87
|
+
}, []);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getHexProof(el) {
|
|
91
|
+
const proof = this.getProof(el);
|
|
92
|
+
return this.bufArrToHexArr(proof);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
verify(proofs, merkleRoot, leaf) {
|
|
96
|
+
let computedHash = toBuffer(leaf);
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < proofs.length; i++) {
|
|
99
|
+
const proofElement = toBuffer(proofs[i]);
|
|
100
|
+
computedHash = this.combinedHash(proofElement, computedHash);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return toHex(computedHash) === merkleRoot;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getPairElement(idx, layer) {
|
|
107
|
+
const pairIdx = idx % 2 === 0 ? idx + 1 : idx - 1;
|
|
108
|
+
|
|
109
|
+
if (pairIdx < layer.length) {
|
|
110
|
+
return layer[pairIdx];
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
bufIndexOf(el, arr) {
|
|
116
|
+
let hash;
|
|
117
|
+
|
|
118
|
+
// Convert element to 32 byte hash if it is not one already
|
|
119
|
+
if (el.length !== 32 || !Buffer.isBuffer(el)) {
|
|
120
|
+
hash = keccakFromString(el);
|
|
121
|
+
} else {
|
|
122
|
+
hash = el;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < arr.length; i++) {
|
|
126
|
+
if (hash.equals(arr[i])) {
|
|
127
|
+
return i;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return -1;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
bufDeduplicate(elements) {
|
|
135
|
+
return elements.filter((el, idx) => idx === 0 || !elements[idx - 1].equals(el));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
bufArrToHexArr(arr) {
|
|
139
|
+
if (arr.some((el) => !Buffer.isBuffer(el))) {
|
|
140
|
+
throw new Error('Array is not an array of buffers');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return arr.map((el) => `0x${el.toString('hex')}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
sortAndConcat(...args) {
|
|
147
|
+
return Buffer.concat([...args].sort(Buffer.compare));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = MerkleTree;
|
|
152
|
+
|
|
153
|
+
module.exports.keccak256 = keccak256;
|
|
154
|
+
module.exports.encodePacked = encodePacked;
|
|
155
|
+
|
|
156
|
+
module.exports.getListHash = (list) =>
|
|
157
|
+
list.length
|
|
158
|
+
? keccak256(
|
|
159
|
+
list
|
|
160
|
+
.map((x) => (x.startsWith('0x') ? x : `0x${x}`))
|
|
161
|
+
.reduce((acc, x) => encodePacked({ type: 'bytes', value: acc }, { type: 'bytes', value: x }), '')
|
|
162
|
+
)
|
|
163
|
+
: '';
|
|
164
|
+
|
|
165
|
+
module.exports.getBlockHash = (block) =>
|
|
166
|
+
block
|
|
167
|
+
? keccak256(
|
|
168
|
+
encodePacked(
|
|
169
|
+
{ type: 'uint256', value: block.height },
|
|
170
|
+
{ type: 'bytes32', value: block.previousHash },
|
|
171
|
+
{ type: 'bytes32', value: block.merkleRoot },
|
|
172
|
+
{ type: 'bytes32', value: block.txsHash }
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
: '';
|
|
176
|
+
|
|
177
|
+
module.exports.getBlockMerkleTree = (txs) =>
|
|
178
|
+
txs.length
|
|
179
|
+
? new MerkleTree(
|
|
180
|
+
txs.map((x) =>
|
|
181
|
+
encodePacked(
|
|
182
|
+
{ type: 'bytes32', value: x.hash.startsWith('0x') ? x.hash : `0x${x.hash}` },
|
|
183
|
+
{ type: 'address', value: x.to },
|
|
184
|
+
{ type: 'uint256', value: x.amount }
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
: {
|
|
189
|
+
getHexRoot: () => '0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff',
|
|
190
|
+
getHexProof: () => [],
|
|
191
|
+
elements: [],
|
|
192
|
+
layers: [],
|
|
193
|
+
verify: () => false,
|
|
194
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ocap/merkle-tree",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.6.10",
|
|
7
|
+
"description": "Merkle tree implementation that powers DID Rollup",
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"lib"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "eslint tests lib",
|
|
14
|
+
"lint:fix": "eslint --fix tests lib",
|
|
15
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
16
|
+
"coverage": "npm run test -- --coverage"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [],
|
|
19
|
+
"author": "wangshijun <wangshijun2010@gmail.com> (http://github.com/wangshijun)",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"jest": "^27.3.1"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@ocap/mcrypto": "1.6.10",
|
|
26
|
+
"@ocap/util": "1.6.10"
|
|
27
|
+
},
|
|
28
|
+
"gitHead": "ab272e8db3a15c6571cc7fae7cc3d3e0fdd4bdb1"
|
|
29
|
+
}
|