Source: primitives/airdropproof.js

'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;