UNPKG

hsd

Version:
521 lines (386 loc) 10.5 kB
'use strict'; const assert = require('bsert'); const bio = require('bufio'); const base16 = require('bcrypto/lib/encoding/base16'); const blake2b = require('bcrypto/lib/blake2b'); const sha256 = require('bcrypto/lib/sha256'); const merkle = require('bcrypto/lib/mrkl'); const AirdropKey = require('./airdropkey'); const InvItem = require('./invitem'); const consensus = require('../protocol/consensus'); const {keyTypes} = AirdropKey; /** @typedef {import('../types').Hash} Hash */ /** @typedef {import('../types').BufioWriter} BufioWriter */ /* * Constants */ const EMPTY = Buffer.alloc(0); const SPONSOR_FEE = 500e6; const RECIPIENT_FEE = 100e6; // SHA256("HNS Signature") const CONTEXT = Buffer.from( '5b21ff4a0fcf78123915eaa0003d2a3e1855a9b15e3441da2ef5a4c01eaf4ff3', 'hex'); const AIRDROP_ROOT = Buffer.from( '10d748eda1b9c67b94d3244e0211677618a9b4b329e896ad90431f9f48034bad', 'hex'); const AIRDROP_REWARD = 4246994314; const AIRDROP_DEPTH = 18; const AIRDROP_SUBDEPTH = 3; const AIRDROP_LEAVES = 216199; const AIRDROP_SUBLEAVES = 8; const FAUCET_ROOT = Buffer.from( 'e2c0299a1e466773516655f09a64b1e16b2579530de6c4a59ce5654dea45180f', 'hex'); const FAUCET_DEPTH = 11; const FAUCET_LEAVES = 1358; const TREE_LEAVES = AIRDROP_LEAVES + FAUCET_LEAVES; const MAX_PROOF_SIZE = 3400; // 3253 /** @typedef {ReturnType<AirdropProof['getJSON']>} AirdropProofJSON */ /** * AirdropProof */ class AirdropProof extends bio.Struct { constructor() { super(); this.index = 0; /** @type {Hash[]} */ this.proof = []; this.subindex = 0; /** @type {Hash[]} */ this.subproof = []; this.key = EMPTY; this.version = 0; this.address = EMPTY; this.fee = 0; this.signature = EMPTY; } getSize(sighash = false) { let size = 0; if (sighash) size += 32; size += 4; size += 1; size += this.proof.length * 32; size += 1; size += 1; size += this.subproof.length * 32; size += bio.sizeVarBytes(this.key); size += 1; size += 1; size += this.address.length; size += bio.sizeVarint(this.fee); if (!sighash) size += bio.sizeVarBytes(this.signature); return size; } /** * @param {BufioWriter} bw * @param {Boolean} [sighash=false] * @returns {BufioWriter} */ write(bw, sighash = false) { if (sighash) bw.writeBytes(CONTEXT); bw.writeU32(this.index); bw.writeU8(this.proof.length); for (const hash of this.proof) bw.writeBytes(hash); bw.writeU8(this.subindex); bw.writeU8(this.subproof.length); for (const hash of this.subproof) bw.writeBytes(hash); bw.writeVarBytes(this.key); bw.writeU8(this.version); bw.writeU8(this.address.length); bw.writeBytes(this.address); bw.writeVarint(this.fee); if (!sighash) bw.writeVarBytes(this.signature); return bw; } /** * @param {Buffer} data * @returns {this} */ decode(data) { const br = bio.read(data); if (data.length > MAX_PROOF_SIZE) throw new Error('Proof too large.'); this.read(br); if (br.left() !== 0) throw new Error('Trailing data.'); return this; } /** * @param {bio.BufferReader} br * @returns {this} */ read(br) { this.index = br.readU32(); assert(this.index < AIRDROP_LEAVES); const count = br.readU8(); assert(count <= AIRDROP_DEPTH); for (let i = 0; i < count; i++) { const hash = br.readBytes(32); this.proof.push(hash); } this.subindex = br.readU8(); assert(this.subindex < AIRDROP_SUBLEAVES); const total = br.readU8(); assert(total <= AIRDROP_SUBDEPTH); for (let i = 0; i < total; i++) { const hash = br.readBytes(32); this.subproof.push(hash); } this.key = br.readVarBytes(); assert(this.key.length > 0); this.version = br.readU8(); assert(this.version <= 31); const size = br.readU8(); assert(size >= 2 && size <= 40); this.address = br.readBytes(size); this.fee = br.readVarint(); this.signature = br.readVarBytes(); return this; } /** * @returns {Buffer} */ hash() { const bw = bio.pool(this.getSize()); this.write(bw); return blake2b.digest(bw.render()); } /** * @param {Hash} [expect] * @returns {Boolean} */ verifyMerkle(expect) { if (expect == null) { expect = this.isAddress() ? FAUCET_ROOT : AIRDROP_ROOT; } assert(Buffer.isBuffer(expect)); assert(expect.length === 32); const {subproof, subindex} = this; const {proof, index} = this; const leaf = blake2b.digest(this.key); if (this.isAddress()) { const root = merkle.deriveRoot(blake2b, leaf, proof, index); return root.equals(expect); } const subroot = merkle.deriveRoot(blake2b, leaf, subproof, subindex); const root = merkle.deriveRoot(blake2b, subroot, proof, index); return root.equals(expect); } /** * @returns {Buffer} */ signatureData() { const size = this.getSize(true); const bw = bio.pool(size); this.write(bw, true); return bw.render(); } /** * @returns {Buffer} */ signatureHash() { return sha256.digest(this.signatureData()); } /** * @returns {AirdropKey|null} */ getKey() { try { return AirdropKey.decode(this.key); } catch (e) { return null; } } /** * @returns {Boolean} */ verifySignature() { const key = this.getKey(); if (!key) return false; if (key.isAddress()) { const fee = key.sponsor ? SPONSOR_FEE : RECIPIENT_FEE; return this.version === key.version && this.address.equals(key.address) && this.fee === fee && this.signature.length === 0; } const msg = this.signatureHash(); return key.verify(msg, this.signature); } /** * @returns {Number} */ position() { let index = this.index; // Position in the bitfield. // Bitfield is organized as: // [airdrop-bits] || [faucet-bits] if (this.isAddress()) { assert(index < FAUCET_LEAVES); index += AIRDROP_LEAVES; } else { assert(index < AIRDROP_LEAVES); } assert(index < TREE_LEAVES); return index; } toTX(TX, Input, Output) { const tx = new TX(); tx.inputs.push(new Input()); tx.outputs.push(new Output()); const input = new Input(); const output = new Output(); input.witness.items.push(this.encode()); output.value = this.getValue() - this.fee; output.address.version = this.version; output.address.hash = this.address; tx.inputs.push(input); tx.outputs.push(output); tx.refresh(); return tx; } toInv() { return new InvItem(InvItem.types.AIRDROP, this.hash()); } getWeight() { return this.getSize(); } getVirtualSize() { const scale = consensus.WITNESS_SCALE_FACTOR; return (this.getWeight() + scale - 1) / scale | 0; } isWeak() { const key = this.getKey(); if (!key) return false; return key.isWeak(); } isAddress() { if (this.key.length === 0) return false; return this.key[0] === keyTypes.ADDRESS; } getValue() { if (!this.isAddress()) return AIRDROP_REWARD; const key = this.getKey(); if (!key) return 0; return key.value; } isSane() { if (this.key.length === 0) return false; if (this.version > 31) return false; if (this.address.length < 2 || this.address.length > 40) return false; const value = this.getValue(); if (value < 0 || value > consensus.MAX_MONEY) return false; if (this.fee < 0 || this.fee > value) return false; if (this.isAddress()) { if (this.subproof.length !== 0) return false; if (this.subindex !== 0) return false; if (this.proof.length > FAUCET_DEPTH) return false; if (this.index >= FAUCET_LEAVES) return false; return true; } if (this.subproof.length > AIRDROP_SUBDEPTH) return false; if (this.subindex >= AIRDROP_SUBLEAVES) return false; if (this.proof.length > AIRDROP_DEPTH) return false; if (this.index >= AIRDROP_LEAVES) return false; if (this.getSize() > MAX_PROOF_SIZE) return false; return true; } /** * @param {Hash} [expect] * @returns {Boolean} */ verify(expect) { if (!this.isSane()) return false; if (!this.verifyMerkle(expect)) return false; if (!this.verifySignature()) return false; return true; } getJSON() { const key = this.getKey(); return { index: this.index, proof: this.proof.map(h => h.toString('hex')), subindex: this.subindex, subproof: this.subproof.map(h => h.toString('hex')), key: key ? key.toJSON() : null, version: this.version, address: this.address.toString('hex'), fee: this.fee, signature: this.signature.toString('hex') }; } /** * @param {AirdropProofJSON} json * @returns {this} */ fromJSON(json) { assert(json && typeof json === 'object'); assert((json.index >>> 0) === json.index); assert(Array.isArray(json.proof)); assert((json.subindex >>> 0) === json.subindex); assert(Array.isArray(json.subproof)); assert(json.key == null || (json.key && typeof json.key === 'object')); assert((json.version & 0xff) === json.version); assert(typeof json.address === 'string'); assert(Number.isSafeInteger(json.fee) && json.fee >= 0); assert(typeof json.signature === 'string'); this.index = json.index; for (const hash of json.proof) this.proof.push(base16.decode(hash)); this.subindex = json.subindex; for (const hash of json.subproof) this.subproof.push(base16.decode(hash)); if (json.key) this.key = AirdropKey.fromJSON(json.key).encode(); this.version = json.version; this.address = base16.decode(json.address); this.fee = json.fee; this.signature = base16.decode(json.signature); return this; } } /* * Static */ AirdropProof.AIRDROP_ROOT = AIRDROP_ROOT; AirdropProof.FAUCET_ROOT = FAUCET_ROOT; AirdropProof.TREE_LEAVES = TREE_LEAVES; AirdropProof.AIRDROP_LEAVES = AIRDROP_LEAVES; AirdropProof.FAUCET_LEAVES = FAUCET_LEAVES; /* * Expose */ module.exports = AirdropProof;