• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

handshake-org / hsd / 11142522588

02 Oct 2024 10:56AM UTC coverage: 70.04% (+0.007%) from 70.033%
11142522588

push

github

nodech
Merge PR #902 from 'nodech/update-types'

7761 of 12916 branches covered (60.09%)

Branch coverage included in aggregate %.

67 of 92 new or added lines in 25 files covered. (72.83%)

8 existing lines in 5 files now uncovered.

24790 of 33559 relevant lines covered (73.87%)

34019.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.48
/lib/primitives/airdropproof.js
1
'use strict';
2

3
const assert = require('bsert');
70✔
4
const bio = require('bufio');
70✔
5
const base16 = require('bcrypto/lib/encoding/base16');
70✔
6
const blake2b = require('bcrypto/lib/blake2b');
70✔
7
const sha256 = require('bcrypto/lib/sha256');
70✔
8
const merkle = require('bcrypto/lib/mrkl');
70✔
9
const AirdropKey = require('./airdropkey');
70✔
10
const InvItem = require('./invitem');
70✔
11
const consensus = require('../protocol/consensus');
70✔
12
const {keyTypes} = AirdropKey;
70✔
13

14
/** @typedef {import('../types').Hash} Hash */
15
/** @typedef {import('../types').BufioWriter} BufioWriter */
16

17
/*
18
 * Constants
19
 */
20

21
const EMPTY = Buffer.alloc(0);
70✔
22
const SPONSOR_FEE = 500e6;
70✔
23
const RECIPIENT_FEE = 100e6;
70✔
24

25
// SHA256("HNS Signature")
26
const CONTEXT = Buffer.from(
70✔
27
  '5b21ff4a0fcf78123915eaa0003d2a3e1855a9b15e3441da2ef5a4c01eaf4ff3',
28
  'hex');
29

30
const AIRDROP_ROOT = Buffer.from(
70✔
31
  '10d748eda1b9c67b94d3244e0211677618a9b4b329e896ad90431f9f48034bad',
32
  'hex');
33

34
const AIRDROP_REWARD = 4246994314;
70✔
35
const AIRDROP_DEPTH = 18;
70✔
36
const AIRDROP_SUBDEPTH = 3;
70✔
37
const AIRDROP_LEAVES = 216199;
70✔
38
const AIRDROP_SUBLEAVES = 8;
70✔
39

40
const FAUCET_ROOT = Buffer.from(
70✔
41
  'e2c0299a1e466773516655f09a64b1e16b2579530de6c4a59ce5654dea45180f',
42
  'hex');
43

44
const FAUCET_DEPTH = 11;
70✔
45
const FAUCET_LEAVES = 1358;
70✔
46

47
const TREE_LEAVES = AIRDROP_LEAVES + FAUCET_LEAVES;
70✔
48

49
const MAX_PROOF_SIZE = 3400; // 3253
70✔
50

51
/** @typedef {ReturnType<AirdropProof['getJSON']>} AirdropProofJSON */
52

53
/**
54
 * AirdropProof
55
 */
56

57
class AirdropProof extends bio.Struct {
58
  constructor() {
59
    super();
113✔
60
    this.index = 0;
113✔
61
    /** @type {Hash[]} */
62
    this.proof = [];
113✔
63
    this.subindex = 0;
113✔
64
    /** @type {Hash[]} */
65
    this.subproof = [];
113✔
66
    this.key = EMPTY;
113✔
67
    this.version = 0;
113✔
68
    this.address = EMPTY;
113✔
69
    this.fee = 0;
113✔
70
    this.signature = EMPTY;
113✔
71
  }
72

73
  getSize(sighash = false) {
121✔
74
    let size = 0;
131✔
75

76
    if (sighash)
131✔
77
      size += 32;
10✔
78

79
    size += 4;
131✔
80
    size += 1;
131✔
81
    size += this.proof.length * 32;
131✔
82
    size += 1;
131✔
83
    size += 1;
131✔
84
    size += this.subproof.length * 32;
131✔
85
    size += bio.sizeVarBytes(this.key);
131✔
86
    size += 1;
131✔
87
    size += 1;
131✔
88
    size += this.address.length;
131✔
89
    size += bio.sizeVarint(this.fee);
131✔
90

91
    if (!sighash)
131✔
92
      size += bio.sizeVarBytes(this.signature);
121✔
93

94
    return size;
131✔
95
  }
96

97
  /**
98
   * @param {BufioWriter} bw
99
   * @param {Boolean} [sighash=false]
100
   * @returns {BufioWriter}
101
   */
102

103
  write(bw, sighash = false) {
48✔
104
    if (sighash)
58✔
105
      bw.writeBytes(CONTEXT);
10✔
106

107
    bw.writeU32(this.index);
58✔
108
    bw.writeU8(this.proof.length);
58✔
109

110
    for (const hash of this.proof)
58✔
111
      bw.writeBytes(hash);
953✔
112

113
    bw.writeU8(this.subindex);
58✔
114
    bw.writeU8(this.subproof.length);
58✔
115

116
    for (const hash of this.subproof)
58✔
117
      bw.writeBytes(hash);
135✔
118

119
    bw.writeVarBytes(this.key);
58✔
120
    bw.writeU8(this.version);
58✔
121
    bw.writeU8(this.address.length);
58✔
122
    bw.writeBytes(this.address);
58✔
123
    bw.writeVarint(this.fee);
58✔
124

125
    if (!sighash)
58✔
126
      bw.writeVarBytes(this.signature);
48✔
127

128
    return bw;
58✔
129
  }
130

131
  /**
132
   * @param {Buffer} data
133
   * @returns {this}
134
   */
135

136
  decode(data) {
137
    const br = bio.read(data);
110✔
138

139
    if (data.length > MAX_PROOF_SIZE)
110!
140
      throw new Error('Proof too large.');
×
141

142
    this.read(br);
110✔
143

144
    if (br.left() !== 0)
110!
145
      throw new Error('Trailing data.');
×
146

147
    return this;
110✔
148
  }
149

150
  /**
151
   * @param {bio.BufferReader} br
152
   * @returns {this}
153
   */
154

155
  read(br) {
156
    this.index = br.readU32();
113✔
157
    assert(this.index < AIRDROP_LEAVES);
113✔
158

159
    const count = br.readU8();
113✔
160
    assert(count <= AIRDROP_DEPTH);
113✔
161

162
    for (let i = 0; i < count; i++) {
113✔
163
      const hash = br.readBytes(32);
1,740✔
164
      this.proof.push(hash);
1,740✔
165
    }
166

167
    this.subindex = br.readU8();
113✔
168
    assert(this.subindex < AIRDROP_SUBLEAVES);
113✔
169

170
    const total = br.readU8();
113✔
171
    assert(total <= AIRDROP_SUBDEPTH);
113✔
172

173
    for (let i = 0; i < total; i++) {
113✔
174
      const hash = br.readBytes(32);
213✔
175
      this.subproof.push(hash);
213✔
176
    }
177

178
    this.key = br.readVarBytes();
113✔
179
    assert(this.key.length > 0);
113✔
180

181
    this.version = br.readU8();
113✔
182

183
    assert(this.version <= 31);
113✔
184

185
    const size = br.readU8();
113✔
186
    assert(size >= 2 && size <= 40);
113✔
187

188
    this.address = br.readBytes(size);
113✔
189
    this.fee = br.readVarint();
113✔
190
    this.signature = br.readVarBytes();
113✔
191

192
    return this;
113✔
193
  }
194

195
  /**
196
   * @returns {Buffer}
197
   */
198

199
  hash() {
200
    const bw = bio.pool(this.getSize());
19✔
201
    this.write(bw);
19✔
202
    return blake2b.digest(bw.render());
19✔
203
  }
204

205
  /**
206
   * @param {Hash} [expect]
207
   * @returns {Boolean}
208
   */
209

210
  verifyMerkle(expect) {
211
    if (expect == null) {
17!
212
      expect = this.isAddress()
17✔
213
        ? FAUCET_ROOT
214
        : AIRDROP_ROOT;
215
    }
216

217
    assert(Buffer.isBuffer(expect));
17✔
218
    assert(expect.length === 32);
17✔
219

220
    const {subproof, subindex} = this;
17✔
221
    const {proof, index} = this;
17✔
222
    const leaf = blake2b.digest(this.key);
17✔
223

224
    if (this.isAddress()) {
17✔
225
      const root = merkle.deriveRoot(blake2b, leaf, proof, index);
6✔
226

227
      return root.equals(expect);
6✔
228
    }
229

230
    const subroot = merkle.deriveRoot(blake2b, leaf, subproof, subindex);
11✔
231
    const root = merkle.deriveRoot(blake2b, subroot, proof, index);
11✔
232

233
    return root.equals(expect);
11✔
234
  }
235

236
  /**
237
   * @returns {Buffer}
238
   */
239

240
  signatureData() {
241
    const size = this.getSize(true);
10✔
242
    const bw = bio.pool(size);
10✔
243

244
    this.write(bw, true);
10✔
245

246
    return bw.render();
10✔
247
  }
248

249
  /**
250
   * @returns {Buffer}
251
   */
252

253
  signatureHash() {
254
    return sha256.digest(this.signatureData());
10✔
255
  }
256

257
  /**
258
   * @returns {AirdropKey|null}
259
   */
260

261
  getKey() {
262
    try {
90✔
263
      return AirdropKey.decode(this.key);
90✔
264
    } catch (e) {
265
      return null;
×
266
    }
267
  }
268

269
  /**
270
   * @returns {Boolean}
271
   */
272

273
  verifySignature() {
274
    const key = this.getKey();
16✔
275

276
    if (!key)
16!
277
      return false;
×
278

279
    if (key.isAddress()) {
16✔
280
      const fee = key.sponsor
6!
281
        ? SPONSOR_FEE
282
        : RECIPIENT_FEE;
283

284
      return this.version === key.version
6✔
285
          && this.address.equals(key.address)
286
          && this.fee === fee
287
          && this.signature.length === 0;
288
    }
289

290
    const msg = this.signatureHash();
10✔
291

292
    return key.verify(msg, this.signature);
10✔
293
  }
294

295
  /**
296
   * @returns {Number}
297
   */
298

299
  position() {
300
    let index = this.index;
47✔
301

302
    // Position in the bitfield.
303
    // Bitfield is organized as:
304
    // [airdrop-bits] || [faucet-bits]
305
    if (this.isAddress()) {
47✔
306
      assert(index < FAUCET_LEAVES);
18✔
307
      index += AIRDROP_LEAVES;
18✔
308
    } else {
309
      assert(index < AIRDROP_LEAVES);
29✔
310
    }
311

312
    assert(index < TREE_LEAVES);
47✔
313

314
    return index;
47✔
315
  }
316

317
  toTX(TX, Input, Output) {
318
    const tx = new TX();
3✔
319

320
    tx.inputs.push(new Input());
3✔
321
    tx.outputs.push(new Output());
3✔
322

323
    const input = new Input();
3✔
324
    const output = new Output();
3✔
325

326
    input.witness.items.push(this.encode());
3✔
327

328
    output.value = this.getValue() - this.fee;
3✔
329
    output.address.version = this.version;
3✔
330
    output.address.hash = this.address;
3✔
331

332
    tx.inputs.push(input);
3✔
333
    tx.outputs.push(output);
3✔
334

335
    tx.refresh();
3✔
336

337
    return tx;
3✔
338
  }
339

340
  toInv() {
341
    return new InvItem(InvItem.types.AIRDROP, this.hash());
×
342
  }
343

344
  getWeight() {
345
    return this.getSize();
15✔
346
  }
347

348
  getVirtualSize() {
349
    const scale = consensus.WITNESS_SCALE_FACTOR;
15✔
350
    return (this.getWeight() + scale - 1) / scale | 0;
15✔
351
  }
352

353
  isWeak() {
354
    const key = this.getKey();
18✔
355

356
    if (!key)
18!
357
      return false;
×
358

359
    return key.isWeak();
18✔
360
  }
361

362
  isAddress() {
363
    if (this.key.length === 0)
312!
364
      return false;
×
365

366
    return this.key[0] === keyTypes.ADDRESS;
312✔
367
  }
368

369
  getValue() {
370
    if (!this.isAddress())
140✔
371
      return AIRDROP_REWARD;
88✔
372

373
    const key = this.getKey();
52✔
374

375
    if (!key)
52!
376
      return 0;
×
377

378
    return key.value;
52✔
379
  }
380

381
  isSane() {
382
    if (this.key.length === 0)
91!
383
      return false;
×
384

385
    if (this.version > 31)
91!
386
      return false;
×
387

388
    if (this.address.length < 2 || this.address.length > 40)
91!
389
      return false;
×
390

391
    const value = this.getValue();
91✔
392

393
    if (value < 0 || value > consensus.MAX_MONEY)
91!
394
      return false;
×
395

396
    if (this.fee < 0 || this.fee > value)
91!
397
      return false;
×
398

399
    if (this.isAddress()) {
91✔
400
      if (this.subproof.length !== 0)
33!
401
        return false;
×
402

403
      if (this.subindex !== 0)
33!
404
        return false;
×
405

406
      if (this.proof.length > FAUCET_DEPTH)
33!
407
        return false;
×
408

409
      if (this.index >= FAUCET_LEAVES)
33!
410
        return false;
×
411

412
      return true;
33✔
413
    }
414

415
    if (this.subproof.length > AIRDROP_SUBDEPTH)
58!
416
      return false;
×
417

418
    if (this.subindex >= AIRDROP_SUBLEAVES)
58!
419
      return false;
×
420

421
    if (this.proof.length > AIRDROP_DEPTH)
58!
422
      return false;
×
423

424
    if (this.index >= AIRDROP_LEAVES)
58!
425
      return false;
×
426

427
    if (this.getSize() > MAX_PROOF_SIZE)
58!
428
      return false;
×
429

430
    return true;
58✔
431
  }
432

433
  /**
434
   * @param {Hash} [expect]
435
   * @returns {Boolean}
436
   */
437

438
  verify(expect) {
439
    if (!this.isSane())
17!
440
      return false;
×
441

442
    if (!this.verifyMerkle(expect))
17✔
443
      return false;
1✔
444

445
    if (!this.verifySignature())
16!
446
      return false;
×
447

448
    return true;
16✔
449
  }
450

451
  getJSON() {
452
    const key = this.getKey();
×
453

454
    return {
×
455
      index: this.index,
456
      proof: this.proof.map(h => h.toString('hex')),
×
457
      subindex: this.subindex,
458
      subproof: this.subproof.map(h => h.toString('hex')),
×
459
      key: key ? key.toJSON() : null,
×
460
      version: this.version,
461
      address: this.address.toString('hex'),
462
      fee: this.fee,
463
      signature: this.signature.toString('hex')
464
    };
465
  }
466

467
  /**
468
   * @param {AirdropProofJSON} json
469
   * @returns {this}
470
   */
471

472
  fromJSON(json) {
473
    assert(json && typeof json === 'object');
×
474
    assert((json.index >>> 0) === json.index);
×
475
    assert(Array.isArray(json.proof));
×
476
    assert((json.subindex >>> 0) === json.subindex);
×
477
    assert(Array.isArray(json.subproof));
×
478
    assert(json.key == null || (json.key && typeof json.key === 'object'));
×
479
    assert((json.version & 0xff) === json.version);
×
480
    assert(typeof json.address === 'string');
×
481
    assert(Number.isSafeInteger(json.fee) && json.fee >= 0);
×
482
    assert(typeof json.signature === 'string');
×
483

484
    this.index = json.index;
×
485

486
    for (const hash of json.proof)
×
NEW
487
      this.proof.push(base16.decode(hash));
×
488

489
    this.subindex = json.subindex;
×
490

491
    for (const hash of json.subproof)
×
NEW
492
      this.subproof.push(base16.decode(hash));
×
493

494
    if (json.key)
×
495
      this.key = AirdropKey.fromJSON(json.key).encode();
×
496

497
    this.version = json.version;
×
498
    this.address = base16.decode(json.address);
×
499
    this.fee = json.fee;
×
500
    this.signature = base16.decode(json.signature);
×
501

502
    return this;
×
503
  }
504
}
505

506
/*
507
 * Static
508
 */
509

510
AirdropProof.AIRDROP_ROOT = AIRDROP_ROOT;
70✔
511
AirdropProof.FAUCET_ROOT = FAUCET_ROOT;
70✔
512
AirdropProof.TREE_LEAVES = TREE_LEAVES;
70✔
513
AirdropProof.AIRDROP_LEAVES = AIRDROP_LEAVES;
70✔
514
AirdropProof.FAUCET_LEAVES = FAUCET_LEAVES;
70✔
515

516
/*
517
 * Expose
518
 */
519

520
module.exports = AirdropProof;
70✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc