520 lines
10 KiB
JavaScript
520 lines
10 KiB
JavaScript
'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;
|