docs: add JS reference source for conversion

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-04-02 02:23:19 +01:00
parent 5a83dd4478
commit 98ce5f2bc9
70 changed files with 152460 additions and 0 deletions

4178
docs/js-blockchain/chain.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,394 @@
/*!
* chainentry.js - chainentry object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const BN = require('bcrypto/lib/bn.js');
const consensus = require('../protocol/consensus');
const Headers = require('../primitives/headers');
const InvItem = require('../primitives/invitem');
const util = require('../utils/util');
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../protocol/network')} Network */
/** @typedef {import('../primitives/block')} Block */
/** @typedef {import('../primitives/merkleblock')} MerkleBlock */
/*
* Constants
*/
const ZERO = new BN(0);
/**
* Chain Entry
* Represents an entry in the chain.
* @alias module:blockchain.ChainEntry
* @property {Hash} hash
* @property {Number} version
* @property {Hash} prevBlock
* @property {Hash} merkleRoot
* @property {Hash} witnessRoot
* @property {Hash} treeRoot
* @property {Hash} reservedRoot
* @property {Number} time
* @property {Number} bits
* @property {Buffer} nonce
* @property {Number} height
* @property {BN} chainwork
*/
class ChainEntry extends bio.Struct {
/**
* Create a chain entry.
* @constructor
* @param {Object?} options
*/
constructor(options) {
super();
this.hash = consensus.ZERO_HASH;
this.version = 0;
this.prevBlock = consensus.ZERO_HASH;
this.merkleRoot = consensus.ZERO_HASH;
this.witnessRoot = consensus.ZERO_HASH;
this.treeRoot = consensus.ZERO_HASH;
this.reservedRoot = consensus.ZERO_HASH;
this.time = 0;
this.bits = 0;
this.nonce = 0;
this.extraNonce = consensus.ZERO_NONCE;
this.mask = consensus.ZERO_HASH;
this.height = 0;
this.chainwork = ZERO;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options.
* @param {Object} options
* @returns {this}
*/
fromOptions(options) {
assert(options, 'Block data is required.');
assert(Buffer.isBuffer(options.hash));
assert((options.version >>> 0) === options.version);
assert(Buffer.isBuffer(options.prevBlock));
assert(Buffer.isBuffer(options.merkleRoot));
assert(Buffer.isBuffer(options.witnessRoot));
assert(Buffer.isBuffer(options.treeRoot));
assert(Buffer.isBuffer(options.reservedRoot));
assert(util.isU64(options.time));
assert((options.bits >>> 0) === options.bits);
assert((options.nonce >>> 0) === options.nonce);
assert(Buffer.isBuffer(options.extraNonce));
assert(Buffer.isBuffer(options.mask));
assert((options.height >>> 0) === options.height);
assert(!options.chainwork || BN.isBN(options.chainwork));
this.hash = options.hash;
this.version = options.version;
this.prevBlock = options.prevBlock;
this.merkleRoot = options.merkleRoot;
this.witnessRoot = options.witnessRoot;
this.treeRoot = options.treeRoot;
this.reservedRoot = options.reservedRoot;
this.time = options.time;
this.bits = options.bits;
this.nonce = options.nonce;
this.extraNonce = options.extraNonce;
this.mask = options.mask;
this.height = options.height;
this.chainwork = options.chainwork || ZERO;
return this;
}
/**
* Calculate the proof: (1 << 256) / (target + 1)
* @returns {BN} proof
*/
getProof() {
const target = consensus.fromCompact(this.bits);
if (target.isNeg() || target.isZero())
return new BN(0);
return ChainEntry.MAX_CHAINWORK.div(target.iaddn(1));
}
/**
* Calculate the chainwork by
* adding proof to previous chainwork.
* @param {ChainEntry?} [prev] - Previous entry.
* @returns {BN} chainwork
*/
getChainwork(prev) {
const proof = this.getProof();
if (!prev)
return proof;
return proof.iadd(prev.chainwork);
}
/**
* Test against the genesis block.
* @returns {Boolean}
*/
isGenesis() {
return this.height === 0;
}
/**
* Test whether the entry contains an unknown version bit.
* @param {Network} network
* @returns {Boolean}
*/
hasUnknown(network) {
return (this.version & network.unknownBits) !== 0;
}
/**
* Test whether the entry contains a version bit.
* @param {Number} bit
* @returns {Boolean}
*/
hasBit(bit) {
return consensus.hasBit(this.version, bit);
}
/**
* Inject properties from block.
* @param {Block|MerkleBlock} block
* @param {ChainEntry?} [prev] - Previous entry.
* @returns {this}
*/
fromBlock(block, prev) {
this.hash = block.hash();
this.version = block.version;
this.prevBlock = block.prevBlock;
this.merkleRoot = block.merkleRoot;
this.witnessRoot = block.witnessRoot;
this.treeRoot = block.treeRoot;
this.reservedRoot = block.reservedRoot;
this.time = block.time;
this.bits = block.bits;
this.nonce = block.nonce;
this.extraNonce = block.extraNonce;
this.mask = block.mask;
this.height = prev ? prev.height + 1 : 0;
this.chainwork = this.getChainwork(prev);
return this;
}
/**
* Get serialization size.
* @returns {Number}
*/
getSize() {
return 36 + consensus.HEADER_SIZE + 32;
}
/**
* Serialize the entry to internal database format.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeHash(this.hash);
bw.writeU32(this.height);
// Preheader.
bw.writeU32(this.nonce);
bw.writeU64(this.time);
bw.writeHash(this.prevBlock);
bw.writeHash(this.treeRoot);
// Subheader.
bw.writeBytes(this.extraNonce);
bw.writeHash(this.reservedRoot);
bw.writeHash(this.witnessRoot);
bw.writeHash(this.merkleRoot);
bw.writeU32(this.version);
bw.writeU32(this.bits);
// Mask.
bw.writeBytes(this.mask);
bw.writeBytes(this.chainwork.toArrayLike(Buffer, 'be', 32));
return bw;
}
/**
* Inject properties from serialized data.
* @param {bio.BufferReader} br
*/
read(br) {
this.hash = br.readHash();
this.height = br.readU32();
// Preheader.
this.nonce = br.readU32();
this.time = br.readU64();
this.prevBlock = br.readHash();
this.treeRoot = br.readHash();
// Subheader.
this.extraNonce = br.readBytes(consensus.NONCE_SIZE);
this.reservedRoot = br.readHash();
this.witnessRoot = br.readHash();
this.merkleRoot = br.readHash();
this.version = br.readU32();
this.bits = br.readU32();
// Mask.
this.mask = br.readBytes(32);
this.chainwork = new BN(br.readBytes(32), 'be');
return this;
}
/**
* Serialize the entry to an object more
* suitable for JSON serialization.
* @returns {Object}
*/
getJSON() {
return {
hash: this.hash.toString('hex'),
height: this.height,
version: this.version,
prevBlock: this.prevBlock.toString('hex'),
merkleRoot: this.merkleRoot.toString('hex'),
witnessRoot: this.witnessRoot.toString('hex'),
treeRoot: this.treeRoot.toString('hex'),
reservedRoot: this.reservedRoot.toString('hex'),
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex'),
chainwork: this.chainwork.toString('hex', 64)
};
}
/**
* Inject properties from json object.
* @param {Object} json
* @returns {this}
*/
fromJSON(json) {
assert(json, 'Block data is required.');
assert((json.height >>> 0) === json.height);
assert((json.version >>> 0) === json.version);
assert(util.isU64(json.time));
assert((json.bits >>> 0) === json.bits);
assert((json.nonce >>> 0) === json.nonce);
const work = util.parseHex(json.chainwork, 32);
this.hash = json.hash;
this.height = json.height;
this.version = json.version;
this.prevBlock = util.parseHex(json.prevBlock, 32);
this.merkleRoot = util.parseHex(json.merkleRoot, 32);
this.witnessRoot = util.parseHex(json.witnessRoot, 32);
this.treeRoot = util.parseHex(json.treeRoot, 32);
this.reservedRoot = util.parseHex(json.reservedRoot, 32);
this.time = json.time;
this.bits = json.bits;
this.nonce = json.nonce;
this.extraNonce = util.parseHex(json.extraNonce, consensus.NONCE_SIZE);
this.mask = util.parseHex(json.mask, 32);
this.chainwork = new BN(work, 'be');
return this;
}
/**
* Convert the entry to a headers object.
* @returns {Headers}
*/
toHeaders() {
return Headers.fromEntry(this);
}
/**
* Convert the entry to an inv item.
* @returns {InvItem}
*/
toInv() {
return new InvItem(InvItem.types.BLOCK, this.hash);
}
/**
* Return a more user-friendly object.
* @returns {Object}
*/
format() {
const json = this.toJSON();
json.version = json.version.toString(16);
return json;
}
/**
* Instantiate chainentry from block.
* @param {Block|MerkleBlock} block
* @param {ChainEntry?} [prev] - Previous entry.
* @returns {ChainEntry}
*/
static fromBlock(block, prev) {
return new this().fromBlock(block, prev);
}
/**
* Test whether an object is a {@link ChainEntry}.
* @param {Object} obj
* @returns {Boolean}
*/
static isChainEntry(obj) {
return obj instanceof ChainEntry;
}
}
/**
* The max chainwork (1 << 256).
* @const {BN}
*/
ChainEntry.MAX_CHAINWORK = new BN(1).ushln(256);
/*
* Expose
*/
module.exports = ChainEntry;

View file

@ -0,0 +1,125 @@
/*!
* common.js - chain constants for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../types').LockFlags} LockFlags */
/**
* @module blockchain/common
*/
/**
* Locktime flags.
* @enum {Number}
*/
exports.lockFlags = {};
/**
* Consensus locktime flags (used for block validation).
* @const {LockFlags}
* @default
*/
exports.MANDATORY_LOCKTIME_FLAGS = 0;
/**
* Standard locktime flags (used for mempool validation).
* @const {LockFlags}
* @default
*/
exports.STANDARD_LOCKTIME_FLAGS = 0
| exports.MANDATORY_LOCKTIME_FLAGS;
/**
* Threshold states for versionbits
* @enum {Number}
* @default
*/
exports.thresholdStates = {
DEFINED: 0,
STARTED: 1,
LOCKED_IN: 2,
ACTIVE: 3,
FAILED: 4
};
/**
* Verify flags for blocks.
* @enum {Number}
* @default
*/
exports.flags = {
VERIFY_NONE: 0,
VERIFY_POW: 1 << 0,
VERIFY_BODY: 1 << 1
};
/**
* Default block verify flags.
* @const {Number}
* @default
*/
exports.DEFAULT_FLAGS = 0
| exports.flags.VERIFY_POW
| exports.flags.VERIFY_BODY;
/**
* Interactive scan actions.
* @enum {Number}
* @default
*/
exports.scanActions = {
NONE: 0,
ABORT: 1,
NEXT: 2,
REPEAT_SET: 3,
REPEAT_ADD: 4,
REPEAT: 5
};
/**
* @typedef {Object} ActionAbort
* @property {exports.scanActions} type - ABORT
*/
/**
* @typedef {Object} ActionNext
* @property {exports.scanActions} type - NEXT
*/
/**
* @typedef {Object} ActionRepeat
* @property {exports.ScanAction} type - REPEAT
*/
/**
* @typedef {Object} ActionRepeatAdd
* @property {exports.scanActions} type - REPEAT_ADD
* @property {Buffer[]} chunks
*/
/**
* @typedef {Object} ActionRepeatSet
* @property {exports.scanActions} type - REPEAT_SET
* @property {BloomFilter} filter
*/
/**
* @typedef {ActionAbort
* | ActionNext
* | ActionRepeat
* | ActionRepeatAdd
* | ActionRepeatSet
* } ScanAction
*/

View file

@ -0,0 +1,17 @@
/*!
* blockchain/index.js - blockchain for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/**
* @module blockchain
*/
exports.ChainDB = require('./chaindb');
exports.ChainEntry = require('./chainentry');
exports.Chain = require('./chain');
exports.common = require('./common');
exports.layout = require('./layout');

View file

@ -0,0 +1,62 @@
/*!
* layout.js - blockchain data layout for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const bdb = require('bdb');
/*
* Database Layout:
* V -> db version
* O -> chain options
* R -> chain state (contains tip)
* D -> versionbits deployments
* e[hash] -> entry
* h[hash] -> height
* H[height] -> hash
* n[hash] -> next hash
* p[hash] -> tip index
* b[hash] -> block (deprecated)
* t[hash] -> extended tx
* c[hash] -> coins
* u[hash] -> undo coins (deprecated)
* v[bit][hash] -> versionbits state
* T[addr-hash][hash] -> dummy (tx by address)
* C[addr-hash][hash][index] -> dummy (coin by address)
* w[height] -> name undo
* s -> tree state
* f -> bit field
* M -> migration state
*/
const layout = {
V: bdb.key('V'),
O: bdb.key('O'),
R: bdb.key('R'),
D: bdb.key('D'),
e: bdb.key('e', ['hash256']),
h: bdb.key('h', ['hash256']),
H: bdb.key('H', ['uint32']),
n: bdb.key('n', ['hash256']),
p: bdb.key('p', ['hash256']),
b: bdb.key('b', ['hash256']),
t: bdb.key('t', ['hash256']),
c: bdb.key('c', ['hash256', 'uint32']),
u: bdb.key('u', ['hash256']),
v: bdb.key('v', ['uint8', 'hash256']),
T: bdb.key('T', ['hash', 'hash256']),
C: bdb.key('C', ['hash', 'hash256', 'uint32']),
w: bdb.key('w', ['uint32']),
s: bdb.key('s'),
f: bdb.key('f'),
M: bdb.key('M')
};
/*
* Expose
*/
module.exports = layout;

View file

@ -0,0 +1,724 @@
/*!
* blockchain/migrations.js - blockchain data migrations for hsd
* Copyright (c) 2021, Nodari Chkuaselidze (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const Logger = require('blgr');
const bio = require('bufio');
const {encoding} = bio;
const bdb = require('bdb');
const Network = require('../protocol/network');
const rules = require('../covenants/rules');
const Block = require('../primitives/block');
const CoinView = require('../coins/coinview');
const UndoCoins = require('../coins/undocoins');
const layout = require('./layout');
const AbstractMigration = require('../migrations/migration');
const migrator = require('../migrations/migrator');
const {
Migrator,
oldLayout,
types
} = migrator;
/** @typedef {import('../types').Hash} Hash */
/** @typedef {ReturnType<bdb.DB['batch']>} Batch */
/** @typedef {migrator.types} MigrationType */
/** @typedef {migrator.MigrationContext} MigrationContext */
/**
* Switch to new migrations layout.
*/
class MigrateMigrations extends AbstractMigration {
/**
* Create migrations migration.
* @param {ChainMigratorOptions} options
*/
constructor(options) {
super(options);
this.options = options;
this.logger = options.logger.context('chain-migration-migrate');
this.db = options.db;
this.ldb = options.ldb;
this.layout = MigrateMigrations.layout();
}
/**
* @returns {Promise<MigrationType>}
*/
async check() {
return types.MIGRATE;
}
/**
* Actual migration
* @param {Batch} b
* @param {MigrationContext} ctx
* @returns {Promise}
*/
async migrate(b, ctx) {
this.logger.info('Migrating migrations..');
const oldLayout = this.layout.oldLayout;
let nextMigration = 1;
const skipped = [];
const oldMigrations = await this.ldb.keys({
gte: oldLayout.M.min(),
lte: oldLayout.M.max(),
parse: key => oldLayout.M.decode(key)[0]
});
for (const id of oldMigrations) {
b.del(oldLayout.M.encode(id));
if (id === 1) {
if (this.options.prune) {
skipped.push(1);
}
nextMigration = 2;
}
}
this.db.writeVersion(b, 2);
ctx.state.version = 0;
ctx.state.skipped = skipped;
ctx.state.nextMigration = nextMigration;
}
static info() {
return {
name: 'Migrate ChainDB migrations',
description: 'ChainDB migration layout has changed.'
};
}
static layout() {
return {
oldLayout: {
M: bdb.key('M', ['uint32'])
},
newLayout: {
M: bdb.key('M')
}
};
}
}
/**
* Migrate chain state and correct total supply.
* Applies to ChainDB v1
*/
class MigrateChainState extends AbstractMigration {
/**
* Create migration chain state
* @constructor
* @param {ChainMigratorOptions} options
*/
constructor(options) {
super(options);
this.options = options;
this.logger = options.logger.context('chain-migration-chainstate');
this.db = options.db;
this.ldb = options.ldb;
this.layout = MigrateChainState.layout();
}
/**
* Check if the migration applies to the database
* @returns {Promise}
*/
async check() {
if (this.options.spv)
return types.FAKE_MIGRATE;
if (this.options.prune)
return types.SKIP;
return types.MIGRATE;
}
/**
* Log warnings when skipped.
*/
warning() {
if (!this.options.prune)
throw new Error('No warnings to show!');
this.logger.warning('Pruned nodes cannot migrate the chain state.');
this.logger.warning('Your total chain value may be inaccurate!');
}
/**
* Actual migration
* @param {Batch} b
* @returns {Promise}
*/
async migrate(b) {
this.logger.info('Migrating chain state.');
this.logger.info('This may take a few minutes...');
const rawState = await this.ldb.get(this.layout.R.encode());
const tipHash = rawState.slice(0, 32);
const rawTipHeight = await this.ldb.get(this.layout.h.encode(tipHash));
const tipHeight = rawTipHeight.readUInt32LE(0);
const pending = {
coin: 0,
value: 0,
burned: 0
};
for (let height = 0; height <= tipHeight; height++) {
const hash = await this.ldb.get(this.layout.H.encode(height));
const block = await this.getBlock(hash);
assert(block);
const view = await this.getBlockView(block);
for (let i = 0; i < block.txs.length; i++) {
const tx = block.txs[i];
if (i > 0) {
for (const {prevout} of tx.inputs) {
const output = view.getOutput(prevout);
assert(output);
if (output.covenant.type >= rules.types.REGISTER
&& output.covenant.type <= rules.types.REVOKE) {
continue;
}
pending.coin -= 1;
pending.value -= output.value;
}
}
for (let i = 0; i < tx.outputs.length; i++) {
const output = tx.outputs[i];
if (output.isUnspendable())
continue;
if (output.covenant.isRegister()) {
pending.coin += 1;
pending.burned += output.value;
}
if (output.covenant.type >= rules.types.REGISTER
&& output.covenant.type <= rules.types.REVOKE) {
continue;
}
if (output.covenant.isClaim()) {
if (output.covenant.getU32(5) !== 1)
continue;
}
pending.coin += 1;
pending.value += output.value;
}
}
}
// prefix hash + tx (8)
// we write coin (8) + value (8) + burned (8)
encoding.writeU64(rawState, pending.coin, 40);
encoding.writeU64(rawState, pending.value, 40 + 8);
encoding.writeU64(rawState, pending.burned, 40 + 16);
b.put(this.layout.R.encode(), rawState);
}
/**
* Get Block (old layout)
* @param {Hash} hash
* @returns {Promise<Block>}
*/
async getBlock(hash) {
assert(Buffer.isBuffer(hash));
const raw = await this.ldb.get(this.layout.b.encode(hash));
if (!raw)
return null;
return Block.decode(raw);
}
/**
* Get block view (old layout)
* @param {Block} block
* @returns {Promise} - UndoCoins
*/
async getBlockView(block) {
const hash = block.hash();
const view = new CoinView();
const raw = await this.ldb.get(this.layout.u.encode(hash));
if (!raw)
return view;
// getBlockView logic.
const undo = UndoCoins.decode(raw);
if (undo.isEmpty())
return view;
for (let i = block.txs.length - 1; i > 0; i--) {
const tx = block.txs[i];
for (let j = tx.inputs.length - 1; j >= 0; j--) {
const input = tx.inputs[j];
undo.apply(view, input.prevout);
}
}
// Undo coins should be empty.
assert(undo.isEmpty(), 'Undo coins data inconsistency.');
return view;
}
static info() {
return {
name: 'Chain State migration',
description: 'Chain state is corrupted.'
};
}
static layout() {
return {
// R -> tip hash
R: bdb.key('R'),
// h[hash] -> height
h: bdb.key('h', ['hash256']),
// H[height] -> hash
H: bdb.key('H', ['uint32']),
// b[hash] -> block
b: bdb.key('b', ['hash256']),
// u[hash] -> undo coins
u: bdb.key('u', ['hash256'])
};
}
}
/**
* Migrate block and undo data to BlockStore from chainDB.
*/
class MigrateBlockStore extends AbstractMigration {
/**
* Create MigrateBlockStore object.
* @param {ChainMigratorOptions} options
*/
constructor(options) {
super(options);
this.options = options;
this.logger = options.logger.context('chain-migration-blockstore');
this.db = options.db;
this.ldb = options.ldb;
this.blocks = options.db.blocks;
this.layout = MigrateBlockStore.layout();
this.batchWriteSize = 10000;
}
/**
* Check if the ChainDB has the blocks.
* @returns {Promise}
*/
async check() {
if (this.options.spv)
return types.FAKE_MIGRATE;
return types.MIGRATE;
}
/**
* Migrate blocks and undo blocks
* @returns {Promise}
*/
async migrate() {
assert(this.blocks, 'Could not find blockstore.');
this.logger.info('Migrating blocks and undo blocks.');
this.logger.info('This may take a few minutes...');
await this.migrateBlocks();
await this.migrateUndoBlocks();
this.logger.info('Compacting database...');
this.logger.info('This may take a few minutes...');
await this.ldb.compactRange();
}
/**
* Migrate the block data.
*/
async migrateBlocks() {
this.logger.info('Migrating blocks...');
let parent = this.ldb.batch();
const iter = this.ldb.iterator({
gte: this.layout.b.min(),
lte: this.layout.b.max(),
keys: true,
values: true
});
let total = 0;
await iter.each(async (key, value) => {
const hash = key.slice(1);
await this.blocks.writeBlock(hash, value);
parent.del(key);
if (++total % this.batchWriteSize === 0) {
await parent.write();
this.logger.debug('Migrated up %d blocks.', total);
parent = this.ldb.batch();
}
});
await parent.write();
this.logger.info('Migrated all %d blocks.', total);
}
/**
* Migrate the undo data.
*/
async migrateUndoBlocks() {
this.logger.info('Migrating undo blocks...');
let parent = this.ldb.batch();
const iter = this.ldb.iterator({
gte: this.layout.u.min(),
lte: this.layout.u.max(),
keys: true,
values: true
});
let total = 0;
await iter.each(async (key, value) => {
const hash = key.slice(1);
await this.blocks.writeUndo(hash, value);
parent.del(key);
if (++total % this.batchWriteSize === 0) {
await parent.write();
this.logger.debug('Migrated up %d undo blocks.', total);
parent = this.ldb.batch();
}
});
await parent.write();
this.logger.info('Migrated all %d undo blocks.', total);
}
static info() {
return {
name: 'BlockStore migration',
description: 'Move block and undo data to the'
+ ' blockstore from the chainDB.'
};
}
static layout() {
return {
// b[hash] -> block
b: bdb.key('b', ['hash256']),
// u[hash] -> undo coins
u: bdb.key('u', ['hash256'])
};
}
};
/**
* Migrate Tree State
*/
class MigrateTreeState extends AbstractMigration {
/**
* Create tree state migrator
* @constructor
* @param {ChainMigratorOptions} options
*/
constructor(options) {
super(options);
this.options = options;
this.logger = options.logger.context('chain-migration-tree-state');
this.db = options.db;
this.ldb = options.ldb;
this.network = options.network;
this.layout = MigrateTreeState.layout();
}
async check() {
return types.MIGRATE;
}
/**
* @param {Batch} b
* @returns {Promise}
*/
async migrate(b) {
if (this.options.spv) {
this.db.writeVersion(b, 3);
return;
}
const {treeInterval} = this.network.names;
const rawState = await this.ldb.get(this.layout.R.encode());
const tipHash = rawState.slice(0, 32);
const rawTipHeight = await this.ldb.get(this.layout.h.encode(tipHash));
const tipHeight = rawTipHeight.readUInt32LE(0);
const lastCommitHeight = tipHeight - (tipHeight % treeInterval);
const hash = await this.ldb.get(this.layout.s.encode());
assert(hash && hash.length === 32);
// new tree root
// see chaindb.js TreeState
const buff = Buffer.alloc(72);
encoding.writeBytes(buff, hash, 0);
encoding.writeU32(buff, lastCommitHeight, 32);
this.db.writeVersion(b, 3);
b.put(this.layout.s.encode(), buff);
}
static info() {
return {
name: 'Migrate Tree State',
description: 'Add compaction information to the tree state.'
};
}
static layout() {
return {
// R -> tip hash
R: bdb.key('R'),
// h[hash] -> height
h: bdb.key('h', ['hash256']),
// s -> tree state
s: bdb.key('s')
};
}
}
class MigrateMigrationStateV1 extends AbstractMigration {
/**
* Create migration migration state
* @constructor
* @param {ChainMigratorOptions} options
*/
constructor(options) {
super(options);
this.options = options;
this.logger = options.logger.context('chain-migration-migration-state-v1');
this.db = options.db;
this.ldb = options.ldb;
this.network = options.network;
}
async check() {
return types.MIGRATE;
}
/**
* @param {Batch} b
* @param {MigrationContext} ctx
* @returns {Promise}
*/
async migrate(b, ctx) {
ctx.state.version = 1;
}
static info() {
return {
name: 'Migrate Migration State',
description: 'Migrate migration state to v1'
};
}
}
/**
* Chain Migrator
* @alias module:blockchain.ChainMigrator
*/
class ChainMigrator extends Migrator {
/**
* Create ChainMigrator object.
* @constructor
* @param {Object} options
*/
constructor(options) {
super(new ChainMigratorOptions(options));
this.logger = this.options.logger.context('chain-migrations');
this.flagError = 'Restart with `hsd --chain-migrate='
+ this.lastMigration + '`';
}
/**
* Check chaindb flags
* @returns {Promise}
*/
async verifyDB() {
await this.db.verifyFlags();
}
/**
* Get list of migrations to run
* @returns {Promise<Set>}
*/
async getMigrationsToRun() {
const state = await this.getState();
const lastID = this.getLastMigrationID();
if (state.nextMigration > lastID)
return new Set();
const ids = new Set();
for (let i = state.nextMigration; i <= lastID; i++)
ids.add(i);
if (state.nextMigration === 0 && await this.ldb.get(oldLayout.M.encode(1)))
ids.delete(1);
return ids;
}
}
/**
* ChainMigratorOptions
* @alias module:blockchain.ChainMigratorOptions
*/
class ChainMigratorOptions {
/**
* Create Chain Migrator Options.
* @constructor
* @param {Object} options
*/
constructor(options) {
this.network = Network.primary;
this.logger = Logger.global;
this.migrations = ChainMigrator.migrations;
this.migrateFlag = -1;
this.dbVersion = 0;
this.db = null;
this.ldb = null;
this.layout = layout;
this.spv = false;
this.prune = false;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from object.
* @param {Object} options
* @returns {ChainMigratorOptions}
*/
fromOptions(options) {
if (options.network != null)
this.network = Network.get(options.network);
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger;
}
if (options.chainDB != null) {
assert(typeof options.chainDB === 'object');
this.db = options.chainDB;
this.ldb = this.db.db;
}
if (options.chainMigrate != null) {
assert(typeof options.chainMigrate === 'number');
this.migrateFlag = options.chainMigrate;
}
if (options.dbVersion != null) {
assert(typeof options.dbVersion === 'number');
this.dbVersion = options.dbVersion;
}
if (options.migrations != null) {
assert(typeof options.migrations === 'object');
this.migrations = options.migrations;
}
if (options.spv != null) {
assert(typeof options.spv === 'boolean');
this.spv = options.spv;
}
if (options.prune != null) {
assert(typeof options.prune === 'boolean');
this.prune = options.prune;
}
return this;
}
}
// List of the migrations with ids
ChainMigrator.migrations = {
0: MigrateMigrations,
1: MigrateChainState,
2: MigrateBlockStore,
3: MigrateTreeState,
4: MigrateMigrationStateV1
};
// Expose migrations
ChainMigrator.MigrateChainState = MigrateChainState;
ChainMigrator.MigrateMigrations = MigrateMigrations;
ChainMigrator.MigrateBlockStore = MigrateBlockStore;
ChainMigrator.MigrateTreeState = MigrateTreeState;
ChainMigrator.MigrateMigrationStateV1 = MigrateMigrationStateV1;
module.exports = ChainMigrator;

View file

@ -0,0 +1,369 @@
/*!
* records.js - chaindb records
* Copyright (c) 2024 The Handshake Developers (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const {BufferMap} = require('buffer-map');
const consensus = require('../protocol/consensus');
const Network = require('../protocol/network');
/**
* ChainFlags
*/
class ChainFlags extends bio.Struct {
/**
* Create chain flags.
* @alias module:blockchain.ChainFlags
* @constructor
*/
constructor(options) {
super();
this.network = Network.primary;
this.spv = false;
this.prune = false;
this.indexTX = false;
this.indexAddress = false;
if (options)
this.fromOptions(options);
}
fromOptions(options) {
this.network = Network.get(options.network);
if (options.spv != null) {
assert(typeof options.spv === 'boolean');
this.spv = options.spv;
}
if (options.prune != null) {
assert(typeof options.prune === 'boolean');
this.prune = options.prune;
}
if (options.indexTX != null) {
assert(typeof options.indexTX === 'boolean');
this.indexTX = options.indexTX;
}
if (options.indexAddress != null) {
assert(typeof options.indexAddress === 'boolean');
this.indexAddress = options.indexAddress;
}
return this;
}
getSize() {
return 12;
}
write(bw) {
let flags = 0;
if (this.spv)
flags |= 1 << 0;
if (this.prune)
flags |= 1 << 1;
if (this.indexTX)
flags |= 1 << 2;
if (this.indexAddress)
flags |= 1 << 3;
bw.writeU32(this.network.magic);
bw.writeU32(flags);
bw.writeU32(0);
return bw;
}
read(br) {
this.network = Network.fromMagic(br.readU32());
const flags = br.readU32();
this.spv = (flags & 1) !== 0;
this.prune = (flags & 2) !== 0;
this.indexTX = (flags & 4) !== 0;
this.indexAddress = (flags & 8) !== 0;
return this;
}
}
/**
* Chain State
*/
class ChainState extends bio.Struct {
/**
* Create chain state.
* @alias module:blockchain.ChainState
* @constructor
*/
constructor() {
super();
this.tip = consensus.ZERO_HASH;
this.tx = 0;
this.coin = 0;
this.value = 0;
this.burned = 0;
this.committed = false;
}
inject(state) {
this.tip = state.tip;
this.tx = state.tx;
this.coin = state.coin;
this.value = state.value;
this.burned = state.burned;
return this;
}
connect(block) {
this.tx += block.txs.length;
}
disconnect(block) {
this.tx -= block.txs.length;
}
add(coin) {
this.coin += 1;
this.value += coin.value;
}
spend(coin) {
this.coin -= 1;
this.value -= coin.value;
}
burn(coin) {
this.coin += 1;
this.burned += coin.value;
}
unburn(coin) {
this.coin -= 1;
this.burned -= coin.value;
}
commit(hash) {
assert(Buffer.isBuffer(hash));
this.tip = hash;
this.committed = true;
return this.encode();
}
getSize() {
return 64;
}
write(bw) {
bw.writeHash(this.tip);
bw.writeU64(this.tx);
bw.writeU64(this.coin);
bw.writeU64(this.value);
bw.writeU64(this.burned);
return bw;
}
read(br) {
this.tip = br.readHash();
this.tx = br.readU64();
this.coin = br.readU64();
this.value = br.readU64();
this.burned = br.readU64();
return this;
}
}
/**
* State Cache
*/
class StateCache {
/**
* Create state cache.
* @alias module:blockchain.StateCache
* @constructor
*/
constructor(network) {
this.network = network;
this.bits = [];
this.updates = [];
this.init();
}
init() {
for (let i = 0; i < 32; i++)
this.bits.push(null);
for (const {bit} of this.network.deploys) {
assert(!this.bits[bit]);
this.bits[bit] = new BufferMap();
}
}
set(bit, entry, state) {
const cache = this.bits[bit];
assert(cache);
if (cache.get(entry.hash) !== state) {
cache.set(entry.hash, state);
this.updates.push(new CacheUpdate(bit, entry.hash, state));
}
}
get(bit, entry) {
const cache = this.bits[bit];
assert(cache);
const state = cache.get(entry.hash);
if (state == null)
return -1;
return state;
}
commit() {
this.updates.length = 0;
}
drop() {
for (const {bit, hash} of this.updates) {
const cache = this.bits[bit];
assert(cache);
cache.delete(hash);
}
this.updates.length = 0;
}
insert(bit, hash, state) {
const cache = this.bits[bit];
assert(cache);
cache.set(hash, state);
}
}
/**
* Cache Update
*/
class CacheUpdate {
/**
* Create cache update.
* @constructor
* @ignore
*/
constructor(bit, hash, state) {
this.bit = bit;
this.hash = hash;
this.state = state;
}
encode() {
const data = Buffer.allocUnsafe(1);
data[0] = this.state;
return data;
}
}
/**
* Tree related state.
*/
class TreeState extends bio.Struct {
/**
* Create tree state.
* @constructor
* @ignore
*/
constructor() {
super();
this.treeRoot = consensus.ZERO_HASH;
this.commitHeight = 0;
this.compactionRoot = consensus.ZERO_HASH;
this.compactionHeight = 0;
this.committed = false;
}
inject(state) {
this.treeRoot = state.treeRoot;
this.commitHeight = state.treeHeight;
this.compactionHeight = state.compactionHeight;
this.compactionRoot = state.compactionRoot;
return this;
}
compact(hash, height) {
assert(Buffer.isBuffer(hash));
assert((height >>> 0) === height);
this.compactionRoot = hash;
this.compactionHeight = height;
};
commit(hash, height) {
assert(Buffer.isBuffer(hash));
assert((height >>> 0) === height);
this.treeRoot = hash;
this.commitHeight = height;
this.committed = true;
return this.encode();
}
getSize() {
return 72;
}
write(bw) {
bw.writeHash(this.treeRoot);
bw.writeU32(this.commitHeight);
bw.writeHash(this.compactionRoot);
bw.writeU32(this.compactionHeight);
return bw;
}
read(br) {
this.treeRoot = br.readHash();
this.commitHeight = br.readU32();
this.compactionRoot = br.readHash();
this.compactionHeight = br.readU32();
return this;
}
}
/*
* Expose
*/
exports.ChainFlags = ChainFlags;
exports.ChainState = ChainState;
exports.StateCache = StateCache;
exports.TreeState = TreeState;
exports.CacheUpdate = CacheUpdate;

View file

@ -0,0 +1,168 @@
'use strict';
const assert = require('bsert');
const AirdropProof = require('../primitives/airdropproof');
const {TREE_LEAVES} = AirdropProof;
/**
* Field
*/
class Field {
constructor(size = 0) {
assert((size >>> 0) === size);
this.size = size;
this.field = Buffer.alloc((size + 7) >>> 3, 0x00);
this.dirty = false;
}
set(i, val) {
assert((i >>> 0) === i);
assert(i < this.size);
assert((val >>> 0) === val);
assert(val === 0 || val === 1);
if (val)
this.field[i >>> 3] |= 1 << (7 - (i & 7));
else
this.field[i >>> 3] &= ~(1 << (7 - (i & 7)));
this.dirty = true;
return this;
}
get(i) {
assert((i >>> 0) === i);
if (i >= this.size)
return 1;
return (this.field[i >>> 3] >> (7 - (i & 7))) & 1;
}
isSpent(i) {
return Boolean(this.get(i));
}
spend(i) {
return this.set(i, 1);
}
unspend(i) {
return this.set(i, 0);
}
encode() {
this.dirty = false;
return this.field;
}
decode(data) {
assert(Buffer.isBuffer(data));
this.field = data;
this.dirty = false;
return this;
}
static decode(size, data) {
return new this(size).decode(data);
}
}
/**
* BitField
*/
class BitField extends Field {
constructor() {
super(TREE_LEAVES);
}
static decode(data) {
return new this().decode(data);
}
}
/**
* BitView
*/
class BitView {
constructor() {
this.bits = new Map();
}
spend(field, tx) {
assert(field instanceof Field);
assert(tx && tx.isCoinbase());
for (let i = 1; i < tx.inputs.length; i++) {
const input = tx.inputs[i];
const output = tx.output(i);
const {witness} = input;
assert(output && witness.items.length === 1);
const {covenant} = output;
if (!covenant.isNone())
continue;
const proof = AirdropProof.decode(witness.items[0]);
const index = proof.position();
if (!this.bits.has(index))
this.bits.set(index, field.get(index));
if (this.bits.get(index) !== 0)
return false;
this.bits.set(index, 1);
}
return true;
}
undo(tx) {
assert(tx && tx.isCoinbase());
for (let i = 1; i < tx.inputs.length; i++) {
const input = tx.inputs[i];
const output = tx.output(i);
const {witness} = input;
assert(output && witness.items.length === 1);
const {covenant} = output;
if (!covenant.isNone())
continue;
const proof = AirdropProof.decode(witness.items[0]);
const index = proof.position();
this.bits.set(index, 0);
}
return this;
}
commit(field) {
assert(field instanceof Field);
for (const [bit, spent] of this.bits)
field.set(bit, spent);
return field.dirty;
}
}
/*
* Expose
*/
exports.Field = Field;
exports.BitField = BitField;
exports.BitView = BitView;

View file

@ -0,0 +1,15 @@
/*!
* covenants/index.js - covenants for hsd
* Copyright (c) 2019, handshake-org developers (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/**
* @module covenants
*/
exports.Namestate = require('./namestate');
exports.Ownership = require('./ownership');
exports.Rules = require('./rules');

View file

@ -0,0 +1,116 @@
'use strict';
const assert = require('bsert');
const sha3 = require('bcrypto/lib/sha3');
const data = require('./lockup.json');
/*
* Constants
*/
const ZERO_HASH = sha3.zero.toString('hex');
/**
* Locked up
*/
class LockedUp {
constructor(data) {
const meta = data[ZERO_HASH];
this.data = data;
this.size = meta[0];
}
has(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
const hex = hash.toString('hex');
const item = this.data[hex];
if (!item)
return false;
return Array.isArray(item);
}
get(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
const hex = hash.toString('hex');
const item = this.data[hex];
if (!item || !Array.isArray(item))
return null;
const target = item[0];
const flags = item[1];
const index = target.indexOf('.');
assert(index !== -1);
const root = (flags & 1) !== 0;
const custom = (flags & 2) !== 0;
const name = target.substring(0, index);
return {
name,
hash,
target,
root,
custom
};
}
hasByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return false;
return this.has(hashName(name));
}
getByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return null;
return this.get(hashName(name));
}
*entries() {
const keys = Object.keys(this.data);
for (const key of keys) {
const hash = Buffer.from(key, 'hex');
yield [hash, this.get(hash)];
}
}
*keys() {
const keys = Object.keys(this.data);
for (const key of keys)
yield Buffer.from(key, 'hex');
}
*values() {
for (const [, item] of this.entries())
yield item;
}
[Symbol.iterator]() {
return this.entries();
}
}
function hashName(name) {
const raw = Buffer.from(name.toLowerCase(), 'ascii');
return sha3.digest(raw);
}
exports.LockedUp = LockedUp;
exports.locked = new LockedUp(data);

173
docs/js-covenants/locked.js Normal file
View file

@ -0,0 +1,173 @@
'use strict';
const assert = require('bsert');
const Path = require('path');
const fs = require('bfile');
const sha3 = require('bcrypto/lib/sha3');
const FILE = Path.resolve(__dirname, 'lockup.db');
const DATA = fs.readFileSync(FILE);
/**
* Locked up
*/
class LockedUp {
constructor(data) {
this.data = data;
this.size = readU32(data, 0);
}
get prefixSize() {
return 4;
}
_compare(b, off) {
const a = this.data;
for (let i = 0; i < 32; i++) {
const x = a[off + i];
const y = b[i];
if (x < y)
return -1;
if (x > y)
return 1;
}
return 0;
}
_find(key) {
let start = 0;
let end = this.size - 1;
while (start <= end) {
const index = (start + end) >>> 1;
const pos = this.prefixSize + index * 36;
const cmp = this._compare(key, pos);
if (cmp === 0)
return readU32(this.data, pos + 32);
if (cmp < 0)
start = index + 1;
else
end = index - 1;
}
return -1;
}
_target(pos) {
const len = this.data[pos];
return this.data.toString('ascii', pos + 1, pos + 1 + len);
}
_flags(pos) {
const len = this.data[pos];
return this.data[pos + 1 + len];
}
_index(pos) {
const len = this.data[pos];
return this.data[pos + 1 + len + 1];
}
_get(hash, pos) {
const target = this._target(pos);
const flags = this._flags(pos);
const index = this._index(pos);
const root = (flags & 1) !== 0;
const custom = (flags & 2) !== 0;
const name = target.substring(0, index);
return {
name,
hash,
target,
root,
custom
};
}
has(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
return this._find(hash) !== -1;
}
get(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
const pos = this._find(hash);
if (pos === -1)
return null;
return this._get(hash, pos);
}
hasByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return false;
return this.has(hashName(name));
}
getByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return null;
return this.get(hashName(name));
}
*entries() {
for (let i = 0; i < this.size; i++) {
const pos = this.prefixSize + i * 36;
const hash = this.data.slice(pos, pos + 32);
const ptr = readU32(this.data, pos + 32);
const item = this._get(hash, ptr);
yield [hash, item];
}
}
*keys() {
for (let i = 0; i < this.size; i++) {
const pos = this.prefixSize + i * 36;
yield this.data.slice(pos, pos + 32);
}
}
*values() {
for (const [, item] of this.entries())
yield item;
}
[Symbol.iterator]() {
return this.entries();
}
}
/*
* Helpers
*/
function readU32(data, off) {
return data.readUInt32LE(off);
}
function hashName(name) {
const raw = Buffer.from(name.toLowerCase(), 'ascii');
return sha3.digest(raw);
}
exports.LockedUp = LockedUp;
exports.locked = new LockedUp(DATA);

BIN
docs/js-covenants/lockup.db Normal file

Binary file not shown.

11557
docs/js-covenants/lockup.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,330 @@
/*!
* namedelta.js - name deltas for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const bio = require('bufio');
const Outpoint = require('../primitives/outpoint');
const {encoding} = bio;
/*
* Constants
*/
const EMPTY = Buffer.alloc(0);
/**
* NameDelta
* @extends {bio.Struct}
*/
class NameDelta extends bio.Struct {
constructor() {
super();
this.height = null;
this.renewal = null;
this.owner = null;
this.value = null;
this.highest = null;
this.data = null;
this.transfer = null;
this.revoked = null;
this.claimed = null;
this.renewals = null;
this.registered = null;
this.expired = null;
this.weak = null;
}
isNull() {
return this.height === null
&& this.renewal === null
&& this.owner === null
&& this.value === null
&& this.highest === null
&& this.data === null
&& this.transfer === null
&& this.revoked === null
&& this.claimed === null
&& this.renewals === null
&& this.registered === null
&& this.expired === null
&& this.weak === null;
}
getSize() {
let size = 0;
size += 4;
if (this.height !== null)
size += 4;
if (this.renewal !== null)
size += 4;
if (this.owner !== null) {
if (!this.owner.isNull())
size += 32 + encoding.sizeVarint(this.owner.index);
}
if (this.value !== null) {
if (this.value !== 0)
size += encoding.sizeVarint(this.value);
}
if (this.highest !== null) {
if (this.highest !== 0)
size += encoding.sizeVarint(this.highest);
}
if (this.data !== null) {
if (this.data)
size += encoding.sizeVarlen(this.data.length);
}
if (this.transfer !== null) {
if (this.transfer !== 0)
size += 4;
}
if (this.revoked !== null) {
if (this.revoked !== 0)
size += 4;
}
if (this.claimed !== null) {
if (this.claimed !== 0)
size += 4;
}
if (this.renewals !== null) {
if (this.renewals !== 0)
size += encoding.sizeVarint(this.renewals);
}
return size;
}
getField() {
let field = 0;
if (this.height !== null)
field |= 1 << 0;
if (this.renewal !== null)
field |= 1 << 1;
if (this.owner !== null) {
field |= 1 << 2;
if (!this.owner.isNull())
field |= 1 << 3;
}
if (this.value !== null) {
field |= 1 << 4;
if (this.value !== 0)
field |= 1 << 5;
}
if (this.highest !== null) {
field |= 1 << 6;
if (this.highest !== 0)
field |= 1 << 7;
}
if (this.data !== null) {
field |= 1 << 8;
if (this.data)
field |= 1 << 9;
}
if (this.transfer !== null) {
field |= 1 << 10;
if (this.transfer !== 0)
field |= 1 << 11;
}
if (this.revoked !== null) {
field |= 1 << 12;
if (this.revoked !== 0)
field |= 1 << 13;
}
if (this.claimed !== null) {
field |= 1 << 14;
if (this.claimed !== 0)
field |= 1 << 15;
}
if (this.renewals !== null) {
field |= 1 << 16;
if (this.renewals !== 0)
field |= 1 << 17;
}
if (this.registered !== null) {
field |= 1 << 18;
if (this.registered)
field |= 1 << 19;
}
if (this.expired !== null) {
field |= 1 << 20;
if (this.expired)
field |= 1 << 21;
}
if (this.weak !== null) {
field |= 1 << 22;
if (this.weak)
field |= 1 << 23;
}
return field;
}
write(bw) {
bw.writeU32(this.getField());
if (this.height !== null)
bw.writeU32(this.height);
if (this.renewal !== null)
bw.writeU32(this.renewal);
if (this.owner !== null) {
if (!this.owner.isNull()) {
bw.writeHash(this.owner.hash);
bw.writeVarint(this.owner.index);
}
}
if (this.value !== null) {
if (this.value !== 0)
bw.writeVarint(this.value);
}
if (this.highest !== null) {
if (this.highest !== 0)
bw.writeVarint(this.highest);
}
if (this.data !== null) {
if (this.data)
bw.writeVarBytes(this.data);
}
if (this.transfer !== null) {
if (this.transfer !== 0)
bw.writeU32(this.transfer);
}
if (this.revoked !== null) {
if (this.revoked !== 0)
bw.writeU32(this.revoked);
}
if (this.claimed !== null) {
if (this.claimed !== 0)
bw.writeU32(this.claimed);
}
if (this.renewals !== null) {
if (this.renewals !== 0)
bw.writeVarint(this.renewals);
}
return bw;
}
read(br) {
const field = br.readU32();
if (field & (1 << 0))
this.height = br.readU32();
if (field & (1 << 1))
this.renewal = br.readU32();
if (field & (1 << 2)) {
this.owner = new Outpoint();
if (field & (1 << 3)) {
this.owner.hash = br.readHash();
this.owner.index = br.readVarint();
}
}
if (field & (1 << 4)) {
this.value = 0;
if (field & (1 << 5))
this.value = br.readVarint();
}
if (field & (1 << 6)) {
this.highest = 0;
if (field & (1 << 7))
this.highest = br.readVarint();
}
if (field & (1 << 8)) {
this.data = EMPTY;
if (field & (1 << 9))
this.data = br.readVarBytes();
}
if (field & (1 << 10)) {
this.transfer = 0;
if (field & (1 << 11))
this.transfer = br.readU32();
}
if (field & (1 << 12)) {
this.revoked = 0;
if (field & (1 << 13))
this.revoked = br.readU32();
}
if (field & (1 << 14)) {
this.claimed = 0;
if (field & (1 << 15))
this.claimed = br.readU32();
}
if (field & (1 << 16)) {
this.renewals = 0;
if (field & (1 << 17))
this.renewals = br.readVarint();
}
if (field & (1 << 18)) {
this.registered = false;
if (field & (1 << 19))
this.registered = true;
}
if (field & (1 << 20)) {
this.expired = false;
if (field & (1 << 21))
this.expired = true;
}
if (field & (1 << 22)) {
this.weak = false;
if (field & (1 << 23))
this.weak = true;
}
return this;
}
}
/*
* Expose
*/
module.exports = NameDelta;

BIN
docs/js-covenants/names.db Normal file

Binary file not shown.

90046
docs/js-covenants/names.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,899 @@
/*!
* namestate.js - name states for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const Network = require('../protocol/network');
const Outpoint = require('../primitives/outpoint');
const {ZERO_HASH} = require('../protocol/consensus');
const NameDelta = require('./namedelta');
const {encoding} = bio;
/*
* Constants
*/
const states = {
OPENING: 0,
LOCKED: 1,
BIDDING: 2,
REVEAL: 3,
CLOSED: 4,
REVOKED: 5
};
const statesByVal = {
[states.OPENING]: 'OPENING',
[states.LOCKED]: 'LOCKED',
[states.BIDDING]: 'BIDDING',
[states.REVEAL]: 'REVEAL',
[states.CLOSED]: 'CLOSED',
[states.REVOKED]: 'REVOKED'
};
const EMPTY = Buffer.alloc(0);
/**
* NameState
* @extends {bio.Struct}
*/
class NameState extends bio.Struct {
constructor() {
super();
this.name = EMPTY;
this.nameHash = ZERO_HASH;
this.height = 0;
this.renewal = 0;
this.owner = new Outpoint();
this.value = 0;
this.highest = 0;
this.data = EMPTY;
this.transfer = 0;
this.revoked = 0;
this.claimed = 0;
this.renewals = 0;
this.registered = false;
this.expired = false;
this.weak = false;
// Not serialized.
this._delta = null;
}
get delta() {
if (!this._delta)
this._delta = new NameDelta();
return this._delta;
}
set delta(delta) {
this._delta = delta;
}
inject(ns) {
assert(ns instanceof this.constructor);
this.name = ns.name;
this.nameHash = ns.nameHash;
this.height = ns.height;
this.renewal = ns.renewal;
this.owner = ns.owner;
this.value = ns.value;
this.highest = ns.highest;
this.data = ns.data;
this.transfer = ns.transfer;
this.revoked = ns.revoked;
this.claimed = ns.claimed;
this.renewals = ns.renewals;
this.registered = ns.registered;
this.expired = ns.expired;
this.weak = ns.weak;
return this;
}
clear() {
this._delta = null;
return this;
}
isNull() {
return this.height === 0
&& this.renewal === 0
&& this.owner.isNull()
&& this.value === 0
&& this.highest === 0
&& this.data.length === 0
&& this.transfer === 0
&& this.revoked === 0
&& this.claimed === 0
&& this.renewals === 0
&& this.registered === false
&& this.expired === false
&& this.weak === false;
}
hasDelta() {
return this._delta && !this._delta.isNull();
}
state(height, network) {
assert((height >>> 0) === height);
assert(network && network.names);
const {
treeInterval,
lockupPeriod,
biddingPeriod,
revealPeriod
} = network.names;
const openPeriod = treeInterval + 1;
if (this.revoked !== 0)
return states.REVOKED;
if (this.claimed !== 0) {
if (height < this.height + lockupPeriod)
return states.LOCKED;
return states.CLOSED;
}
if (height < this.height + openPeriod)
return states.OPENING;
if (height < this.height + openPeriod + biddingPeriod)
return states.BIDDING;
if (height < this.height + openPeriod + biddingPeriod + revealPeriod)
return states.REVEAL;
return states.CLOSED;
}
isOpening(height, network) {
return this.state(height, network) === states.OPENING;
}
isLocked(height, network) {
return this.state(height, network) === states.LOCKED;
}
isBidding(height, network) {
return this.state(height, network) === states.BIDDING;
}
isReveal(height, network) {
return this.state(height, network) === states.REVEAL;
}
isClosed(height, network) {
return this.state(height, network) === states.CLOSED;
}
isRevoked(height, network) {
return this.state(height, network) === states.REVOKED;
}
isRedeemable(height, network) {
return this.state(height, network) >= states.CLOSED;
}
isClaimable(height, network) {
assert((height >>> 0) === height);
assert(network && network.names);
return this.claimed !== 0
&& !network.names.noReserved
&& height < network.names.claimPeriod;
}
isExpired(height, network) {
assert((height >>> 0) === height);
assert(network && network.names);
if (this.revoked !== 0) {
if (height < this.revoked + network.names.auctionMaturity)
return false;
return true;
}
// Can only renew once we reach the closed state.
if (!this.isClosed(height, network))
return false;
// Claimed names can only expire once the claim period is over.
if (this.isClaimable(height, network))
return false;
// If we haven't been renewed in two years, start over.
if (height >= this.renewal + network.names.renewalWindow)
return true;
// If nobody revealed their bids, start over.
if (this.owner.isNull())
return true;
return false;
}
maybeExpire(height, network) {
if (this.isExpired(height, network)) {
const {data} = this;
// Note: we keep the name data even
// after expiration (but not revocation).
this.reset(height);
this.setExpired(true);
this.setData(data);
return true;
}
return false;
}
reset(height) {
assert((height >>> 0) === height);
this.setHeight(height);
this.setRenewal(height);
this.setOwner(new Outpoint());
this.setValue(0);
this.setHighest(0);
this.setData(null);
this.setTransfer(0);
this.setRevoked(0);
this.setClaimed(0);
this.setRenewals(0);
this.setRegistered(false);
this.setExpired(false);
this.setWeak(false);
return this;
}
set(name, height) {
assert(Buffer.isBuffer(name));
this.name = name;
this.reset(height);
return this;
}
setHeight(height) {
assert((height >>> 0) === height);
if (height === this.height)
return this;
if (this.delta.height === null)
this.delta.height = this.height;
this.height = height;
return this;
}
setRenewal(renewal) {
assert((renewal >>> 0) === renewal);
if (renewal === this.renewal)
return this;
if (this.delta.renewal === null)
this.delta.renewal = this.renewal;
this.renewal = renewal;
return this;
}
setOwner(owner) {
assert(owner instanceof Outpoint);
if (owner.equals(this.owner))
return this;
if (this.delta.owner === null)
this.delta.owner = this.owner;
this.owner = owner;
return this;
}
setValue(value) {
assert(Number.isSafeInteger(value) && value >= 0);
if (value === this.value)
return this;
if (this.delta.value === null)
this.delta.value = this.value;
this.value = value;
return this;
}
setHighest(highest) {
assert(Number.isSafeInteger(highest) && highest >= 0);
if (highest === this.highest)
return this;
if (this.delta.highest === null)
this.delta.highest = this.highest;
this.highest = highest;
return this;
}
setData(data) {
if (data === null)
data = EMPTY;
assert(Buffer.isBuffer(data));
if (this.data.equals(data))
return this;
if (this.delta.data === null)
this.delta.data = this.data;
this.data = data;
return this;
}
setTransfer(transfer) {
assert((transfer >>> 0) === transfer);
if (transfer === this.transfer)
return this;
if (this.delta.transfer === null)
this.delta.transfer = this.transfer;
this.transfer = transfer;
return this;
}
setRevoked(revoked) {
assert((revoked >>> 0) === revoked);
if (revoked === this.revoked)
return this;
if (this.delta.revoked === null)
this.delta.revoked = this.revoked;
this.revoked = revoked;
return this;
}
setClaimed(claimed) {
assert((claimed >>> 0) === claimed);
if (claimed === this.claimed)
return this;
if (this.delta.claimed === null)
this.delta.claimed = this.claimed;
this.claimed = claimed;
return this;
}
setRenewals(renewals) {
assert((renewals >>> 0) === renewals);
if (renewals === this.renewals)
return this;
if (this.delta.renewals === null)
this.delta.renewals = this.renewals;
this.renewals = renewals;
return this;
}
setRegistered(registered) {
assert(typeof registered === 'boolean');
if (registered === this.registered)
return this;
if (this.delta.registered === null)
this.delta.registered = this.registered;
this.registered = registered;
return this;
}
setExpired(expired) {
assert(typeof expired === 'boolean');
if (expired === this.expired)
return this;
if (this.delta.expired === null)
this.delta.expired = this.expired;
this.expired = expired;
return this;
}
setWeak(weak) {
assert(typeof weak === 'boolean');
if (weak === this.weak)
return this;
if (this.delta.weak === null)
this.delta.weak = this.weak;
this.weak = weak;
return this;
}
applyState(delta) {
assert(delta instanceof NameDelta);
if (delta.height !== null)
this.height = delta.height;
if (delta.renewal !== null)
this.renewal = delta.renewal;
if (delta.owner !== null)
this.owner = delta.owner;
if (delta.value !== null)
this.value = delta.value;
if (delta.highest !== null)
this.highest = delta.highest;
if (delta.data !== null)
this.data = delta.data;
if (delta.transfer !== null)
this.transfer = delta.transfer;
if (delta.revoked !== null)
this.revoked = delta.revoked;
if (delta.claimed !== null)
this.claimed = delta.claimed;
if (delta.renewals !== null)
this.renewals = delta.renewals;
if (delta.registered !== null)
this.registered = delta.registered;
if (delta.expired !== null)
this.expired = delta.expired;
if (delta.weak !== null)
this.weak = delta.weak;
return this;
}
getSize() {
let size = 0;
size += 1;
size += this.name.length;
size += 2;
size += this.data.length;
size += 4;
size += 4;
size += 2;
if (!this.owner.isNull())
size += 32 + encoding.sizeVarint(this.owner.index);
if (this.value !== 0)
size += encoding.sizeVarint(this.value);
if (this.highest !== 0)
size += encoding.sizeVarint(this.highest);
if (this.transfer !== 0)
size += 4;
if (this.revoked !== 0)
size += 4;
if (this.claimed !== 0)
size += 4;
if (this.renewals !== 0)
size += encoding.sizeVarint(this.renewals);
return size;
}
getField() {
let field = 0;
if (!this.owner.isNull())
field |= 1 << 0;
if (this.value !== 0)
field |= 1 << 1;
if (this.highest !== 0)
field |= 1 << 2;
if (this.transfer !== 0)
field |= 1 << 3;
if (this.revoked !== 0)
field |= 1 << 4;
if (this.claimed !== 0)
field |= 1 << 5;
if (this.renewals !== 0)
field |= 1 << 6;
if (this.registered)
field |= 1 << 7;
if (this.expired)
field |= 1 << 8;
if (this.weak)
field |= 1 << 9;
return field;
}
write(bw) {
bw.writeU8(this.name.length);
bw.writeBytes(this.name);
bw.writeU16(this.data.length);
bw.writeBytes(this.data);
bw.writeU32(this.height);
bw.writeU32(this.renewal);
bw.writeU16(this.getField());
if (!this.owner.isNull()) {
bw.writeHash(this.owner.hash);
bw.writeVarint(this.owner.index);
}
if (this.value !== 0)
bw.writeVarint(this.value);
if (this.highest !== 0)
bw.writeVarint(this.highest);
if (this.transfer !== 0)
bw.writeU32(this.transfer);
if (this.revoked !== 0)
bw.writeU32(this.revoked);
if (this.claimed !== 0)
bw.writeU32(this.claimed);
if (this.renewals !== 0)
bw.writeVarint(this.renewals);
return bw;
}
read(br) {
this.name = br.readBytes(br.readU8());
this.data = br.readBytes(br.readU16());
this.height = br.readU32();
this.renewal = br.readU32();
const field = br.readU16();
if (field & (1 << 0)) {
this.owner.hash = br.readHash();
this.owner.index = br.readVarint();
}
if (field & (1 << 1))
this.value = br.readVarint();
if (field & (1 << 2))
this.highest = br.readVarint();
if (field & (1 << 3))
this.transfer = br.readU32();
if (field & (1 << 4))
this.revoked = br.readU32();
if (field & (1 << 5))
this.claimed = br.readU32();
if (field & (1 << 6))
this.renewals = br.readVarint();
if (field & (1 << 7))
this.registered = true;
if (field & (1 << 8))
this.expired = true;
if (field & (1 << 9))
this.weak = true;
return this;
}
getJSON(height, network) {
let state = undefined;
let stats = undefined;
if (height != null) {
network = Network.get(network);
state = this.state(height, network);
state = statesByVal[state];
stats = this.toStats(height, network);
}
return {
name: this.name.toString('binary'),
nameHash: this.nameHash.toString('hex'),
state: state,
height: this.height,
renewal: this.renewal,
owner: this.owner.toJSON(),
value: this.value,
highest: this.highest,
data: this.data.toString('hex'),
transfer: this.transfer,
revoked: this.revoked,
claimed: this.claimed,
renewals: this.renewals,
registered: this.registered,
expired: this.expired,
weak: this.weak,
stats: stats
};
}
fromJSON(json) {
assert(json && typeof json === 'object');
assert(typeof json.name === 'string');
assert(json.name.length >= 0 && json.name.length <= 63);
assert(typeof json.nameHash === 'string');
assert(json.nameHash.length === 64);
assert((json.height >>> 0) === json.height);
assert((json.renewal >>> 0) === json.renewal);
assert(json.owner && typeof json.owner === 'object');
assert(Number.isSafeInteger(json.value) && json.value >= 0);
assert(Number.isSafeInteger(json.highest) && json.highest >= 0);
assert(typeof json.data === 'string');
assert((json.data.length & 1) === 0);
assert((json.transfer >>> 0) === json.transfer);
assert((json.revoked >>> 0) === json.revoked);
assert((json.claimed >>> 0) === json.claimed);
assert((json.renewals >>> 0) === json.renewals);
assert(typeof json.registered === 'boolean');
assert(typeof json.expired === 'boolean');
assert(typeof json.weak === 'boolean');
this.name = Buffer.from(json.name, 'binary');
this.nameHash = Buffer.from(json.nameHash, 'hex');
this.height = json.height;
this.renewal = json.renewal;
this.owner = Outpoint.fromJSON(json.owner);
this.value = json.value;
this.highest = json.highest;
this.data = Buffer.from(json.data, 'hex');
this.transfer = json.transfer;
this.revoked = json.revoked;
this.claimed = json.claimed;
this.renewals = json.renewals;
this.registered = json.registered;
this.expired = json.expired;
this.weak = json.weak;
return this;
}
toStats(height, network) {
assert((height >>> 0) === height);
assert(network && network.names);
const {blocksPerDay} = network.pow;
const blocksPerHour = blocksPerDay / 24;
const {
treeInterval,
lockupPeriod,
biddingPeriod,
revealPeriod,
renewalWindow,
auctionMaturity,
transferLockup,
claimPeriod
} = network.names;
const openPeriod = treeInterval + 1;
const stats = {};
let state = this.state(height, network);
// Special case for a state that is not a state:
// EXPIRED but not revoked.
const EXPIRED = -1;
if (this.isExpired(height, network)) {
if (this.owner.isNull())
return null;
if (state !== states.REVOKED)
state = EXPIRED;
}
switch (state) {
case states.OPENING: {
const start = this.height;
const end = this.height + openPeriod;
const blocks = end - height;
const hours = blocks / blocksPerHour;
stats.openPeriodStart = start;
stats.openPeriodEnd = end;
stats.blocksUntilBidding = blocks;
stats.hoursUntilBidding = Number(hours.toFixed(2));
break;
}
case states.LOCKED: {
const start = this.height;
const end = this.height + lockupPeriod;
const blocks = end - height;
const hours = blocks / blocksPerHour;
stats.lockupPeriodStart = start;
stats.lockupPeriodEnd = end;
stats.blocksUntilClosed = blocks;
stats.hoursUntilClosed = Number(hours.toFixed(2));
break;
}
case states.BIDDING: {
const start = this.height + openPeriod;
const end = start + biddingPeriod;
const blocks = end - height;
const hours = blocks / blocksPerHour;
stats.bidPeriodStart = start;
stats.bidPeriodEnd = end;
stats.blocksUntilReveal = blocks;
stats.hoursUntilReveal = Number(hours.toFixed(2));
break;
}
case states.REVEAL: {
const start = this.height + openPeriod + biddingPeriod;
const end = start + revealPeriod;
const blocks = end - height;
const hours = blocks / blocksPerHour;
stats.revealPeriodStart = start;
stats.revealPeriodEnd = end;
stats.blocksUntilClose = blocks;
stats.hoursUntilClose = Number(hours.toFixed(2));
break;
}
case states.CLOSED: {
const start = this.renewal;
const normalEnd = start + renewalWindow;
const end = this.claimed ? Math.max(claimPeriod, normalEnd) : normalEnd;
const blocks = end - height;
const days = blocks / blocksPerDay;
stats.renewalPeriodStart = start;
stats.renewalPeriodEnd = end;
stats.blocksUntilExpire = blocks;
assert(stats.blocksUntilExpire >= 0);
stats.daysUntilExpire = Number(days.toFixed(2));
// Add these details if name is in mid-transfer
if (this.transfer !== 0) {
const start = this.transfer;
const end = start + transferLockup;
const blocks = end - height;
const hours = blocks / blocksPerHour;
stats.transferLockupStart = start;
stats.transferLockupEnd = end;
stats.blocksUntilValidFinalize = blocks;
stats.hoursUntilValidFinalize = Number(hours.toFixed(2));
}
break;
}
case states.REVOKED: {
const start = this.revoked;
const end = start + auctionMaturity;
const blocks = end - height;
const hours = blocks / blocksPerHour;
stats.revokePeriodStart = start;
stats.revokePeriodEnd = end;
stats.blocksUntilReopen = blocks;
stats.hoursUntilReopen = Number(hours.toFixed(2));
break;
}
case EXPIRED: {
const expired = this.renewal + renewalWindow;
stats.blocksSinceExpired = height - expired;
break;
}
}
return stats;
}
format(height, network) {
return this.getJSON(height, network);
}
}
/*
* Static
*/
NameState.states = states;
NameState.statesByVal = statesByVal;
// Max size: 668
NameState.MAX_SIZE = (0
+ 1 + 63
+ 2 + 512
+ 4
+ 4
+ 2
+ 32
+ 9
+ 9
+ 9
+ 4
+ 4
+ 4
+ 9);
/*
* Expose
*/
module.exports = NameState;

View file

@ -0,0 +1,312 @@
/*!
* ownership.js - DNSSEC ownership proofs for hsd
* Copyright (c) 2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const base32 = require('bcrypto/lib/encoding/base32');
const util = require('bns/lib/util');
const blake2b = require('bcrypto/lib/blake2b');
const StubResolver = require('bns/lib/resolver/stub');
const BNSOwnership = require('bns/lib/ownership');
const consensus = require('../protocol/consensus');
const Network = require('../protocol/network');
const reserved = require('./reserved');
const {Proof: BNSProof} = BNSOwnership;
/** @typedef {import('../types').NetworkType} NetworkType */
/*
* Constants
*/
const EMPTY = Buffer.alloc(0);
/** @type {Ownership} */
let ownership = null;
/**
* Proof
*/
class Proof extends BNSProof {
constructor() {
super();
}
/**
* @param {Buffer} data
* @returns {this}
*/
decode(data) {
const br = bio.read(data);
if (data.length > 10000)
throw new Error('Proof too large.');
this.read(br);
if (br.left() !== 0)
throw new Error('Trailing data.');
return this;
}
/**
* @returns {String[]}
*/
getNames() {
const target = this.getTarget();
if (target === '.')
return ['', target];
return [util.label(target, 0), target];
}
/**
* @returns {String}
*/
getName() {
return this.getNames()[0];
}
addData(items) {
return ownership.addData(this, items);
}
getData(network) {
return ownership.getData(this, network);
}
isWeak() {
return ownership.isWeak(this);
}
getWindow() {
return ownership.getWindow(this);
}
isSane() {
return ownership.isSane(this);
}
verifyTimes(time) {
return ownership.verifyTimes(this, time);
}
verifySignatures() {
return ownership.verifySignatures(this);
}
}
/**
* Ownership
*/
class Ownership extends BNSOwnership {
constructor() {
super();
this.Resolver = StubResolver;
this.secure = true;
this.Proof = Proof;
this.OwnershipProof = Proof;
}
hasPrefix(proof, target, [txt]) {
// Used only in testing.
return /^hns-[0-9a-z]+:/.test(txt);
}
isData(proof, target, [txt], network) {
assert(network && typeof network.claimPrefix === 'string');
const prefix = network.claimPrefix;
return util.startsWith(txt, prefix);
}
decodeData(txt, network) {
if (typeof network === 'string')
network = Network.get(network);
assert(network && typeof network.claimPrefix === 'string');
const prefix = network.claimPrefix;
const b32 = txt.substring(prefix.length);
const raw = base32.decode(b32);
const br = bio.read(raw);
const version = br.readU8();
const size = br.readU8();
const hash = br.readBytes(size);
const fee = br.readVarint();
const commitHash = br.readHash();
const commitHeight = br.readU32();
return {
address: {
version,
hash: hash.toString('hex')
},
fee,
commitHash: commitHash.toString('hex'),
commitHeight
};
}
parseData(proof, target, [txt], network) {
assert(target !== '.');
assert(network && typeof network.claimPrefix === 'string');
const prefix = network.claimPrefix;
const b32 = txt.substring(prefix.length);
const raw = base32.decode(b32);
const br = bio.read(raw);
const version = br.readU8();
if (version > 31)
return null;
const size = br.readU8();
if (size < 2 || size > 40)
return null;
const hash = br.readBytes(size);
const fee = br.readVarint();
if (fee > consensus.MAX_MONEY)
return null;
const commitHash = br.readHash();
const commitHeight = br.readU32();
br.verifyChecksum(blake2b.digest);
if (br.left() !== 0)
return null;
const name = util.label(target, 0);
const item = reserved.getByName(name);
if (!item)
return null;
if (target !== item.target)
return null;
const value = item.value;
if (fee > value)
return null;
const [inception, expiration] = proof.getWindow();
if (inception === 0 && expiration === 0)
return null;
const weak = proof.isWeak();
const data = new ProofData();
data.name = name;
data.target = target;
data.weak = weak;
data.commitHash = commitHash;
data.commitHeight = commitHeight;
data.inception = inception;
data.expiration = expiration;
data.fee = fee;
data.value = value;
data.version = version;
data.hash = hash;
return data;
}
createData(address, fee, commitHash, commitHeight, network) {
assert(address && address.hash);
assert(Number.isSafeInteger(fee) && fee >= 0);
assert(Buffer.isBuffer(commitHash) && commitHash.length === 32);
assert((commitHeight >>> 0) === commitHeight);
assert(commitHeight !== 0);
assert(network && network.claimPrefix);
const prefix = network.claimPrefix;
const {version, hash} = address;
assert(typeof prefix === 'string');
assert((version & 0xff) === version);
assert(Buffer.isBuffer(hash));
assert(version <= 31);
assert(hash.length >= 2 && hash.length <= 40);
const size = 1
+ 1 + hash.length
+ bio.sizeVarint(fee)
+ 32
+ 4
+ 4;
const bw = bio.write(size);
bw.writeU8(version);
bw.writeU8(hash.length);
bw.writeBytes(hash);
bw.writeVarint(fee);
bw.writeHash(commitHash);
bw.writeU32(commitHeight);
bw.writeChecksum(blake2b.digest);
const raw = bw.render();
return prefix + base32.encode(raw);
}
}
/**
* ProofData
*/
class ProofData {
constructor() {
this.name = '';
this.target = '.';
this.weak = false;
this.commitHash = blake2b.zero;
this.commitHeight = 0;
this.inception = 0;
this.expiration = 0;
this.fee = 0;
this.value = 0;
this.version = 0;
this.hash = EMPTY;
}
}
/*
* Ownership
*/
ownership = new Ownership();
/*
* Expose
*/
Ownership.Proof = Proof;
Ownership.OwnershipProof = Proof;
Ownership.ProofData = ProofData;
Ownership.ownership = ownership;
module.exports = Ownership;

View file

@ -0,0 +1,145 @@
'use strict';
const assert = require('bsert');
const sha3 = require('bcrypto/lib/sha3');
const data = require('./names.json');
/*
* Constants
*/
const ZERO_HASH = sha3.zero.toString('hex');
/**
* Reserved
*/
class Reserved {
constructor(data) {
const meta = data[ZERO_HASH];
this.data = data;
this.size = meta[0];
this.nameValue = meta[1];
this.rootValue = meta[2];
this.topValue = meta[3];
}
has(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
const hex = hash.toString('hex');
const item = this.data[hex];
if (!item)
return false;
return Array.isArray(item);
}
get(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
const hex = hash.toString('hex');
const item = this.data[hex];
if (!item || !Array.isArray(item))
return null;
const target = item[0];
const flags = item[1];
const index = target.indexOf('.');
assert(index !== -1);
const root = (flags & 1) !== 0;
const top100 = (flags & 2) !== 0;
const custom = (flags & 4) !== 0;
const zero = (flags & 8) !== 0;
const name = target.substring(0, index);
let value = this.nameValue;
if (root)
value += this.rootValue;
if (top100)
value += this.topValue;
if (custom)
value += item[2];
if (zero)
value = 0;
return {
name,
hash,
target,
value,
root,
top100,
custom,
zero
};
}
hasByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return false;
return this.has(hashName(name));
}
getByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return null;
return this.get(hashName(name));
}
*entries() {
const keys = Object.keys(this.data);
for (const key of keys) {
const hash = Buffer.from(key, 'hex');
yield [hash, this.get(hash)];
}
}
*keys() {
const keys = Object.keys(this.data);
for (const key of keys)
yield Buffer.from(key, 'hex');
}
*values() {
for (const [, item] of this.entries())
yield item;
}
[Symbol.iterator]() {
return this.entries();
}
}
/*
* Helpers
*/
function hashName(name) {
const raw = Buffer.from(name.toLowerCase(), 'ascii');
return sha3.digest(raw);
}
/*
* Expose
*/
module.exports = new Reserved(data);

View file

@ -0,0 +1,210 @@
'use strict';
const assert = require('bsert');
const Path = require('path');
const fs = require('bfile');
const sha3 = require('bcrypto/lib/sha3');
/*
* Constants
*/
const FILE = Path.resolve(__dirname, 'names.db');
const DATA = fs.readFileSync(FILE);
/**
* Reserved
*/
class Reserved {
constructor(data) {
this.data = data;
this.size = readU32(data, 0);
this.nameValue = readU64(data, 4);
this.rootValue = readU64(data, 12);
this.topValue = readU64(data, 20);
}
_compare(b, off) {
const a = this.data;
for (let i = 0; i < 32; i++) {
const x = a[off + i];
const y = b[i];
if (x < y)
return -1;
if (x > y)
return 1;
}
return 0;
}
_find(key) {
let start = 0;
let end = this.size - 1;
while (start <= end) {
const index = (start + end) >>> 1;
const pos = 28 + index * 36;
const cmp = this._compare(key, pos);
if (cmp === 0)
return readU32(this.data, pos + 32);
if (cmp < 0)
start = index + 1;
else
end = index - 1;
}
return -1;
}
_target(pos) {
const len = this.data[pos];
return this.data.toString('ascii', pos + 1, pos + 1 + len);
}
_flags(pos) {
const len = this.data[pos];
return this.data[pos + 1 + len];
}
_index(pos) {
const len = this.data[pos];
return this.data[pos + 1 + len + 1];
}
_value(pos) {
const len = this.data[pos];
const off = pos + 1 + len + 1 + 1;
return readU64(this.data, off);
}
_get(hash, pos) {
const target = this._target(pos);
const flags = this._flags(pos);
const index = this._index(pos);
const root = (flags & 1) !== 0;
const top100 = (flags & 2) !== 0;
const custom = (flags & 4) !== 0;
const zero = (flags & 8) !== 0;
const name = target.substring(0, index);
let value = this.nameValue;
if (root)
value += this.rootValue;
if (top100)
value += this.topValue;
if (custom)
value += this._value(pos);
if (zero)
value = 0;
return {
name,
hash,
target,
value,
root,
top100,
custom,
zero
};
}
has(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
return this._find(hash) !== -1;
}
get(hash) {
assert(Buffer.isBuffer(hash) && hash.length === 32);
const pos = this._find(hash);
if (pos === -1)
return null;
return this._get(hash, pos);
}
hasByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return false;
return this.has(hashName(name));
}
getByName(name) {
assert(typeof name === 'string');
if (name.length === 0 || name.length > 63)
return null;
return this.get(hashName(name));
}
*entries() {
for (let i = 0; i < this.size; i++) {
const pos = 28 + i * 36;
const hash = this.data.slice(pos, pos + 32);
const ptr = readU32(this.data, pos + 32);
const item = this._get(hash, ptr);
yield [hash, item];
}
}
*keys() {
for (let i = 0; i < this.size; i++) {
const pos = 28 + i * 36;
yield this.data.slice(pos, pos + 32);
}
}
*values() {
for (const [, item] of this.entries())
yield item;
}
[Symbol.iterator]() {
return this.entries();
}
}
/*
* Helpers
*/
function readU32(data, off) {
return data.readUInt32LE(off);
}
function readU64(data, off) {
const lo = data.readUInt32LE(off);
const hi = data.readUInt32LE(off + 4);
return hi * 0x100000000 + lo;
}
function hashName(name) {
const raw = Buffer.from(name.toLowerCase(), 'ascii');
return sha3.digest(raw);
}
/*
* Expose
*/
module.exports = new Reserved(DATA);

1496
docs/js-covenants/rules.js Normal file

File diff suppressed because it is too large Load diff

68
docs/js-covenants/undo.js Normal file
View file

@ -0,0 +1,68 @@
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const NameDelta = require('./namedelta');
class NameUndo extends bio.Struct {
constructor() {
super();
this.names = [];
}
fromView(view) {
assert(view && view.names);
for (const ns of view.names.values()) {
if (!ns.hasDelta())
continue;
this.names.push([ns.nameHash, ns.delta]);
}
return this;
}
getSize() {
let size = 0;
size += 4;
for (const [, delta] of this.names) {
size += 32;
size += delta.getSize();
}
return size;
}
write(bw) {
bw.writeU32(this.names.length);
for (const [nameHash, delta] of this.names) {
bw.writeBytes(nameHash);
delta.write(bw);
}
return bw;
}
read(br) {
const count = br.readU32();
for (let i = 0; i < count; i++) {
const nameHash = br.readBytes(32);
const delta = NameDelta.read(br);
this.names.push([nameHash, delta]);
}
return this;
}
static fromView(view) {
return new this().fromView(view);
}
}
module.exports = NameUndo;

81
docs/js-covenants/view.js Normal file
View file

@ -0,0 +1,81 @@
'use strict';
const assert = require('bsert');
const {BufferMap} = require('buffer-map');
const NameState = require('./namestate');
const NameUndo = require('./undo');
/** @typedef {import('../types').Hash} Hash */
class View {
constructor() {
/** @type {BufferMap<NameState>} */
this.names = new BufferMap();
}
/**
* @param {Object} db
* @param {Hash} nameHash
* @returns {NameState}
*/
getNameStateSync(db, nameHash) {
assert(db && typeof db.getNameState === 'function');
assert(Buffer.isBuffer(nameHash));
const cache = this.names.get(nameHash);
if (cache)
return cache;
/** @type {NameState?} */
const ns = db.getNameState(nameHash);
if (!ns) {
const ns = new NameState();
ns.nameHash = nameHash;
this.names.set(nameHash, ns);
return ns;
}
this.names.set(nameHash, ns);
return ns;
}
/**
* @param {Object} db
* @param {Hash} nameHash
* @returns {Promise<NameState>}
*/
async getNameState(db, nameHash) {
assert(db && typeof db.getNameState === 'function');
assert(Buffer.isBuffer(nameHash));
const cache = this.names.get(nameHash);
if (cache)
return cache;
/** @type {NameState?} */
const ns = await db.getNameState(nameHash);
if (!ns) {
const ns = new NameState();
ns.nameHash = nameHash;
this.names.set(nameHash, ns);
return ns;
}
this.names.set(nameHash, ns);
return ns;
}
toNameUndo() {
return NameUndo.fromView(this);
}
}
module.exports = View;

56
docs/js-dns/common.js Normal file
View file

@ -0,0 +1,56 @@
/*!
* common.js - dns constants for hsd
* Copyright (c) 2021, The Handshake Developers (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/**
* @module dns/common
*/
exports.DUMMY = Buffer.alloc(0);
// About one mainnet Urkel Tree interval.
// (60 seconds * 10 minutes * 36)
exports.DEFAULT_TTL = 21600;
// NS SOA RRSIG NSEC DNSKEY
// Types available for the root "."
exports.TYPE_MAP_ROOT = Buffer.from('000722000000000380', 'hex');
// RRSIG NSEC
exports.TYPE_MAP_EMPTY = Buffer.from('0006000000000003', 'hex');
// NS RRSIG NSEC
exports.TYPE_MAP_NS = Buffer.from('0006200000000003', 'hex');
// TXT RRSIG NSEC
exports.TYPE_MAP_TXT = Buffer.from('0006000080000003', 'hex');
// A RRSIG NSEC
exports.TYPE_MAP_A = Buffer.from('0006400000000003', 'hex');
// AAAA RRSIG NSEC
exports.TYPE_MAP_AAAA = Buffer.from('0006000000080003', 'hex');
exports.hsTypes = {
DS: 0,
NS: 1,
GLUE4: 2,
GLUE6: 3,
SYNTH4: 4,
SYNTH6: 5,
TXT: 6
};
exports.hsTypesByVal = {
[exports.hsTypes.DS]: 'DS',
[exports.hsTypes.NS]: 'NS',
[exports.hsTypes.GLUE4]: 'GLUE4',
[exports.hsTypes.GLUE6]: 'GLUE6',
[exports.hsTypes.SYNTH4]: 'SYNTH4',
[exports.hsTypes.SYNTH6]: 'SYNTH6',
[exports.hsTypes.TXT]: 'TXT'
};

62
docs/js-dns/key.js Normal file
View file

@ -0,0 +1,62 @@
/*!
* key.js - dnssec key for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const {dnssec, wire} = require('bns');
const {Record} = wire;
// pub: 034fd714449d8cfcccfdaba52c64d63e3aca72be3f94bfeb60aeb5a42ed3d0c205
exports.kskPriv = Buffer.from(
'1c74c825c5b0f08cf6be846bfc93c423f03e3e1f6202fb2d96474b1520bbafad',
'hex');
// pub: 032399cfb3a72515ad609f09fd22954319d24b7c438dce00f535c7ee13010856e2
exports.zskPriv = Buffer.from(
'54276ff8604a3494c5c76d6651f14b289c7253ba636be4bfd7969308f48da47d',
'hex');
exports.ksk = Record.fromJSON({
name: '.',
ttl: 10800,
class: 'IN',
type: 'DNSKEY',
data: {
flags: 257,
protocol: 3,
algorithm: 13,
publicKey: ''
+ 'T9cURJ2M/Mz9q6UsZNY+Ospyvj+Uv+tgrrWkLtPQwgU/Xu5Yk0l02Sn5ua2x'
+ 'AQfEYIzRO6v5iA+BejMeEwNP4Q=='
}
});
exports.zsk = Record.fromJSON({
name: '.',
ttl: 10800,
class: 'IN',
type: 'DNSKEY',
data: {
flags: 256,
protocol: 3,
algorithm: 13,
publicKey: ''
+ 'I5nPs6clFa1gnwn9IpVDGdJLfEONzgD1NcfuEwEIVuIoHdZGgvVblsLNbRO+'
+ 'spW3nQYHg92svhy1HOjTiFBIsQ=='
}
});
// . DS 35215 13 2
// 7C50EA94A63AEECB65B510D1EAC1846C973A89D4AB292287D5A4D715136B57A3
exports.ds = dnssec.createDS(exports.ksk, dnssec.hashes.SHA256);
exports.signKSK = function signKSK(section, type) {
return dnssec.signType(section, type, exports.ksk, exports.kskPriv);
};
exports.signZSK = function signZSK(section, type) {
return dnssec.signType(section, type, exports.zsk, exports.zskPriv);
};

66
docs/js-dns/nsec.js Normal file
View file

@ -0,0 +1,66 @@
'use strict';
const assert = require('bsert');
const {wire, util} = require('bns');
const {Record, NSECRecord, types} = wire;
const {DEFAULT_TTL} = require('./common');
function create(name, nextDomain, typeBitmap) {
const rr = new Record();
const rd = new NSECRecord();
rr.name = util.fqdn(name);
rr.type = types.NSEC;
rr.ttl = DEFAULT_TTL;
rd.nextDomain = util.fqdn(nextDomain);
rd.typeBitmap = typeBitmap;
rr.data = rd;
return rr;
}
// Find the successor of a top level name
function nextName(tld) {
tld = util.trimFQDN(tld.toLowerCase());
// If the label is already 63 octets
// increment last character by one
if (tld.length === 63) {
// Assuming no escaped octets are present
let last = tld.charCodeAt(62);
last = String.fromCharCode(last + 1);
return tld.slice(0, -1) + last + '.';
}
return tld + '\\000.';
}
// Find the predecessor of a top level name
function prevName(tld) {
tld = util.trimFQDN(tld.toLowerCase());
assert(tld.length !== 0);
// Decrement the last character by 1
// assuming no escaped octets are present
let last = tld.charCodeAt(tld.length - 1);
last = String.fromCharCode(last - 1);
tld = tld.slice(0, -1) + last;
// See RFC4034 6.1 Canonical DNS Name Order
// https://tools.ietf.org/html/rfc4034#section-6.1
// Appending \255 prevents names that begin
// with the decremented name from falling
// in range i.e. if the name is `hello` a lexically
// smaller name is `helln` append `\255`
// to ensure that helln\255 > hellna
// while keeping helln\255 < hello
if (tld.length < 63) {
tld += '\\255';
}
return util.fqdn(tld);
}
exports.create = create;
exports.prevName = prevName;
exports.nextName = nextName;

948
docs/js-dns/resource.js Normal file
View file

@ -0,0 +1,948 @@
/*!
* resource.js - hns records for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const {encoding, wire, util} = require('bns');
const base32 = require('bcrypto/lib/encoding/base32');
const {IP} = require('binet');
const bio = require('bufio');
const key = require('./key');
const nsec = require('./nsec');
const {Struct} = bio;
const {
DUMMY,
DEFAULT_TTL,
TYPE_MAP_EMPTY,
TYPE_MAP_NS,
TYPE_MAP_TXT,
hsTypes
} = require('./common');
const {
sizeName,
writeNameBW,
readNameBR,
sizeString,
writeStringBW,
readStringBR,
isName,
readIP,
writeIP
} = encoding;
const {
Message,
Record,
ARecord,
AAAARecord,
NSRecord,
TXTRecord,
DSRecord,
types
} = wire;
/**
* Resource
* @extends {Struct}
*/
class Resource extends Struct {
constructor() {
super();
this.ttl = DEFAULT_TTL;
this.records = [];
}
hasType(type) {
assert((type & 0xff) === type);
for (const record of this.records) {
if (record.type === type)
return true;
}
return false;
}
hasNS() {
for (const {type} of this.records) {
if (type < hsTypes.NS || type > hsTypes.SYNTH6)
continue;
return true;
}
return false;
}
hasDS() {
return this.hasType(hsTypes.DS);
}
encode() {
const bw = bio.write(512);
this.write(bw, new Map());
return bw.slice();
}
getSize(map) {
let size = 1;
for (const rr of this.records)
size += 1 + rr.getSize(map);
return size;
}
write(bw, map) {
bw.writeU8(0);
for (const rr of this.records) {
bw.writeU8(rr.type);
rr.write(bw, map);
}
return this;
}
read(br) {
const version = br.readU8();
if (version !== 0)
throw new Error(`Unknown serialization version: ${version}.`);
while (br.left()) {
const RD = typeToClass(br.readU8());
// Break at unknown records.
if (!RD)
break;
this.records.push(RD.read(br));
}
return this;
}
toNS(name) {
const authority = [];
const set = new Set();
for (const record of this.records) {
switch (record.type) {
case hsTypes.NS:
case hsTypes.GLUE4:
case hsTypes.GLUE6:
case hsTypes.SYNTH4:
case hsTypes.SYNTH6:
break;
default:
continue;
}
const rr = record.toDNS(name, this.ttl);
if (set.has(rr.data.ns))
continue;
set.add(rr.data.ns);
authority.push(rr);
}
return authority;
}
toGlue(name) {
const additional = [];
for (const record of this.records) {
switch (record.type) {
case hsTypes.GLUE4:
case hsTypes.GLUE6:
if (!util.isSubdomain(name, record.ns))
continue;
break;
case hsTypes.SYNTH4:
case hsTypes.SYNTH6:
break;
default:
continue;
}
additional.push(record.toGlue(record.ns, this.ttl));
}
return additional;
}
toDS(name) {
const answer = [];
for (const record of this.records) {
if (record.type !== hsTypes.DS)
continue;
answer.push(record.toDNS(name, this.ttl));
}
return answer;
}
toTXT(name) {
const answer = [];
for (const record of this.records) {
if (record.type !== hsTypes.TXT)
continue;
answer.push(record.toDNS(name, this.ttl));
}
return answer;
}
toZone(name, sign = false) {
const zone = [];
const set = new Set();
for (const record of this.records) {
const rr = record.toDNS(name, this.ttl);
if (rr.type === types.NS) {
if (set.has(rr.data.ns))
continue;
set.add(rr.data.ns);
}
zone.push(rr);
}
if (sign) {
const set = new Set();
for (const rr of zone)
set.add(rr.type);
const types = [...set].sort();
for (const type of types)
key.signZSK(zone, type);
}
// Add the glue last.
for (const record of this.records) {
switch (record.type) {
case hsTypes.GLUE4:
case hsTypes.GLUE6:
case hsTypes.SYNTH4:
case hsTypes.SYNTH6: {
if (!util.isSubdomain(name, record.ns))
continue;
zone.push(record.toGlue(record.ns, this.ttl));
break;
}
}
}
return zone;
}
toReferral(name, type, isTLD) {
const res = new Message();
// no DS referrals for TLDs
const badReferral = isTLD && type === types.DS;
if (this.hasNS() && !badReferral) {
res.authority = this.toNS(name).concat(this.toDS(name));
res.additional = this.toGlue(name);
if (this.hasDS()) {
key.signZSK(res.authority, types.DS);
} else {
// unsigned zone proof
res.authority.push(this.toNSEC(name));
key.signZSK(res.authority, types.NSEC);
}
} else {
// Needs SOA.
res.aa = true;
// negative answer proof
res.authority.push(this.toNSEC(name));
key.signZSK(res.authority, types.NSEC);
}
return res;
}
toNSEC(name) {
let typeMap = TYPE_MAP_EMPTY;
if (this.hasNS())
typeMap = TYPE_MAP_NS;
else if (this.hasType(hsTypes.TXT))
typeMap = TYPE_MAP_TXT;
return nsec.create(name, nsec.nextName(name), typeMap);
}
toDNS(name, type) {
assert(util.isFQDN(name));
assert((type >>> 0) === type);
const labels = util.split(name);
// Referral.
if (labels.length > 1) {
const tld = util.from(name, labels, -1);
return this.toReferral(tld, type, false);
}
// Potentially an answer.
const res = new Message();
// TLDs are authoritative over their own NS & TXT records.
// The NS records in the root zone are just "hints"
// and therefore are not signed by the root ZSK.
// The only records root is authoritative over is DS.
switch (type) {
case types.TXT:
if (!this.hasNS()) {
res.aa = true;
res.answer = this.toTXT(name);
key.signZSK(res.answer, types.TXT);
}
break;
case types.DS:
res.aa = true;
res.answer = this.toDS(name);
key.signZSK(res.answer, types.DS);
break;
}
// Nope, we may need a referral
if (res.answer.length === 0 && res.authority.length === 0) {
return this.toReferral(name, type, true);
}
return res;
}
getJSON(name) {
const json = { records: [] };
for (const record of this.records)
json.records.push(record.getJSON());
return json;
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid json.');
assert(Array.isArray(json.records), 'Invalid records.');
for (const item of json.records) {
assert(item && typeof item === 'object', 'Invalid record.');
const RD = stringToClass(item.type);
if (!RD)
throw new Error(`Unknown type: ${item.type}.`);
this.records.push(RD.fromJSON(item));
}
return this;
}
}
/**
* DS
* @extends {Struct}
*/
class DS extends Struct {
constructor() {
super();
this.keyTag = 0;
this.algorithm = 0;
this.digestType = 0;
this.digest = DUMMY;
}
get type() {
return hsTypes.DS;
}
getSize() {
return 5 + this.digest.length;
}
write(bw) {
bw.writeU16BE(this.keyTag);
bw.writeU8(this.algorithm);
bw.writeU8(this.digestType);
bw.writeU8(this.digest.length);
bw.writeBytes(this.digest);
return this;
}
read(br) {
this.keyTag = br.readU16BE();
this.algorithm = br.readU8();
this.digestType = br.readU8();
this.digest = br.readBytes(br.readU8());
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
const rr = new Record();
const rd = new DSRecord();
rr.name = name;
rr.type = types.DS;
rr.ttl = ttl;
rr.data = rd;
rd.keyTag = this.keyTag;
rd.algorithm = this.algorithm;
rd.digestType = this.digestType;
rd.digest = this.digest;
return rr;
}
getJSON() {
return {
type: 'DS',
keyTag: this.keyTag,
algorithm: this.algorithm,
digestType: this.digestType,
digest: this.digest.toString('hex')
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid DS record.');
assert(json.type === 'DS',
'Invalid DS record. Type must be "DS".');
assert((json.keyTag & 0xffff) === json.keyTag,
'Invalid DS record. KeyTag must be a uint16.');
assert((json.algorithm & 0xff) === json.algorithm,
'Invalid DS record. Algorithm must be a uint8.');
assert((json.digestType & 0xff) === json.digestType,
'Invalid DS record. DigestType must be a uint8.');
assert(typeof json.digest === 'string',
'Invalid DS record. Digest must be a String.');
assert((json.digest.length >>> 1) <= 255,
'Invalid DS record. Digest is too large.');
this.keyTag = json.keyTag;
this.algorithm = json.algorithm;
this.digestType = json.digestType;
this.digest = util.parseHex(json.digest);
return this;
}
}
/**
* NS
* @extends {Struct}
*/
class NS extends Struct {
constructor() {
super();
this.ns = '.';
}
get type() {
return hsTypes.NS;
}
getSize(map) {
return sizeName(this.ns, map);
}
write(bw, map) {
writeNameBW(bw, this.ns, map);
return this;
}
read(br) {
this.ns = readNameBR(br);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
getJSON() {
return {
type: 'NS',
ns: this.ns
};
}
fromJSON(json) {
assert(json && typeof json === 'object',
'Invalid NS record.');
assert(json.type === 'NS',
'Invalid NS record. Type must be "NS".');
assert(isName(json.ns),
'Invalid NS record. ns must be a valid name.');
this.ns = json.ns;
return this;
}
}
/**
* GLUE4
* @extends {Struct}
*/
class GLUE4 extends Struct {
constructor() {
super();
this.ns = '.';
this.address = '0.0.0.0';
}
get type() {
return hsTypes.GLUE4;
}
getSize(map) {
return sizeName(this.ns, map) + 4;
}
write(bw, map) {
writeNameBW(bw, this.ns, map);
writeIP(bw, this.address, 4);
return this;
}
read(br) {
this.ns = readNameBR(br);
this.address = readIP(br, 4);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createA(name, ttl, this.address);
}
getJSON() {
return {
type: 'GLUE4',
ns: this.ns,
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid GLUE4 record.');
assert(json.type === 'GLUE4',
'Invalid GLUE4 record. Type must be "GLUE4".');
assert(isName(json.ns),
'Invalid GLUE4 record. ns must be a valid name.');
assert(IP.isIPv4String(json.address),
'Invalid GLUE4 record. Address must be a valid IPv4 address.');
this.ns = json.ns;
this.address = IP.normalize(json.address);
return this;
}
}
/**
* GLUE6
* @extends {Struct}
*/
class GLUE6 extends Struct {
constructor() {
super();
this.ns = '.';
this.address = '::';
}
get type() {
return hsTypes.GLUE6;
}
getSize(map) {
return sizeName(this.ns, map) + 16;
}
write(bw, map) {
writeNameBW(bw, this.ns, map);
writeIP(bw, this.address, 16);
return this;
}
read(br) {
this.ns = readNameBR(br);
this.address = readIP(br, 16);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createAAAA(name, ttl, this.address);
}
getJSON() {
return {
type: 'GLUE6',
ns: this.ns,
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid GLUE6 record.');
assert(json.type === 'GLUE6',
'Invalid GLUE6 record. Type must be "GLUE6".');
assert(isName(json.ns),
'Invalid GLUE6 record. ns must be a valid name.');
assert(IP.isIPv6String(json.address),
'Invalid GLUE6 record. Address must be a valid IPv6 address.');
this.ns = json.ns;
this.address = IP.normalize(json.address);
return this;
}
}
/**
* SYNTH4
* @extends {Struct}
*/
class SYNTH4 extends Struct {
constructor() {
super();
this.address = '0.0.0.0';
}
get type() {
return hsTypes.SYNTH4;
}
get ns() {
const ip = IP.toBuffer(this.address).slice(12);
return `_${base32.encodeHex(ip)}._synth.`;
}
getSize() {
return 4;
}
write(bw) {
writeIP(bw, this.address, 4);
return this;
}
read(br) {
this.address = readIP(br, 4);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createA(name, ttl, this.address);
}
getJSON() {
return {
type: 'SYNTH4',
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid SYNTH4 record.');
assert(json.type === 'SYNTH4',
'Invalid SYNTH4 record. Type must be "SYNTH4".');
assert(IP.isIPv4String(json.address),
'Invalid SYNTH4 record. Address must be a valid IPv4 address.');
this.address = IP.normalize(json.address);
return this;
}
}
/**
* SYNTH6
* @extends {Struct}
*/
class SYNTH6 extends Struct {
constructor() {
super();
this.address = '::';
}
get type() {
return hsTypes.SYNTH6;
}
get ns() {
const ip = IP.toBuffer(this.address);
return `_${base32.encodeHex(ip)}._synth.`;
}
getSize() {
return 16;
}
write(bw) {
writeIP(bw, this.address, 16);
return this;
}
read(br) {
this.address = readIP(br, 16);
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
return createNS(name, ttl, this.ns);
}
toGlue(name = '.', ttl = DEFAULT_TTL) {
return createAAAA(name, ttl, this.address);
}
getJSON() {
return {
type: 'SYNTH6',
address: this.address
};
}
fromJSON(json) {
assert(json && typeof json === 'object', 'Invalid SYNTH6 record.');
assert(json.type === 'SYNTH6',
'Invalid SYNTH6 record. Type must be "SYNTH6".');
assert(IP.isIPv6String(json.address),
'Invalid SYNTH6 record. Address must be a valid IPv6 address.');
this.address = IP.normalize(json.address);
return this;
}
}
/**
* TXT
* @extends {Struct}
*/
class TXT extends Struct {
constructor() {
super();
this.txt = [];
}
get type() {
return hsTypes.TXT;
}
getSize() {
let size = 1;
for (const txt of this.txt)
size += sizeString(txt);
return size;
}
write(bw) {
bw.writeU8(this.txt.length);
for (const txt of this.txt)
writeStringBW(bw, txt);
return this;
}
read(br) {
const count = br.readU8();
for (let i = 0; i < count; i++)
this.txt.push(readStringBR(br));
return this;
}
toDNS(name = '.', ttl = DEFAULT_TTL) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
const rr = new Record();
const rd = new TXTRecord();
rr.name = name;
rr.type = types.TXT;
rr.ttl = ttl;
rr.data = rd;
rd.txt.push(...this.txt);
return rr;
}
getJSON() {
return {
type: 'TXT',
txt: this.txt
};
}
fromJSON(json) {
assert(json && typeof json === 'object',
'Invalid TXT record.');
assert(json.type === 'TXT',
'Invalid TXT record. Type must be "TXT".');
assert(Array.isArray(json.txt),
'Invalid TXT record. txt must be an Array.');
for (const txt of json.txt) {
assert(typeof txt === 'string',
'Invalid TXT record. Entries in txt Array must be type String.');
assert(txt.length <= 255,
'Invalid TXT record. Entries in txt Array must be <= 255 in length.');
this.txt.push(txt);
}
return this;
}
}
/*
* Helpers
*/
function typeToClass(type) {
assert((type & 0xff) === type);
switch (type) {
case hsTypes.DS:
return DS;
case hsTypes.NS:
return NS;
case hsTypes.GLUE4:
return GLUE4;
case hsTypes.GLUE6:
return GLUE6;
case hsTypes.SYNTH4:
return SYNTH4;
case hsTypes.SYNTH6:
return SYNTH6;
case hsTypes.TXT:
return TXT;
default:
return null;
}
}
function stringToClass(type) {
assert(typeof type === 'string');
if (!Object.prototype.hasOwnProperty.call(hsTypes, type))
return null;
return typeToClass(hsTypes[type]);
}
function createNS(name, ttl, ns) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
assert(util.isFQDN(ns));
const rr = new Record();
const rd = new NSRecord();
rr.name = name;
rr.ttl = ttl;
rr.type = types.NS;
rr.data = rd;
rd.ns = ns;
return rr;
}
function createA(name, ttl, address) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
assert(IP.isIPv4String(address));
const rr = new Record();
const rd = new ARecord();
rr.name = name;
rr.ttl = ttl;
rr.type = types.A;
rr.data = rd;
rd.address = address;
return rr;
}
function createAAAA(name, ttl, address) {
assert(util.isFQDN(name));
assert((ttl >>> 0) === ttl);
assert(IP.isIPv6String(address));
const rr = new Record();
const rd = new AAAARecord();
rr.name = name;
rr.ttl = ttl;
rr.type = types.AAAA;
rr.data = rd;
rd.address = address;
return rr;
}
/*
* Expose
*/
exports.Resource = Resource;
exports.DS = DS;
exports.NS = NS;
exports.GLUE4 = GLUE4;
exports.GLUE6 = GLUE6;
exports.SYNTH4 = SYNTH4;
exports.SYNTH6 = SYNTH6;
exports.TXT = TXT;

836
docs/js-dns/server.js Normal file
View file

@ -0,0 +1,836 @@
/*!
* dns.js - dns server for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const IP = require('binet');
const Logger = require('blgr');
const bns = require('bns');
const UnboundResolver = require('bns/lib/resolver/unbound');
const RecursiveResolver = require('bns/lib/resolver/recursive');
const RootResolver = require('bns/lib/resolver/root');
const secp256k1 = require('bcrypto/lib/secp256k1');
const LRU = require('blru');
const base32 = require('bcrypto/lib/encoding/base32');
const NameState = require('../covenants/namestate');
const rules = require('../covenants/rules');
const reserved = require('../covenants/reserved');
const {Resource} = require('./resource');
const key = require('./key');
const nsec = require('./nsec');
const {
DEFAULT_TTL,
TYPE_MAP_ROOT,
TYPE_MAP_EMPTY,
TYPE_MAP_NS,
TYPE_MAP_A,
TYPE_MAP_AAAA
} = require('./common');
const {
DNSServer,
hsig,
wire,
util
} = bns;
const {
Message,
Record,
ARecord,
AAAARecord,
NSRecord,
SOARecord,
types,
codes
} = wire;
/*
* Constants
*/
const RES_OPT = { inet6: false, tcp: true };
const CACHE_TTL = 30 * 60 * 1000;
/**
* RootCache
*/
class RootCache {
constructor(size) {
this.cache = new LRU(size);
}
set(name, type, msg) {
const key = toKey(name, type);
const raw = msg.compress();
this.cache.set(key, {
time: Date.now(),
raw
});
return this;
}
get(name, type) {
const key = toKey(name, type);
const item = this.cache.get(key);
if (!item)
return null;
if (Date.now() > item.time + CACHE_TTL)
return null;
return Message.decode(item.raw);
}
reset() {
this.cache.reset();
}
}
/**
* RootServer
* @extends {DNSServer}
*/
class RootServer extends DNSServer {
constructor(options) {
super(RES_OPT);
this.ra = false;
this.edns = true;
this.dnssec = true;
this.noSig0 = false;
this.icann = new RootResolver(RES_OPT);
this.logger = Logger.global;
this.key = secp256k1.privateKeyGenerate();
this.host = '127.0.0.1';
this.port = 5300;
this.lookup = null;
this.middle = null;
this.publicHost = '127.0.0.1';
// Plugins can add or remove items from
// this set before the server is opened.
this.blacklist = new Set([
'bit', // Namecoin
'eth', // ENS
'exit', // Tor
'gnu', // GNUnet (GNS)
'i2p', // Invisible Internet Project
'onion', // Tor
'tor', // OnioNS
'zkey' // GNS
]);
this.cache = new RootCache(3000);
if (options)
this.initOptions(options);
// Create SYNTH record to use for root zone NS
let ip = IP.toBuffer(this.publicHost);
if (IP.family(this.publicHost) === 4)
ip = ip.slice(12);
this.synth = `_${base32.encodeHex(ip)}._synth.`;
this.initNode();
}
initOptions(options) {
assert(options);
this.parseOptions(options);
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger.context('ns');
}
if (options.key != null) {
assert(Buffer.isBuffer(options.key));
assert(options.key.length === 32);
this.key = options.key;
}
if (options.host != null) {
assert(typeof options.host === 'string');
this.host = IP.normalize(options.host);
this.publicHost = this.host;
}
if (options.port != null) {
assert((options.port & 0xffff) === options.port);
assert(options.port !== 0);
this.port = options.port;
}
if (options.lookup != null) {
assert(typeof options.lookup === 'function');
this.lookup = options.lookup;
}
if (options.noSig0 != null) {
assert(typeof options.noSig0 === 'boolean');
this.noSig0 = options.noSig0;
}
if (options.publicHost != null) {
assert(typeof options.publicHost === 'string');
this.publicHost = IP.normalize(options.publicHost);
}
return this;
}
initNode() {
this.on('error', (err) => {
this.logger.error(err);
});
this.on('query', (req, res) => {
this.logMessage('\n\nDNS Request:', req);
this.logMessage('\n\nDNS Response:', res);
});
return this;
}
logMessage(prefix, msg) {
if (this.logger.level < 5)
return;
const logs = msg.toString().trim().split('\n');
this.logger.spam(prefix);
for (const log of logs)
this.logger.spam(log);
}
signSize() {
if (!this.sig0)
return 94;
return 0;
}
sign(msg, host, port) {
if (!this.noSig0)
return hsig.sign(msg, this.key);
return msg;
}
async lookupName(name) {
if (!this.lookup)
throw new Error('Tree not available.');
const hash = rules.hashName(name);
const data = await this.lookup(hash);
if (!data)
return null;
const ns = NameState.decode(data);
if (ns.data.length === 0)
return null;
return ns.data;
}
async response(req, rinfo) {
const [qs] = req.question;
const name = qs.name.toLowerCase();
const type = qs.type;
// Our root zone.
if (name === '.') {
const res = new Message();
res.aa = true;
switch (type) {
case types.ANY:
case types.NS:
res.answer.push(this.toNS());
key.signZSK(res.answer, types.NS);
if (IP.family(this.publicHost) === 4) {
res.additional.push(this.toA());
key.signZSK(res.additional, types.A);
} else {
res.additional.push(this.toAAAA());
key.signZSK(res.additional, types.AAAA);
}
break;
case types.SOA:
res.answer.push(this.toSOA());
key.signZSK(res.answer, types.SOA);
res.authority.push(this.toNS());
key.signZSK(res.authority, types.NS);
if (IP.family(this.publicHost) === 4) {
res.additional.push(this.toA());
key.signZSK(res.additional, types.A);
} else {
res.additional.push(this.toAAAA());
key.signZSK(res.additional, types.AAAA);
}
break;
case types.DNSKEY:
res.answer.push(key.ksk.deepClone());
res.answer.push(key.zsk.deepClone());
key.signKSK(res.answer, types.DNSKEY);
break;
default:
// Minimally covering NSEC proof:
res.authority.push(this.toNSEC());
key.signZSK(res.authority, types.NSEC);
res.authority.push(this.toSOA());
key.signZSK(res.authority, types.SOA);
break;
}
return res;
}
// Process the name.
const labels = util.split(name);
const tld = util.label(name, labels, -1);
// Handle reverse pointers.
if (tld === '_synth' && labels.length <= 2 && name[0] === '_') {
const res = new Message();
const rr = new Record();
res.aa = true;
rr.name = name;
rr.ttl = 21600;
// TLD '._synth' is being queried on its own, send SOA
// so recursive asks again with complete synth record.
if (labels.length === 1) {
// Empty non-terminal proof:
res.authority.push(
nsec.create(
'_synth.',
'\\000._synth.',
TYPE_MAP_EMPTY
)
);
key.signZSK(res.authority, types.NSEC);
res.authority.push(this.toSOA());
key.signZSK(res.authority, types.SOA);
return res;
}
const hash = util.label(name, labels, -2);
const ip = IP.map(base32.decodeHex(hash.substring(1)));
const synthType = IP.isIPv4(ip) ? types.A : types.AAAA;
// Query must be for the correct synth version
if (type !== synthType) {
// SYNTH4/6 proof:
const typeMap = synthType === types.A ? TYPE_MAP_A : TYPE_MAP_AAAA;
res.authority.push(nsec.create(name, '\\000.' + name, typeMap));
key.signZSK(res.authority, types.NSEC);
res.authority.push(this.toSOA());
key.signZSK(res.authority, types.SOA);
return res;
}
if (synthType === types.A) {
rr.type = types.A;
rr.data = new ARecord();
} else {
rr.type = types.AAAA;
rr.data = new AAAARecord();
}
rr.data.address = IP.toString(ip);
res.answer.push(rr);
key.signZSK(res.answer, rr.type);
return res;
}
// REFUSED for invalid names
// this simplifies NSEC proofs
// by avoiding octets like \000
// Also, this decreases load on
// the server since it avoids signing
// useless proofs for invalid TLDs
// (These requests are most
// likely bad anyways)
if (!rules.verifyName(tld)) {
const res = new Message();
res.code = codes.REFUSED;
return res;
}
// Ask the urkel tree for the name data.
const data = !this.blacklist.has(tld)
? (await this.lookupName(tld))
: null;
// Non-existent domain.
if (!data) {
const item = this.getReserved(tld);
// This name is in the existing root zone.
// Fall back to ICANN's servers if not yet
// registered on the handshake blockchain.
// This is an example of "Dynamic Fallback"
// as mentioned in the whitepaper.
if (item && item.root) {
const res = await this.icann.lookup(tld);
if (res.ad && res.code !== codes.NXDOMAIN) {
// answer must be a referral since lookup
// function always asks for NS
assert(res.code === codes.NOERROR);
assert(res.answer.length === 0);
assert(hasValidOwner(res.authority, tld));
res.ad = false;
res.question = [qs];
const secure = util.hasType(res.authority, types.DS);
// no DS referrals for TLDs
if (type === types.DS && labels.length === 1) {
const dsSet = util.extractSet(res.authority,
util.fqdn(tld), types.DS);
res.aa = true;
res.answer = dsSet;
key.signZSK(res.answer, types.DS);
res.authority = [];
res.additional = [];
if (res.answer.length === 0) {
res.authority.push(this.toSOA());
key.signZSK(res.authority, types.SOA);
}
}
// No DS we must add a minimally covering proof
if (!secure) {
// Replace any NSEC/NSEC3 records
const filterTypes = [types.NSEC, types.NSEC3];
res.authority = util.filterSet(res.authority, ...filterTypes);
const next = nsec.nextName(tld);
const rr = nsec.create(tld, next, TYPE_MAP_NS);
res.authority.push(rr);
key.signZSK(res.authority, types.NSEC);
} else {
key.signZSK(res.authority, types.DS);
}
return res;
}
}
const res = new Message();
res.code = codes.NXDOMAIN;
res.aa = true;
// Doesn't exist.
//
// We should be giving a real NSEC proof
// here, but I don't think it's possible
// with the current construction.
//
// I imagine this would only be possible
// if NSEC3 begins to support BLAKE2b for
// name hashing. Even then, it's still
// not possible for SPV nodes since they
// can't arbitrarily iterate over the tree.
//
// Instead, we give a minimally covering
// NSEC record based on rfc4470
// https://tools.ietf.org/html/rfc4470
// Proving the name doesn't exist
const prev = nsec.prevName(tld);
const next = nsec.nextName(tld);
const nameSet = [nsec.create(prev, next, TYPE_MAP_EMPTY)];
key.signZSK(nameSet, types.NSEC);
// Proving a wildcard doesn't exist
const wildcardSet = [nsec.create('!.', '+.', TYPE_MAP_EMPTY)];
key.signZSK(wildcardSet, types.NSEC);
res.authority = res.authority.concat(nameSet, wildcardSet);
res.authority.push(this.toSOA());
key.signZSK(res.authority, types.SOA);
return res;
}
// Our resolution.
const resource = Resource.decode(data);
const res = resource.toDNS(name, type);
if (res.answer.length === 0 && res.aa) {
res.authority.push(this.toSOA());
key.signZSK(res.authority, types.SOA);
}
return res;
}
async resolve(req, rinfo) {
const [qs] = req.question;
const {name, type} = qs;
const tld = util.from(name, -1);
// Plugins can insert middleware here and hijack the
// lookup for special TLDs before checking Urkel tree.
// We also pass the entire question in case a plugin
// is able to return an authoritative (non-referral) answer.
if (typeof this.middle === 'function') {
let res;
try {
res = await this.middle(tld, req, rinfo);
} catch (e) {
this.logger.warning(
'Root server middleware resolution failed for name: %s',
name
);
this.logger.debug(e.stack);
}
if (res) {
return res;
}
}
// Hit the cache first.
const cache = this.cache.get(name, type);
if (cache)
return cache;
const res = await this.response(req, rinfo);
if (!util.equal(tld, '_synth.'))
this.cache.set(name, type, res);
return res;
}
async open() {
await super.open(this.port, this.host);
this.logger.info('Root nameserver listening on port %d.', this.port);
}
getReserved(tld) {
return reserved.getByName(tld);
}
// Intended to be called by plugin.
signRRSet(rrset, type) {
key.signZSK(rrset, type);
}
resetCache() {
this.cache.reset();
}
serial() {
const date = new Date();
const y = date.getUTCFullYear() * 1e6;
const m = (date.getUTCMonth() + 1) * 1e4;
const d = date.getUTCDate() * 1e2;
const h = date.getUTCHours();
return y + m + d + h;
}
toSOA() {
const rr = new Record();
const rd = new SOARecord();
rr.name = '.';
rr.type = types.SOA;
rr.ttl = 86400;
rr.data = rd;
rd.ns = '.';
rd.mbox = '.';
rd.serial = this.serial();
rd.refresh = 1800;
rd.retry = 900;
rd.expire = 604800;
rd.minttl = DEFAULT_TTL;
return rr;
}
toNS() {
const rr = new Record();
const rd = new NSRecord();
rr.name = '.';
rr.type = types.NS;
rr.ttl = 518400;
rr.data = rd;
rd.ns = this.synth;
return rr;
}
// Glue only
toA() {
const rr = new Record();
const rd = new ARecord();
rr.name = this.synth;
rr.type = types.A;
rr.ttl = 518400;
rr.data = rd;
rd.address = this.publicHost;
return rr;
}
// Glue only
toAAAA() {
const rr = new Record();
const rd = new AAAARecord();
rr.name = this.synth;
rr.type = types.AAAA;
rr.ttl = 518400;
rr.data = rd;
rd.address = this.publicHost;
return rr;
}
toNSEC() {
const next = nsec.nextName('.');
return nsec.create('.', next, TYPE_MAP_ROOT);
}
}
/**
* RecursiveServer
* @extends {DNSServer}
*/
class RecursiveServer extends DNSServer {
constructor(options) {
super(RES_OPT);
this.ra = true;
this.edns = true;
this.dnssec = true;
this.noSig0 = false;
this.noAny = true;
this.logger = Logger.global;
this.key = secp256k1.privateKeyGenerate();
this.host = '127.0.0.1';
this.port = 5301;
this.stubHost = '127.0.0.1';
this.stubPort = 5300;
this.hns = new UnboundResolver({
inet6: false,
tcp: true,
edns: true,
dnssec: true,
minimize: true
});
if (options)
this.initOptions(options);
this.initNode();
this.hns.setStub(this.stubHost, this.stubPort, key.ds);
}
initOptions(options) {
assert(options);
this.parseOptions(options);
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger.context('rs');
}
if (options.key != null) {
assert(Buffer.isBuffer(options.key));
assert(options.key.length === 32);
this.key = options.key;
}
if (options.host != null) {
assert(typeof options.host === 'string');
this.host = IP.normalize(options.host);
}
if (options.port != null) {
assert((options.port & 0xffff) === options.port);
assert(options.port !== 0);
this.port = options.port;
}
if (options.stubHost != null) {
assert(typeof options.stubHost === 'string');
this.stubHost = IP.normalize(options.stubHost);
if (this.stubHost === '0.0.0.0' || this.stubHost === '::')
this.stubHost = '127.0.0.1';
}
if (options.stubPort != null) {
assert((options.stubPort & 0xffff) === options.stubPort);
assert(options.stubPort !== 0);
this.stubPort = options.stubPort;
}
if (options.noSig0 != null) {
assert(typeof options.noSig0 === 'boolean');
this.noSig0 = options.noSig0;
}
if (options.noUnbound != null) {
assert(typeof options.noUnbound === 'boolean');
if (options.noUnbound) {
this.hns = new RecursiveResolver({
inet6: false,
tcp: true,
edns: true,
dnssec: true,
minimize: true
});
}
}
return this;
}
initNode() {
this.hns.on('log', (...args) => {
this.logger.debug(...args);
});
this.on('error', (err) => {
this.logger.error(err);
});
this.on('query', (req, res) => {
this.logMessage('\n\nDNS Request:', req);
this.logMessage('\n\nDNS Response:', res);
});
return this;
}
logMessage(prefix, msg) {
if (this.logger.level < 5)
return;
const logs = msg.toString().trim().split('\n');
this.logger.spam(prefix);
for (const log of logs)
this.logger.spam(log);
}
signSize() {
if (!this.noSig0)
return 94;
return 0;
}
sign(msg, host, port) {
if (!this.noSig0)
return hsig.sign(msg, this.key);
return msg;
}
async open(...args) {
await this.hns.open();
await super.open(this.port, this.host);
this.logger.info('Recursive server listening on port %d.', this.port);
}
async close() {
await super.close();
await this.hns.close();
}
async resolve(req, rinfo) {
const [qs] = req.question;
return this.hns.resolve(qs);
}
async lookup(name, type) {
return this.hns.lookup(name, type);
}
}
/*
* Helpers
*/
function toKey(name, type) {
const labels = util.split(name);
const label = util.from(name, labels, -1);
// Ignore type if we're a referral.
if (labels.length > 1)
return label.toLowerCase();
let key = '';
key += label.toLowerCase();
key += ';';
key += type.toString(10);
return key;
}
function hasValidOwner(section, owner) {
owner = util.fqdn(owner);
for (const rr of section) {
if (rr.type === types.NS)
continue;
if (!util.equal(rr.name, owner))
return false;
}
return true;
}
/*
* Expose
*/
exports.RootServer = RootServer;
exports.RecursiveServer = RecursiveServer;

View file

@ -0,0 +1,592 @@
/*!
* abstractblock.js - abstract block object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const BLAKE2b = require('bcrypto/lib/blake2b');
const SHA3 = require('bcrypto/lib/sha3');
const bio = require('bufio');
const InvItem = require('./invitem');
const consensus = require('../protocol/consensus');
const util = require('../utils/util');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/**
* Abstract Block
* The class which all block-like objects inherit from.
* @alias module:primitives.AbstractBlock
* @abstract
* @property {Number} version
* @property {Hash} prevBlock
* @property {Hash} merkleRoot
* @property {Number} time
* @property {Number} bits
* @property {Number} nonce
*/
class AbstractBlock extends bio.Struct {
/**
* Create an abstract block.
* @constructor
*/
constructor() {
super();
this.version = 0;
this.prevBlock = consensus.ZERO_HASH;
this.merkleRoot = consensus.ZERO_HASH;
this.witnessRoot = consensus.ZERO_HASH;
this.treeRoot = consensus.ZERO_HASH;
this.reservedRoot = consensus.ZERO_HASH;
this.time = 0;
this.bits = 0;
this.nonce = 0;
this.extraNonce = consensus.ZERO_NONCE;
this.mask = consensus.ZERO_HASH;
this.mutable = false;
/** @type {Buffer?} */
this._hash = null;
/** @type {Buffer?} */
this._maskHash = null;
}
/**
* Inject properties from options object.
* @param {Object} options
*/
parseOptions(options) {
assert(options, 'Block data is required.');
assert((options.version >>> 0) === options.version);
assert(Buffer.isBuffer(options.prevBlock));
assert(Buffer.isBuffer(options.merkleRoot));
assert(Buffer.isBuffer(options.witnessRoot));
assert(Buffer.isBuffer(options.treeRoot));
assert(Buffer.isBuffer(options.reservedRoot));
assert(util.isU64(options.time));
assert((options.bits >>> 0) === options.bits);
assert((options.nonce >>> 0) === options.nonce);
assert(Buffer.isBuffer(options.extraNonce)
&& options.extraNonce.length === consensus.NONCE_SIZE);
assert(Buffer.isBuffer(options.mask));
this.version = options.version;
this.prevBlock = options.prevBlock;
this.merkleRoot = options.merkleRoot;
this.witnessRoot = options.witnessRoot;
this.treeRoot = options.treeRoot;
this.reservedRoot = options.reservedRoot;
this.time = options.time;
this.bits = options.bits;
this.nonce = options.nonce;
this.extraNonce = options.extraNonce;
this.mask = options.mask;
if (options.mutable != null) {
assert(typeof options.mutable === 'boolean');
this.mutable = options.mutable;
}
return this;
}
/**
* Inject properties from json object.
* @param {Object} json
*/
parseJSON(json) {
assert(json, 'Block data is required.');
assert((json.version >>> 0) === json.version);
assert((json.time >>> 0) === json.time);
assert((json.bits >>> 0) === json.bits);
assert((json.nonce >>> 0) === json.nonce);
this.version = json.version;
this.prevBlock = util.parseHex(json.prevBlock, 32);
this.merkleRoot = util.parseHex(json.merkleRoot, 32);
this.witnessRoot = util.parseHex(json.witnessRoot, 32);
this.treeRoot = util.parseHex(json.treeRoot, 32);
this.reservedRoot = util.parseHex(json.reservedRoot, 32);
this.time = json.time;
this.bits = json.bits;
this.nonce = json.nonce;
this.extraNonce = util.parseHex(json.extraNonce, consensus.NONCE_SIZE);
this.mask = util.parseHex(json.mask, 32);
return this;
}
/**
* Test whether the block is a memblock.
* @returns {Boolean}
*/
isMemory() {
return false;
}
/**
* Clear any cached values (abstract).
*/
_refresh() {
this._hash = null;
this._maskHash = null;
return this;
}
/**
* Clear any cached values.
*/
refresh() {
return this._refresh();
}
/**
* Hash the block header.
* @returns {Hash}
*/
hash() {
if (this.mutable)
return this.powHash();
if (!this._hash)
this._hash = this.powHash();
return this._hash;
}
/**
* Hash the block header.
* @returns {String}
*/
hashHex() {
return this.hash().toString('hex');
}
/**
* Get header size.
* @returns {Number}
*/
sizeHead() {
return consensus.HEADER_SIZE;
}
/**
* Serialize the block headers.
* @returns {Buffer}
*/
toHead() {
const size = this.sizeHead();
return this.writeHead(bio.write(size)).render();
}
/**
* Inject properties from serialized data.
* @param {Buffer} data
* @returns {this}
*/
fromHead(data) {
return this.readHead(bio.read(data));
}
/**
* Retrieve deterministically random padding.
* @param {Number} size
* @returns {Buffer}
*/
padding(size) {
assert((size >>> 0) === size);
const pad = Buffer.alloc(size);
for (let i = 0; i < size; i++)
pad[i] = this.prevBlock[i % 32] ^ this.treeRoot[i % 32];
return pad;
}
/**
* Serialize subheader for proof.
* @returns {Buffer}
*/
toSubhead() {
const bw = bio.write(128);
// The subheader contains miner-mutable
// and less essential data (that is,
// less essential for SPV resolvers).
bw.writeBytes(this.extraNonce);
bw.writeHash(this.reservedRoot);
bw.writeHash(this.witnessRoot);
bw.writeHash(this.merkleRoot);
bw.writeU32(this.version);
bw.writeU32(this.bits);
// Exactly one blake2b block (128 bytes).
assert(bw.offset === BLAKE2b.blockSize);
return bw.render();
}
/**
* Compute subheader hash.
* @returns {Buffer}
*/
subHash() {
return BLAKE2b.digest(this.toSubhead());
}
/**
* Compute xor bytes hash.
* @returns {Buffer}
*/
maskHash() {
if (this._maskHash != null)
return this._maskHash;
// Hash with previous block in case a pool wants
// to re-use the same mask for the next block!
return BLAKE2b.multi(this.prevBlock, this.mask);
}
/**
* Compute commitment hash.
* @returns {Buffer}
*/
commitHash() {
// Note for mining pools: do not send
// the mask itself to individual miners.
return BLAKE2b.multi(this.subHash(), this.maskHash());
}
/**
* Serialize preheader.
* @returns {Buffer}
*/
toPrehead() {
const bw = bio.write(128);
// The preheader contains only the truly
// essential data. This optimizes for
// SPV resolvers, who may only need
// access to the tree root as well as
// the ability to validate the PoW.
//
// Note that we don't consider the
// target commitment "essential" as
// the pow can still be validated
// contextually without it.
//
// Furthermore, the preheader does not
// contain any miner malleable data
// aside from the timestamp and nonce.
//
// Any malleable data is contained
// within the commitment hash. Miners
// are penalized for updating this
// data, as it will cost them two
// rounds of hashing.
//
// We could drop the padding here and
// just use a 20 byte blake2 hash for
// the xor bytes (which seems much
// cleaner), but this is insecure due
// to the following attack:
// todo - explain attack.
//
// The position of the nonce and
// timestamp intentionally provide
// incentives to keep the timestamp
// up-to-date.
//
// The first 8 bytes of this block
// of data can be treated as a uint64
// and incremented as such. If more
// than a second has passed since
// the last timestamp update, a miner
// can simply let the nonce overflow
// into the timestamp.
bw.writeU32(this.nonce);
bw.writeU64(this.time);
bw.writeBytes(this.padding(20));
bw.writeHash(this.prevBlock);
bw.writeHash(this.treeRoot);
bw.writeHash(this.commitHash());
// Exactly one blake2b block (128 bytes).
assert(bw.offset === BLAKE2b.blockSize);
return bw.render();
}
/**
* Calculate share hash.
* @returns {Hash}
*/
shareHash() {
const data = this.toPrehead();
// 128 bytes (output as BLAKE2b-512).
const left = BLAKE2b.digest(data, 64);
// 128 + 8 = 136 bytes.
const right = SHA3.multi(data, this.padding(8));
// 64 + 32 + 32 = 128 bytes.
return BLAKE2b.multi(left, this.padding(32), right);
}
/**
* Calculate PoW hash.
* @returns {Buffer}
*/
powHash() {
const hash = this.shareHash();
// XOR the PoW hash with arbitrary bytes.
// This can optionally be used by mining
// pools to mitigate block withholding
// attacks. Idea from Kevin Pan:
//
// https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2017-October/015163.html
//
// The goal here is to allow a pool to
// deny individual miners the ability
// to recognize whether they have found
// a block, but still allow them to
// recognize a share.
//
// Example:
//
// Network target:
// 00000000 00000000 10000000 ...
//
// Share target:
// 00000000 10000000 00000000 ...
//
// Mask:
// 00000000 01010101 10000000 ...
//
// The mask bytes are hidden from the
// individual miner, but known to the
// pool, and precommitted to in the
// block header (i.e. hashed).
//
// Following our example further:
//
// Miner share:
// 00000000 01010101 00000000 ...
//
// PoW hash (after XOR):
// 00000000 00000000 10000000 ...
//
// At this point, the miner has found
// a block, but this is unknown to
// him or her as they do not have
// access to the mask bytes directly.
for (let i = 0; i < 32; i++)
hash[i] ^= this.mask[i];
return hash;
}
/**
* Serialize the block headers.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
writeHead(bw) {
// Preheader.
bw.writeU32(this.nonce);
bw.writeU64(this.time);
bw.writeHash(this.prevBlock);
bw.writeHash(this.treeRoot);
// Subheader.
bw.writeBytes(this.extraNonce);
bw.writeHash(this.reservedRoot);
bw.writeHash(this.witnessRoot);
bw.writeHash(this.merkleRoot);
bw.writeU32(this.version);
bw.writeU32(this.bits);
// Mask.
bw.writeBytes(this.mask);
return bw;
}
/**
* Parse the block headers.
* @param {bio.BufferReader} br
*/
readHead(br) {
// Preheader.
this.nonce = br.readU32();
this.time = br.readU64();
this.prevBlock = br.readHash();
this.treeRoot = br.readHash();
// Subheader.
this.extraNonce = br.readBytes(consensus.NONCE_SIZE);
this.reservedRoot = br.readHash();
this.witnessRoot = br.readHash();
this.merkleRoot = br.readHash();
this.version = br.readU32();
this.bits = br.readU32();
// Mask.
this.mask = br.readBytes(32);
return this;
}
/**
* Encode to miner serialization.
* @returns {Buffer}
*/
toMiner() {
const bw = bio.write(128 + 128);
// Preheader.
bw.writeU32(this.nonce);
bw.writeU64(this.time);
bw.writeBytes(this.padding(20));
bw.writeHash(this.prevBlock);
bw.writeHash(this.treeRoot);
// Replace commitment hash with mask hash.
bw.writeHash(this.maskHash());
// Subheader.
bw.writeBytes(this.extraNonce);
bw.writeHash(this.reservedRoot);
bw.writeHash(this.witnessRoot);
bw.writeHash(this.merkleRoot);
bw.writeU32(this.version);
bw.writeU32(this.bits);
return bw.render();
}
/**
* Decode from miner serialization.
* @param {Buffer} data
*/
fromMiner(data) {
const br = bio.read(data);
// Preheader.
this.nonce = br.readU32();
this.time = br.readU64();
const padding = br.readBytes(20);
this.prevBlock = br.readHash();
this.treeRoot = br.readHash();
assert(padding.equals(this.padding(20)));
// Note: mask _hash_.
this._maskHash = br.readHash();
// Subheader.
this.extraNonce = br.readBytes(consensus.NONCE_SIZE);
this.reservedRoot = br.readHash();
this.witnessRoot = br.readHash();
this.merkleRoot = br.readHash();
this.version = br.readU32();
this.bits = br.readU32();
// Mask (unknown).
this.mask = Buffer.alloc(32, 0x00);
return this;
}
/**
* Verify the block.
* @returns {Boolean}
*/
verify() {
if (!this.verifyPOW())
return false;
if (!this.verifyBody())
return false;
return true;
}
/**
* Verify proof-of-work.
* @returns {Boolean}
*/
verifyPOW() {
return consensus.verifyPOW(this.hash(), this.bits);
}
/**
* Verify the block.
* @returns {Boolean}
*/
verifyBody() {
throw new Error('Abstract method.');
}
/**
* Convert the block to an inv item.
* @returns {InvItem}
*/
toInv() {
return new InvItem(InvItem.types.BLOCK, this.hash());
}
/**
* Decode from miner serialization.
* @param {Buffer} data
*/
static fromMiner(data) {
return new this().fromMiner(data);
}
}
/*
* Expose
*/
module.exports = AbstractBlock;

View file

@ -0,0 +1,566 @@
/*!
* address.js - address object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const bech32 = require('bcrypto/lib/encoding/bech32');
const blake2b = require('bcrypto/lib/blake2b');
const sha3 = require('bcrypto/lib/sha3');
const Network = require('../protocol/network');
const consensus = require('../protocol/consensus');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../script/script')} Script */
/** @typedef {import('../script/witness')} Witness */
/*
* Constants
*/
const ZERO_HASH160 = Buffer.alloc(20, 0x00);
/**
* @typedef {Object} AddressOptions
* @property {Hash} hash
* @property {Number} version
*/
/**
* Address
* Represents an address.
* @alias module:primitives.Address
* @property {Number} version
* @property {Buffer} hash
*/
class Address extends bio.Struct {
/**
* Create an address.
* @constructor
* @param {AddressOptions|String} [options]
* @param {(NetworkType|Network)?} [network]
*/
constructor(options, network) {
super();
this.version = 0;
this.hash = ZERO_HASH160;
if (options)
this.fromOptions(options, network);
}
/**
* Inject properties from options object.
* @param {AddressOptions|String} options
* @param {(NetworkType|Network)?} [network]
*/
fromOptions(options, network) {
if (typeof options === 'string')
return this.fromString(options, network);
assert(options);
const {hash, version} = options;
return this.fromHash(hash, version);
}
/**
* Count the sigops in a script, taking into account witness programs.
* @param {Witness} witness
* @returns {Number} sigop count
*/
getSigops(witness) {
if (this.version === 0) {
if (this.hash.length === 20)
return 1;
if (this.hash.length === 32 && witness.items.length > 0) {
const redeem = witness.getRedeem();
return redeem.getSigops();
}
}
return 0;
}
/**
* Get the address hash.
* @returns {Hash}
*/
getHash() {
return this.hash;
}
/**
* Test whether the address is null.
* @returns {Boolean}
*/
isNull() {
if (this.hash.length === 20)
return this.hash.equals(ZERO_HASH160);
if (this.hash.length === 32)
return this.hash.equals(consensus.ZERO_HASH);
for (let i = 0; i < this.hash.length; i++) {
if (this.hash[i] !== 0)
return false;
}
return true;
}
/**
* Test whether the address is unspendable.
* @returns {Boolean}
*/
isUnspendable() {
return this.isNulldata();
}
/**
* Test equality against another address.
* @param {Address} addr
* @returns {Boolean}
*/
equals(addr) {
assert(addr instanceof Address);
return this.version === addr.version
&& this.hash.equals(addr.hash);
}
/**
* Compare against another address.
* @param {Address} addr
* @returns {Number}
*/
compare(addr) {
assert(addr instanceof Address);
const cmp = this.version - addr.version;
if (cmp !== 0)
return cmp;
return this.hash.compare(addr.hash);
}
/**
* Inject properties from another address.
* @param {Address} addr
* @returns {this}
*/
inject(addr) {
this.version = addr.version;
this.hash = addr.hash;
return this;
}
/**
* Clone address.
* @returns {this}
*/
clone() {
// @ts-ignore
return new this.constructor().inject(this);
}
/**
* Compile the address object to a bech32 address.
* @param {(NetworkType|Network)?} [network]
* @returns {String}
* @throws Error on bad hash/prefix.
*/
toString(network) {
const version = this.version;
const hash = this.hash;
assert(version <= 31);
assert(hash.length >= 2 && hash.length <= 40);
network = Network.get(network);
const hrp = network.addressPrefix;
return bech32.encode(hrp, version, hash);
}
/**
* Instantiate address from pubkey.
* @param {Buffer} key
* @returns {Address}
*/
fromPubkey(key) {
assert(Buffer.isBuffer(key) && key.length === 33);
return this.fromHash(blake2b.digest(key, 20), 0);
}
/**
* Instantiate address from script.
* @param {Script} script
* @returns {Address}
*/
fromScript(script) {
assert(script && typeof script.encode === 'function');
return this.fromHash(sha3.digest(script.encode()), 0);
}
/**
* Inject properties from bech32 address.
* @param {String} data
* @param {(NetworkType|Network)?} [network]
* @throws Parse error
*/
fromString(data, network) {
assert(typeof data === 'string');
const [hrp, version, hash] = bech32.decode(data);
Network.fromAddress(hrp, network);
return this.fromHash(hash, version);
}
/**
* Inject properties from witness.
* @param {Witness} witness
* @returns {Address|null}
*/
fromWitness(witness) {
const [, pk] = witness.getPubkeyhashInput();
if (pk) {
this.hash = blake2b.digest(pk, 20);
this.version = 0;
return this;
}
const redeem = witness.getScripthashInput();
if (redeem) {
this.hash = sha3.digest(redeem);
this.version = 0;
return this;
}
return null;
}
/**
* Inject properties from a hash.
* @param {Hash} hash
* @param {Number} [version=0]
* @throws on bad hash size
*/
fromHash(hash, version) {
if (version == null)
version = 0;
assert(Buffer.isBuffer(hash));
assert((version & 0xff) === version);
assert(version >= 0 && version <= 31, 'Bad program version.');
assert(hash.length >= 2 && hash.length <= 40, 'Hash is the wrong size.');
if (version === 0) {
assert(hash.length === 20 || hash.length === 32,
'Witness program hash is the wrong size.');
}
this.hash = hash;
this.version = version;
return this;
}
/**
* Inject properties from witness pubkeyhash.
* @param {Buffer} hash
* @returns {Address}
*/
fromPubkeyhash(hash) {
assert(hash && hash.length === 20, 'P2WPKH must be 20 bytes.');
return this.fromHash(hash, 0);
}
/**
* Inject properties from witness scripthash.
* @param {Buffer} hash
* @returns {Address}
*/
fromScripthash(hash) {
assert(hash && hash.length === 32, 'P2WSH must be 32 bytes.');
return this.fromHash(hash, 0);
}
/**
* Inject properties from witness program.
* @param {Number} version
* @param {Buffer} hash
* @returns {Address}
*/
fromProgram(version, hash) {
assert(version >= 0, 'Bad version for witness program.');
return this.fromHash(hash, version);
}
/**
* Instantiate address from nulldata.
* @param {Buffer} data
* @returns {Address}
*/
fromNulldata(data) {
return this.fromHash(data, 31);
}
/**
* Test whether the address is witness pubkeyhash.
* @returns {Boolean}
*/
isPubkeyhash() {
return this.version === 0 && this.hash.length === 20;
}
/**
* Test whether the address is witness scripthash.
* @returns {Boolean}
*/
isScripthash() {
return this.version === 0 && this.hash.length === 32;
}
/**
* Test whether the address is unspendable.
* @returns {Boolean}
*/
isNulldata() {
return this.version === 31;
}
/**
* Test whether the address is an unknown witness program.
* @returns {Boolean}
*/
isUnknown() {
switch (this.version) {
case 0:
return this.hash.length !== 20 && this.hash.length !== 32;
case 31:
return false;
}
return true;
}
/**
* Test address validity.
* @returns {Boolean}
*/
isValid() {
assert(this.version >= 0);
if (this.version > 31)
return false;
if (this.hash.length < 2 || this.hash.length > 40)
return false;
return true;
}
/**
* Calculate address size.
* @returns {Number}
*/
getSize() {
return 1 + 1 + this.hash.length;
}
/**
* Write address to buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU8(this.version);
bw.writeU8(this.hash.length);
bw.writeBytes(this.hash);
return bw;
}
/**
* Read address from buffer reader.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
const version = br.readU8();
assert(version <= 31);
const size = br.readU8();
assert(size >= 2 && size <= 40);
const hash = br.readBytes(size);
return this.fromHash(hash, version);
}
/**
* Inspect the Address.
* @returns {String}
*/
format() {
return '<Address:'
+ ` version=${this.version}`
+ ` str=${this.toString()}`
+ '>';
}
/**
* Instantiate address from pubkey.
* @param {Buffer} key
* @returns {Address}
*/
static fromPubkey(key) {
return new this().fromPubkey(key);
}
/**
* Instantiate address from script.
* @param {Script} script
* @returns {Address}
*/
static fromScript(script) {
return new this().fromScript(script);
}
/**
* Create an Address from a witness.
* Attempt to extract address
* properties from a witness.
* @param {Witness} witness
* @returns {Address|null}
*/
static fromWitness(witness) {
return new this().fromWitness(witness);
}
/**
* Create a naked address from hash/version.
* @param {Hash} hash
* @param {Number} [version=0]
* @returns {Address}
* @throws on bad hash size
*/
static fromHash(hash, version) {
return new this().fromHash(hash, version);
}
/**
* Instantiate address from witness pubkeyhash.
* @param {Buffer} hash
* @returns {Address}
*/
static fromPubkeyhash(hash) {
return new this().fromPubkeyhash(hash);
}
/**
* Instantiate address from witness scripthash.
* @param {Buffer} hash
* @returns {Address}
*/
static fromScripthash(hash) {
return new this().fromScripthash(hash);
}
/**
* Instantiate address from witness program.
* @param {Number} version
* @param {Buffer} hash
* @returns {Address}
*/
static fromProgram(version, hash) {
return new this().fromProgram(version, hash);
}
/**
* Instantiate address from nulldata.
* @param {Buffer} data
* @returns {Address}
*/
static fromNulldata(data) {
return new this().fromNulldata(data);
}
/**
* Get the hash of a base58 address or address-related object.
* @param {Address|Hash} data
* @returns {Hash}
*/
static getHash(data) {
if (!data)
throw new Error('Object is not an address.');
if (Buffer.isBuffer(data))
return data;
if (data instanceof Address)
return data.hash;
throw new Error('Object is not an address.');
}
}
/*
* Expose
*/
module.exports = Address;

View file

@ -0,0 +1,483 @@
/* eslint camelcase: 'off' */
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const base16 = require('bcrypto/lib/encoding/base16');
const bech32 = require('bcrypto/lib/encoding/bech32');
const BLAKE2b = require('bcrypto/lib/blake2b');
const SHA256 = require('bcrypto/lib/sha256');
const rsa = require('bcrypto/lib/rsa');
const p256 = require('bcrypto/lib/p256');
const ed25519 = require('bcrypto/lib/ed25519');
const {countLeft} = require('bcrypto/lib/encoding/util');
const Goo = require('goosig');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').Amount} AmountValue */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/*
* Goo
*/
const goo = new Goo(Goo.RSA2048, 2, 3);
/*
* Constants
*/
const keyTypes = {
RSA: 0,
GOO: 1,
P256: 2,
ED25519: 3,
ADDRESS: 4
};
const keyTypesByVal = {
[keyTypes.RSA]: 'RSA',
[keyTypes.GOO]: 'GOO',
[keyTypes.P256]: 'P256',
[keyTypes.ED25519]: 'ED25519',
[keyTypes.ADDRESS]: 'ADDRESS'
};
const EMPTY = Buffer.alloc(0);
/**
* AirdropKey
*/
class AirdropKey extends bio.Struct {
constructor() {
super();
this.type = keyTypes.RSA;
this.n = EMPTY;
this.e = EMPTY;
this.C1 = EMPTY;
this.point = EMPTY;
this.version = 0;
this.address = EMPTY;
this.value = 0;
this.sponsor = false;
this.nonce = SHA256.zero;
this.tweak = null;
}
/**
* @param {AirdropKey} key
* @returns {this}
*/
inject(key) {
assert(key instanceof AirdropKey);
this.type = key.type;
this.n = key.n;
this.e = key.e;
this.C1 = key.C1;
this.point = key.point;
this.version = key.version;
this.address = key.address;
this.value = key.value;
this.sponsor = key.sponsor;
this.nonce = key.nonce;
this.tweak = key.tweak;
return this;
}
isRSA() {
return this.type === keyTypes.RSA;
}
isGoo() {
return this.type === keyTypes.GOO;
}
isP256() {
return this.type === keyTypes.P256;
}
isED25519() {
return this.type === keyTypes.ED25519;
}
isAddress() {
return this.type === keyTypes.ADDRESS;
}
isWeak() {
if (!this.isRSA())
return false;
return countLeft(this.n) < 2048 - 7;
}
/**
* @returns {Boolean}
*/
validate() {
switch (this.type) {
case keyTypes.RSA: {
let key;
try {
key = rsa.publicKeyImport({ n: this.n, e: this.e });
} catch (e) {
return false;
}
const bits = rsa.publicKeyBits(key);
// Allow 1024 bit RSA for now.
// We can softfork out later.
return bits >= 1024 && bits <= 4096;
}
case keyTypes.GOO: {
return this.C1.length === goo.size;
}
case keyTypes.P256: {
return p256.publicKeyVerify(this.point);
}
case keyTypes.ED25519: {
return ed25519.publicKeyVerify(this.point);
}
case keyTypes.ADDRESS: {
return true;
}
default: {
throw new assert.AssertionError('Invalid key type.');
}
}
}
/**
* @param {Buffer} msg
* @param {Buffer} sig
* @returns {Boolean}
*/
verify(msg, sig) {
assert(Buffer.isBuffer(msg));
assert(Buffer.isBuffer(sig));
switch (this.type) {
case keyTypes.RSA: {
let key;
try {
key = rsa.publicKeyImport({ n: this.n, e: this.e });
} catch (e) {
return false;
}
return rsa.verify(SHA256, msg, sig, key);
}
case keyTypes.GOO: {
return goo.verify(msg, sig, this.C1);
}
case keyTypes.P256: {
return p256.verify(msg, sig, this.point);
}
case keyTypes.ED25519: {
return ed25519.verify(msg, sig, this.point);
}
case keyTypes.ADDRESS: {
return true;
}
default: {
throw new assert.AssertionError('Invalid key type.');
}
}
}
/**
* @returns {Hash}
*/
hash() {
const bw = bio.pool(this.getSize());
this.write(bw);
return BLAKE2b.digest(bw.render());
}
getSize() {
let size = 0;
size += 1;
switch (this.type) {
case keyTypes.RSA:
assert(this.n.length <= 0xffff);
assert(this.e.length <= 0xff);
size += 2;
size += this.n.length;
size += 1;
size += this.e.length;
size += 32;
break;
case keyTypes.GOO:
size += goo.size;
break;
case keyTypes.P256:
size += 33;
size += 32;
break;
case keyTypes.ED25519:
size += 32;
size += 32;
break;
case keyTypes.ADDRESS:
size += 1;
size += 1;
size += this.address.length;
size += 8;
size += 1;
break;
default:
throw new assert.AssertionError('Invalid key type.');
}
return size;
}
/**
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU8(this.type);
switch (this.type) {
case keyTypes.RSA:
bw.writeU16(this.n.length);
bw.writeBytes(this.n);
bw.writeU8(this.e.length);
bw.writeBytes(this.e);
bw.writeBytes(this.nonce);
break;
case keyTypes.GOO:
bw.writeBytes(this.C1);
break;
case keyTypes.P256:
case keyTypes.ED25519:
bw.writeBytes(this.point);
bw.writeBytes(this.nonce);
break;
case keyTypes.ADDRESS:
bw.writeU8(this.version);
bw.writeU8(this.address.length);
bw.writeBytes(this.address);
bw.writeU64(this.value);
bw.writeU8(this.sponsor ? 1 : 0);
break;
default:
throw new assert.AssertionError('Invalid key type.');
}
return bw;
}
/**
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.type = br.readU8();
switch (this.type) {
case keyTypes.RSA: {
this.n = br.readBytes(br.readU16());
this.e = br.readBytes(br.readU8());
this.nonce = br.readBytes(32);
break;
}
case keyTypes.GOO: {
this.C1 = br.readBytes(goo.size);
break;
}
case keyTypes.P256: {
this.point = br.readBytes(33);
this.nonce = br.readBytes(32);
break;
}
case keyTypes.ED25519: {
this.point = br.readBytes(32);
this.nonce = br.readBytes(32);
break;
}
case keyTypes.ADDRESS: {
this.version = br.readU8();
this.address = br.readBytes(br.readU8());
this.value = br.readU64();
this.sponsor = br.readU8() === 1;
break;
}
default: {
throw new Error('Unknown key type.');
}
}
return this;
}
/**
* @param {String} addr
* @param {AmountValue} value
* @param {Boolean} sponsor
* @returns {this}
*/
fromAddress(addr, value, sponsor = false) {
assert(typeof addr === 'string');
assert(Number.isSafeInteger(value) && value >= 0);
assert(typeof sponsor === 'boolean');
const [hrp, version, hash] = bech32.decode(addr);
assert(hrp === 'hs' || hrp === 'ts' || hrp === 'rs');
assert(version === 0);
assert(hash.length === 20 || hash.length === 32);
this.type = keyTypes.ADDRESS;
this.version = version;
this.address = hash;
this.value = value;
this.sponsor = sponsor;
return this;
}
getJSON() {
return {
type: keyTypesByVal[this.type] || 'UNKNOWN',
n: this.n.length > 0
? this.n.toString('hex')
: undefined,
e: this.e.length > 0
? this.e.toString('hex')
: undefined,
C1: this.C1.length > 0
? this.C1.toString('hex')
: undefined,
point: this.point.length > 0
? this.point.toString('hex')
: undefined,
version: this.address.length > 0
? this.version
: undefined,
address: this.address.length > 0
? this.address.toString('hex')
: undefined,
value: this.value || undefined,
sponsor: this.value
? this.sponsor
: undefined,
nonce: !this.isGoo() && !this.isAddress()
? this.nonce.toString('hex')
: undefined
};
}
/**
* @param {Object} json
* @returns {this}
*/
fromJSON(json) {
assert(json && typeof json === 'object');
assert(typeof json.type === 'string');
assert(Object.prototype.hasOwnProperty.call(keyTypes, json.type));
this.type = keyTypes[json.type];
console.log(base16.decode.toString());
switch (this.type) {
case keyTypes.RSA: {
this.n = base16.decode(json.n);
this.e = base16.decode(json.e);
this.nonce = base16.decode(json.nonce);
break;
}
case keyTypes.GOO: {
this.C1 = base16.decode(json.C1);
break;
}
case keyTypes.P256: {
this.point = base16.decode(json.point);
this.nonce = base16.decode(json.nonce);
break;
}
case keyTypes.ED25519: {
this.point = base16.decode(json.point);
this.nonce = base16.decode(json.nonce);
break;
}
case keyTypes.ADDRESS: {
assert((json.version & 0xff) === json.version);
assert(Number.isSafeInteger(json.value) && json.value >= 0);
assert(typeof json.sponsor === 'boolean');
this.version = json.version;
this.address = base16.decode(json.address);
this.value = json.value;
this.sponsor = json.sponsor;
break;
}
default: {
throw new Error('Unknown key type.');
}
}
return this;
}
/**
* @param {String} addr
* @param {AmountValue} value
* @param {Boolean} sponsor
* @returns {AirdropKey}
*/
static fromAddress(addr, value, sponsor) {
return new this().fromAddress(addr, value, sponsor);
}
}
/*
* Static
*/
AirdropKey.keyTypes = keyTypes;
AirdropKey.keyTypesByVal = keyTypesByVal;
/*
* Expose
*/
module.exports = AirdropKey;

View file

@ -0,0 +1,520 @@
'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;

550
docs/js-primitives/block.js Normal file
View file

@ -0,0 +1,550 @@
/*!
* block.js - block object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const {BufferSet} = require('buffer-map');
const blake2b = require('bcrypto/lib/blake2b');
const merkle = require('bcrypto/lib/mrkl');
const consensus = require('../protocol/consensus');
const AbstractBlock = require('./abstractblock');
const TX = require('./tx');
const MerkleBlock = require('./merkleblock');
const Headers = require('./headers');
const Network = require('../protocol/network');
const util = require('../utils/util');
const {encoding} = bio;
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').Amount} AmountValue */
/** @typedef {import('../types').RawBlock} RawBlock */
/** @typedef {import('../coins/coinview')} CoinView */
/**
* Block
* Represents a full block.
* @alias module:primitives.Block
* @extends AbstractBlock
*/
class Block extends AbstractBlock {
/**
* Create a block.
* @constructor
* @param {Object} [options]
*/
constructor(options) {
super();
/** @type {TX[]} */
this.txs = [];
/** @type {Buffer?} */
this._raw = null;
/** @type {Sizes?} */
this._sizes = null;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
this.parseOptions(options);
if (options.txs) {
assert(Array.isArray(options.txs));
for (const tx of options.txs) {
assert(tx instanceof TX);
this.txs.push(tx);
}
}
return this;
}
/**
* Clear any cached values.
* @param {Boolean?} [all] - Clear transactions.
* @returns {this}
*/
refresh(all) {
this._refresh();
this._raw = null;
this._sizes = null;
if (!all)
return this;
for (const tx of this.txs)
tx.refresh();
return this;
}
/**
* Calculate virtual block size.
* @returns {Number} Virtual size.
*/
getVirtualSize() {
const scale = consensus.WITNESS_SCALE_FACTOR;
return (this.getWeight() + scale - 1) / scale | 0;
}
/**
* Calculate block weight.
* @returns {Number} weight
*/
getWeight() {
const {base, witness} = this.getSizes();
const total = base + witness;
return base * (consensus.WITNESS_SCALE_FACTOR - 1) + total;
}
/**
* Get real block size.
* @returns {Number} size
*/
getSize() {
const {base, witness} = this.getSizes();
return base + witness;
}
/**
* Get base block size (without witness).
* @returns {Number} size
*/
getBaseSize() {
const {base} = this.getSizes();
return base;
}
/**
* Test whether the block contains a
* transaction with a non-empty witness.
* @returns {Boolean}
*/
hasWitness() {
for (const tx of this.txs) {
if (tx.hasWitness())
return true;
}
return false;
}
/**
* Test the block's transaction vector against a hash.
* @param {Hash} hash
* @returns {Boolean}
*/
hasTX(hash) {
return this.indexOf(hash) !== -1;
}
/**
* Find the index of a transaction in the block.
* @param {Hash} hash
* @returns {Number} index (-1 if not present).
*/
indexOf(hash) {
for (let i = 0; i < this.txs.length; i++) {
const tx = this.txs[i];
if (tx.hash().equals(hash))
return i;
}
return -1;
}
/**
* Calculate merkle root.
* @returns {Hash}
*/
createMerkleRoot() {
const leaves = [];
for (const tx of this.txs)
leaves.push(tx.hash());
return merkle.createRoot(blake2b, leaves);
}
/**
* Calculate witness root.
* @returns {Hash}
*/
createWitnessRoot() {
const leaves = [];
for (const tx of this.txs)
leaves.push(tx.witnessHash());
return merkle.createRoot(blake2b, leaves);
}
/**
* Retrieve the merkle root from the block header.
* @returns {Hash}
*/
getMerkleRoot() {
return this.merkleRoot;
}
/**
* Do non-contextual verification on the block. Including checking the block
* size, the coinbase and the merkle root. This is consensus-critical.
* @returns {Boolean}
*/
verifyBody() {
const [valid] = this.checkBody();
return valid;
}
/**
* Do non-contextual verification on the block. Including checking the block
* size, the coinbase and the merkle root. This is consensus-critical.
* @returns {Array} [valid, reason, score]
*/
checkBody() {
// Check base size.
if (this.txs.length === 0
|| this.txs.length > consensus.MAX_BLOCK_SIZE
|| this.getBaseSize() > consensus.MAX_BLOCK_SIZE) {
return [false, 'bad-blk-length', 100];
}
// Check block weight.
if (this.getWeight() > consensus.MAX_BLOCK_WEIGHT)
return [false, 'bad-blk-weight', 100];
// Check merkle root.
const merkleRoot = this.createMerkleRoot();
if (merkleRoot.equals(consensus.ZERO_HASH))
return [false, 'bad-txnmrklroot', 100];
if (!merkleRoot.equals(this.merkleRoot))
return [false, 'bad-txnmrklroot', 100];
// Check witness root.
const witnessRoot = this.createWitnessRoot();
if (!witnessRoot.equals(this.witnessRoot))
return [false, 'bad-witnessroot', 100];
// First TX must be a coinbase.
if (this.txs.length === 0 || !this.txs[0].isCoinbase())
return [false, 'bad-cb-missing', 100];
// Test all transactions.
for (let i = 0; i < this.txs.length; i++) {
const tx = this.txs[i];
// The rest of the txs must not be coinbases.
if (i > 0 && tx.isCoinbase())
return [false, 'bad-cb-multiple', 100];
// Sanity checks.
const [valid, reason, score] = tx.checkSanity();
if (!valid)
return [valid, reason, score];
}
return [true, 'valid', 0];
}
/**
* Retrieve the coinbase height from the coinbase input script.
* @returns {Number} height (-1 if not present).
*/
getCoinbaseHeight() {
if (this.txs.length === 0)
return -1;
const cb = this.txs[0];
return cb.locktime;
}
/**
* Get the "claimed" reward by the coinbase.
* @returns {AmountValue} claimed
*/
getClaimed() {
assert(this.txs.length > 0);
assert(this.txs[0].isCoinbase());
return this.txs[0].getOutputValue();
}
/**
* Get all unique outpoint hashes in the
* block. Coinbases are ignored.
* @returns {Hash[]} Outpoint hashes.
*/
getPrevout() {
const prevout = new BufferSet();
for (let i = 1; i < this.txs.length; i++) {
const tx = this.txs[i];
for (const input of tx.inputs)
prevout.add(input.prevout.hash);
}
return prevout.toArray();
}
/**
* Inspect the block and return a more
* user-friendly representation of the data.
* @param {CoinView} [view]
* @param {Number} [height]
* @returns {Object}
*/
format(view, height) {
return {
hash: this.hash().toString('hex'),
height: height != null ? height : -1,
size: this.getSize(),
virtualSize: this.getVirtualSize(),
date: util.date(this.time),
version: this.version.toString(16),
prevBlock: this.prevBlock.toString('hex'),
merkleRoot: this.merkleRoot.toString('hex'),
witnessRoot: this.witnessRoot.toString('hex'),
treeRoot: this.treeRoot.toString('hex'),
reservedRoot: this.reservedRoot.toString('hex'),
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex'),
txs: this.txs.map((tx, i) => {
return tx.format(view, null, i);
})
};
}
/**
* Convert the block to an object suitable
* for JSON serialization.
* @param {Network} [network]
* @param {CoinView} [view]
* @param {Number} [height]
* @param {Number} [depth]
* @returns {Object}
*/
getJSON(network, view, height, depth) {
network = Network.get(network);
return {
hash: this.hash().toString('hex'),
height: height,
depth: depth,
version: this.version,
prevBlock: this.prevBlock.toString('hex'),
merkleRoot: this.merkleRoot.toString('hex'),
witnessRoot: this.witnessRoot.toString('hex'),
treeRoot: this.treeRoot.toString('hex'),
reservedRoot: this.reservedRoot.toString('hex'),
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex'),
txs: this.txs.map((tx, i) => {
return tx.getJSON(network, view, null, i);
})
};
}
/**
* Inject properties from json object.
* @param {Object} json
*/
fromJSON(json) {
assert(json, 'Block data is required.');
assert(Array.isArray(json.txs));
this.parseJSON(json);
for (const tx of json.txs)
this.txs.push(TX.fromJSON(tx));
return this;
}
/**
* Inject properties from serialized data.
* @param {bio.BufferReader} br
*/
read(br) {
br.start();
this.readHead(br);
const count = br.readVarint();
let witness = 0;
for (let i = 0; i < count; i++) {
const tx = TX.read(br);
witness += tx._sizes.witness;
this.txs.push(tx);
}
if (!this.mutable) {
const raw = br.endData();
const base = raw.length - witness;
this._raw = raw;
this._sizes = new Sizes(base, witness);
}
return this;
}
/**
* Convert the Block to a MerkleBlock.
* @param {BloomFilter} filter - Bloom filter for transactions
* to match. The merkle block will contain only the
* matched transactions.
* @returns {MerkleBlock}
*/
toMerkle(filter) {
return MerkleBlock.fromBlock(this, filter);
}
/**
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
if (this._raw) {
bw.writeBytes(this._raw);
return bw;
}
this.writeHead(bw);
bw.writeVarint(this.txs.length);
for (const tx of this.txs)
tx.write(bw);
return bw;
}
/**
* @returns {Buffer}
*/
encode() {
if (this.mutable)
return super.encode();
if (!this._raw)
this._raw = super.encode();
return this._raw;
}
/**
* Convert the block to a headers object.
* @returns {Headers}
*/
toHeaders() {
return Headers.fromBlock(this);
}
/**
* Get real block size with witness.
* @returns {Sizes}
*/
getSizes() {
if (this._sizes)
return this._sizes;
let base = 0;
let witness = 0;
base += this.sizeHead();
base += encoding.sizeVarint(this.txs.length);
for (const tx of this.txs) {
const sizes = tx.getSizes();
base += sizes.base;
witness += sizes.witness;
}
const sizes = new Sizes(base, witness);
if (!this.mutable)
this._sizes = sizes;
return sizes;
}
/**
* Test whether an object is a Block.
* @param {Object} obj
* @returns {Boolean}
*/
static isBlock(obj) {
return obj instanceof Block;
}
}
/*
* Helpers
*/
class Sizes {
constructor(base, witness) {
this.base = base;
this.witness = witness;
}
}
/*
* Expose
*/
module.exports = Block;

352
docs/js-primitives/claim.js Normal file
View file

@ -0,0 +1,352 @@
/*!
* claim.js - DNSSEC ownership proofs for hsd
* Copyright (c) 2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const blake2b = require('bcrypto/lib/blake2b');
const consensus = require('../protocol/consensus');
const policy = require('../protocol/policy');
const rules = require('../covenants/rules');
const Ownership = require('../covenants/ownership');
const InvItem = require('./invitem');
const TX = require('./tx');
const Input = require('./input');
const Output = require('./output');
const {OwnershipProof} = Ownership;
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').Amount} AmountValue */
/** @typedef {import('../types').Rate} Rate */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../protocol/network')} Network */
/*
* Constants
*/
const EMPTY = Buffer.alloc(0);
/**
* Claim
* @extends {bio.Struct}
*/
class Claim extends bio.Struct {
constructor() {
super();
this.blob = EMPTY;
/** @type {Hash?} */
this._hash = null;
this._data = null;
}
/**
* @returns {this}
*/
refresh() {
this._hash = null;
this._data = null;
return this;
}
/**
* @returns {Hash}
*/
hash() {
if (!this._hash)
this._hash = blake2b.digest(this.blob);
return this._hash;
}
/**
* @returns {String}
*/
hashHex() {
return this.hash().toString('hex');
}
/**
* @param {Network} network
* @returns {Object}
*/
getData(network) {
if (!this._data) {
const proof = this.getProof();
if (!proof)
return null;
const data = proof.getData(network);
if (!data)
return null;
this._data = data;
}
return this._data;
}
/**
* @returns {Number}
*/
getSize() {
return 2 + this.blob.length;
}
/**
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU16(this.blob.length);
bw.writeBytes(this.blob);
return bw;
}
/**
* @param {Buffer} data
* @returns {this}
*/
decode(data) {
const br = bio.read(data);
if (data.length > 2 + 10000)
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) {
const size = br.readU16();
if (size > 10000)
throw new Error('Invalid claim size.');
this.blob = br.readBytes(size);
return this;
}
/**
* @returns {InvItem}
*/
toInv() {
return new InvItem(InvItem.types.CLAIM, this.hash());
}
/**
* @returns {Number}
*/
getWeight() {
return this.getSize();
}
/**
* @returns {Number}
*/
getVirtualSize() {
const scale = consensus.WITNESS_SCALE_FACTOR;
return (this.getWeight() + scale - 1) / scale | 0;
}
/**
* @param {Number} [size]
* @param {Number} [rate]
* @returns {AmountValue}
*/
getMinFee(size, rate) {
if (size == null)
size = this.getVirtualSize();
return policy.getMinFee(size, rate);
}
/**
* @param {Network} [network]
* @returns {AmountValue}
*/
getFee(network) {
const data = this.getData(network);
assert(data);
return data.fee;
}
/**
* @param {Number} [size]
* @param {Network} [network]
* @returns {Rate}
*/
getRate(size, network) {
const fee = this.getFee(network);
if (size == null)
size = this.getVirtualSize();
return policy.getRate(size, fee);
}
/**
* @param {Network} network
* @param {Number} height
* @returns {TX}
*/
toTX(network, height) {
const data = this.getData(network);
assert(data);
const tx = new TX();
tx.inputs.push(new Input());
tx.outputs.push(new Output());
const input = new Input();
input.witness.items.push(this.blob);
const output = new Output();
output.value = data.value - data.fee;
output.address.version = data.version;
output.address.hash = data.hash;
let flags = 0;
if (data.weak)
flags |= 1;
output.covenant.setClaim(
rules.hashName(data.name),
height,
Buffer.from(data.name, 'binary'),
flags,
data.commitHash,
data.commitHeight
);
tx.inputs.push(input);
tx.outputs.push(output);
tx.refresh();
return tx;
}
/**
* @returns {OwnershipProof}
*/
getProof() {
try {
return this.toProof();
} catch (e) {
return new OwnershipProof();
}
}
/**
* @returns {OwnershipProof}
*/
toProof() {
return OwnershipProof.decode(this.blob);
}
/**
* @returns {Buffer}
*/
toBlob() {
return this.blob;
}
/**
* @returns {Object}
*/
getJSON() {
const proof = this.getProof();
return proof.toJSON();
}
/**
* Inject properties from blob.
* @param {Buffer} blob
* @returns {this}
*/
fromBlob(blob) {
assert(Buffer.isBuffer(blob));
this.blob = blob;
return this;
}
/**
* @param {OwnershipProof} proof
* @returns {this}
*/
fromProof(proof) {
assert(proof instanceof OwnershipProof);
this.blob = proof.encode();
return this;
}
/**
* Instantiate claim from raw proof.
* @param {Buffer} blob
* @returns {Claim}
*/
static fromBlob(blob) {
return new this().fromBlob(blob);
}
/**
* Instantiate claim from proof.
* @param {OwnershipProof} proof
* @returns {Claim}
*/
static fromProof(proof) {
return new this().fromProof(proof);
}
}
/*
* Expose
*/
module.exports = Claim;

362
docs/js-primitives/coin.js Normal file
View file

@ -0,0 +1,362 @@
/*!
* coin.js - coin object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const Amount = require('../ui/amount');
const Output = require('./output');
const Network = require('../protocol/network');
const consensus = require('../protocol/consensus');
const Outpoint = require('./outpoint');
const util = require('../utils/util');
/** @typedef {import('bufio').BufferReader} BufferReader */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../types').HexHash} HexHash */
/** @typedef {import('./tx')} TX */
/**
* Coin
* Represents an unspent output.
* @alias module:primitives.Coin
* @extends Output
* @property {Number} version
* @property {Number} height
* @property {Amount} value
* @property {Script} script
* @property {Boolean} coinbase
* @property {Hash} hash
* @property {Number} index
*/
class Coin extends Output {
/**
* Create a coin.
* @constructor
* @param {Object?} [options]
*/
constructor(options) {
super();
this.version = 1;
this.height = -1;
this.coinbase = false;
this.hash = consensus.ZERO_HASH;
this.index = 0;
if (options)
this.fromOptions(options);
}
/**
* Inject options into coin.
* @param {Object} [options]
*/
fromOptions(options) {
assert(options, 'Coin data is required.');
if (options.version != null) {
assert((options.version >>> 0) === options.version,
'Version must be a uint32.');
this.version = options.version;
}
if (options.height != null) {
if (options.height !== -1) {
assert((options.height >>> 0) === options.height,
'Height must be a uint32.');
this.height = options.height;
} else {
this.height = -1;
}
}
if (options.value != null) {
assert(Number.isSafeInteger(options.value) && options.value >= 0,
'Value must be a uint64.');
this.value = options.value;
}
if (options.address)
this.address.fromOptions(options.address);
if (options.covenant)
this.covenant.fromOptions(options.covenant);
if (options.coinbase != null) {
assert(typeof options.coinbase === 'boolean',
'Coinbase must be a boolean.');
this.coinbase = options.coinbase;
}
if (options.hash != null) {
assert(Buffer.isBuffer(options.hash));
this.hash = options.hash;
}
if (options.index != null) {
assert((options.index >>> 0) === options.index,
'Index must be a uint32.');
this.index = options.index;
}
return this;
}
/**
* Clone the coin.
* @returns {this}
*/
clone() {
assert(false, 'Coins are not cloneable.');
return this;
}
/**
* Calculate number of confirmations since coin was created.
* @param {Number} height - Current chain height. Network
* height is used if not passed in.
* @return {Number}
*/
getDepth(height) {
assert(typeof height === 'number', 'Must pass a height.');
if (this.height === -1)
return 0;
if (height === -1)
return 0;
if (height < this.height)
return 0;
return height - this.height + 1;
}
/**
* Serialize coin to a key
* suitable for a hash table.
* @returns {Buffer}
*/
toKey() {
return Outpoint.toKey(this.hash, this.index);
}
/**
* Inject properties from hash table key.
* @param {Buffer} key
* @returns {Coin}
*/
fromKey(key) {
const {hash, index} = Outpoint.fromKey(key);
this.hash = hash;
this.index = index;
return this;
}
/**
* Instantiate coin from hash table key.
* @param {Buffer} key
* @returns {Coin}
*/
static fromKey(key) {
return new this().fromKey(key);
}
/**
* Get little-endian hash.
* @returns {HexHash?}
*/
txid() {
if (!this.hash)
return null;
return this.hash.toString('hex');
}
/**
* Convert the coin to a more user-friendly object.
* @returns {Object}
*/
format() {
return {
version: this.version,
height: this.height,
value: Amount.coin(this.value),
address: this.address,
covenant: this.covenant.toJSON(),
coinbase: this.coinbase,
hash: this.txid(),
index: this.index
};
}
/**
* Convert the coin to an object suitable
* for JSON serialization.
* @param {Network} [network]
* @param {Boolean} [minimal]
* @returns {Object}
*/
getJSON(network, minimal) {
network = Network.get(network);
return {
version: this.version,
height: this.height,
value: this.value,
address: this.address.toString(network),
covenant: this.covenant.toJSON(),
coinbase: this.coinbase,
hash: !minimal ? this.txid() : undefined,
index: !minimal ? this.index : undefined
};
}
/**
* Inject JSON properties into coin.
* @param {Object} json
* @param {(NetworkType|Network)?} [network]
*/
fromJSON(json, network) {
assert(json, 'Coin data required.');
assert((json.version >>> 0) === json.version, 'Version must be a uint32.');
assert(json.height === -1 || (json.height >>> 0) === json.height,
'Height must be a uint32.');
assert(util.isU64(json.value), 'Value must be a uint64.');
assert(typeof json.coinbase === 'boolean', 'Coinbase must be a boolean.');
this.version = json.version;
this.height = json.height;
this.value = json.value;
this.address.fromString(json.address, network);
this.coinbase = json.coinbase;
if (json.covenant != null)
this.covenant.fromJSON(json.covenant);
if (json.hash != null) {
assert((json.index >>> 0) === json.index, 'Index must be a uint32.');
this.hash = util.parseHex(json.hash, 32);
this.index = json.index;
}
return this;
}
/**
* Calculate size of coin.
* @returns {Number}
*/
getSize() {
return 17 + this.address.getSize() + this.covenant.getVarSize();
}
/**
* Write the coin to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
let height = this.height;
if (height === -1)
height = 0xffffffff;
bw.writeU32(this.version);
bw.writeU32(height);
bw.writeU64(this.value);
this.address.write(bw);
this.covenant.write(bw);
bw.writeU8(this.coinbase ? 1 : 0);
return bw;
}
/**
* Inject properties from serialized buffer writer.
* @param {BufferReader} br
*/
read(br) {
this.version = br.readU32();
this.height = br.readU32();
this.value = br.readU64();
this.address.read(br);
this.covenant.read(br);
this.coinbase = br.readU8() === 1;
if (this.height === 0xffffffff)
this.height = -1;
return this;
}
/**
* Inject properties from TX.
* @param {TX} tx
* @param {Number} index
* @param {Number} height
* @returns {this}
*/
fromTX(tx, index, height) {
assert(typeof index === 'number');
assert(typeof height === 'number');
assert(index >= 0 && index < tx.outputs.length);
this.version = tx.version;
this.height = height;
this.value = tx.outputs[index].value;
this.address = tx.outputs[index].address;
this.covenant = tx.outputs[index].covenant;
this.coinbase = tx.isCoinbase();
this.hash = tx.hash();
this.index = index;
return this;
}
/**
* Instantiate a coin from a TX
* @param {TX} tx
* @param {Number} index - Output index.
* @param {Number} height - Chain height.
* @returns {Coin}
*/
static fromTX(tx, index, height) {
return new this().fromTX(tx, index, height);
}
/**
* Test an object to see if it is a Coin.
* @param {Object} obj
* @returns {Boolean}
*/
static isCoin(obj) {
return obj instanceof Coin;
}
}
/*
* Expose
*/
module.exports = Coin;

View file

@ -0,0 +1,891 @@
/*!
* covenant.js - covenant object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const util = require('../utils/util');
const rules = require('../covenants/rules');
const consensus = require('../protocol/consensus');
const {encoding} = bio;
const {types, typesByVal} = rules;
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./address')} Address */
/** @typedef {ReturnType<Covenant['getJSON']>} CovenantJSON */
/**
* Covenant
* @alias module:primitives.Covenant
* @property {Number} type
* @property {Buffer[]} items
* @property {Number} length
*/
class Covenant extends bio.Struct {
/**
* Create a covenant.
* @constructor
* @param {rules.types|Object} [type]
* @param {Buffer[]} [items]
*/
constructor(type, items) {
super();
this.type = types.NONE;
this.items = [];
if (type != null)
this.fromOptions(type, items);
}
/**
* Inject properties from options object.
* @param {rules.types|Object} [type]
* @param {Buffer[]} [items]
* @returns {this}
*/
fromOptions(type, items) {
if (type && typeof type === 'object') {
items = type.items;
type = type.type;
}
if (Array.isArray(type))
return this.fromArray(type);
if (type != null) {
assert((type & 0xff) === type);
this.type = type;
if (items)
return this.fromArray(items);
return this;
}
return this;
}
/**
* Get an item.
* @param {Number} index
* @returns {Buffer}
*/
get(index) {
if (index < 0)
index += this.items.length;
assert((index >>> 0) === index);
assert(index < this.items.length);
return this.items[index];
}
/**
* Set an item.
* @param {Number} index
* @param {Buffer} item
* @returns {Covenant}
*/
set(index, item) {
if (index < 0)
index += this.items.length;
assert((index >>> 0) === index);
assert(index <= this.items.length);
assert(Buffer.isBuffer(item));
this.items[index] = item;
return this;
}
/**
* Push an item.
* @param {Buffer} item
* @returns {this}
*/
push(item) {
assert(Buffer.isBuffer(item));
this.items.push(item);
return this;
}
/**
* Get a uint8.
* @param {Number} index
* @returns {Number}
*/
getU8(index) {
const item = this.get(index);
assert(item.length === 1);
return item[0];
}
/**
* Push a uint8.
* @param {Number} num
* @returns {Covenant}
*/
pushU8(num) {
assert((num & 0xff) === num);
const item = Buffer.allocUnsafe(1);
item[0] = num;
this.push(item);
return this;
}
/**
* Get a uint32.
* @param {Number} index
* @returns {Number}
*/
getU32(index) {
const item = this.get(index);
assert(item.length === 4);
return bio.readU32(item, 0);
}
/**
* Push a uint32.
* @param {Number} num
* @returns {Covenant}
*/
pushU32(num) {
assert((num >>> 0) === num);
const item = Buffer.allocUnsafe(4);
bio.writeU32(item, num, 0);
this.push(item);
return this;
}
/**
* Get a hash.
* @param {Number} index
* @returns {Hash}
*/
getHash(index) {
const item = this.get(index);
assert(item.length === 32);
return item;
}
/**
* Push a hash.
* @param {Hash} hash
* @returns {Covenant}
*/
pushHash(hash) {
assert(Buffer.isBuffer(hash));
assert(hash.length === 32);
this.push(hash);
return this;
}
/**
* Get a string.
* @param {Number} index
* @returns {String}
*/
getString(index) {
const item = this.get(index);
assert(item.length >= 1 && item.length <= 63);
return item.toString('binary');
}
/**
* Push a string.
* @param {String} str
* @returns {Covenant}
*/
pushString(str) {
assert(typeof str === 'string');
assert(str.length >= 1 && str.length <= 63);
this.push(Buffer.from(str, 'binary'));
return this;
}
/**
* Test whether the covenant is known.
* @returns {Boolean}
*/
isKnown() {
return this.type <= types.REVOKE;
}
/**
* Test whether the covenant is unknown.
* @returns {Boolean}
*/
isUnknown() {
return this.type > types.REVOKE;
}
/**
* Test whether the covenant is a payment.
* @returns {Boolean}
*/
isNone() {
return this.type === types.NONE;
}
/**
* Test whether the covenant is a claim.
* @returns {Boolean}
*/
isClaim() {
return this.type === types.CLAIM;
}
/**
* Test whether the covenant is an open.
* @returns {Boolean}
*/
isOpen() {
return this.type === types.OPEN;
}
/**
* Test whether the covenant is a bid.
* @returns {Boolean}
*/
isBid() {
return this.type === types.BID;
}
/**
* Test whether the covenant is a reveal.
* @returns {Boolean}
*/
isReveal() {
return this.type === types.REVEAL;
}
/**
* Test whether the covenant is a redeem.
* @returns {Boolean}
*/
isRedeem() {
return this.type === types.REDEEM;
}
/**
* Test whether the covenant is a register.
* @returns {Boolean}
*/
isRegister() {
return this.type === types.REGISTER;
}
/**
* Test whether the covenant is an update.
* @returns {Boolean}
*/
isUpdate() {
return this.type === types.UPDATE;
}
/**
* Test whether the covenant is a renewal.
* @returns {Boolean}
*/
isRenew() {
return this.type === types.RENEW;
}
/**
* Test whether the covenant is a transfer.
* @returns {Boolean}
*/
isTransfer() {
return this.type === types.TRANSFER;
}
/**
* Test whether the covenant is a finalize.
* @returns {Boolean}
*/
isFinalize() {
return this.type === types.FINALIZE;
}
/**
* Test whether the covenant is a revocation.
* @returns {Boolean}
*/
isRevoke() {
return this.type === types.REVOKE;
}
/**
* Build helpers
*/
/**
* Set covenant to NONE.
* @returns {Covenant}
*/
setNone() {
this.type = types.NONE;
this.items = [];
return this;
}
/**
* Set covenant to OPEN.
* @param {Hash} nameHash
* @param {Buffer} rawName
* @returns {Covenant}
*/
setOpen(nameHash, rawName) {
this.type = types.OPEN;
this.items = [];
this.pushHash(nameHash);
this.pushU32(0);
this.push(rawName);
return this;
}
/**
* Set covenant to BID.
* @param {Hash} nameHash
* @param {Number} height
* @param {Buffer} rawName
* @param {Hash} blind
* @returns {Covenant}
*/
setBid(nameHash, height, rawName, blind) {
this.type = types.BID;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.push(rawName);
this.pushHash(blind);
return this;
}
/**
* Set covenant to REVEAL.
* @param {Hash} nameHash
* @param {Number} height
* @param {Hash} nonce
* @returns {Covenant}
*/
setReveal(nameHash, height, nonce) {
this.type = types.REVEAL;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.pushHash(nonce);
return this;
}
/**
* Set covenant to REDEEM.
* @param {Hash} nameHash
* @param {Number} height
* @returns {Covenant}
*/
setRedeem(nameHash, height) {
this.type = types.REDEEM;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
return this;
}
/**
* Set covenant to REGISTER.
* @param {Hash} nameHash
* @param {Number} height
* @param {Buffer} record
* @param {Hash} blockHash
* @returns {Covenant}
*/
setRegister(nameHash, height, record, blockHash) {
this.type = types.REGISTER;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.push(record);
this.pushHash(blockHash);
return this;
}
/**
* Set covenant to UPDATE.
* @param {Hash} nameHash
* @param {Number} height
* @param {Buffer} resource
* @returns {Covenant}
*/
setUpdate(nameHash, height, resource) {
this.type = types.UPDATE;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.push(resource);
return this;
}
/**
* Set covenant to RENEW.
* @param {Hash} nameHash
* @param {Number} height
* @param {Hash} blockHash
* @returns {Covenant}
*/
setRenew(nameHash, height, blockHash) {
this.type = types.RENEW;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.pushHash(blockHash);
return this;
}
/**
* Set covenant to TRANSFER.
* @param {Hash} nameHash
* @param {Number} height
* @param {Address} address
* @returns {Covenant}
*/
setTransfer(nameHash, height, address) {
this.type = types.TRANSFER;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.pushU8(address.version);
this.push(address.hash);
return this;
}
/**
* Set covenant to REVOKE.
* @param {Hash} nameHash
* @param {Number} height
* @param {Buffer} rawName
* @param {Number} flags
* @param {Number} claimed
* @param {Number} renewals
* @param {Hash} blockHash
* @returns {Covenant}
*/
setFinalize(nameHash, height, rawName, flags, claimed, renewals, blockHash) {
this.type = types.FINALIZE;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.push(rawName);
this.pushU8(flags);
this.pushU32(claimed);
this.pushU32(renewals);
this.pushHash(blockHash);
return this;
}
/**
* Set covenant to REVOKE.
* @param {Hash} nameHash
* @param {Number} height
* @returns {Covenant}
*/
setRevoke(nameHash, height) {
this.type = types.REVOKE;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
return this;
}
/**
* Set covenant to CLAIM.
* @param {Hash} nameHash
* @param {Number} height
* @param {Buffer} rawName
* @param {Number} flags
* @param {Hash} commitHash
* @param {Number} commitHeight
* @returns {Covenant}
*/
setClaim(nameHash, height, rawName, flags, commitHash, commitHeight) {
this.type = types.CLAIM;
this.items = [];
this.pushHash(nameHash);
this.pushU32(height);
this.push(rawName);
this.pushU8(flags);
this.pushHash(commitHash);
this.pushU32(commitHeight);
return this;
}
/**
* Test whether the covenant is name-related.
* @returns {Boolean}
*/
isName() {
if (this.type < types.CLAIM)
return false;
if (this.type > types.REVOKE)
return false;
return true;
}
/**
* Test whether a covenant type should be
* considered subject to the dust policy rule.
* @returns {Boolean}
*/
isDustworthy() {
switch (this.type) {
case types.NONE:
case types.BID:
return true;
default:
return this.type > types.REVOKE;
}
}
/**
* Test whether a coin should be considered
* unspendable in the coin selector.
* @returns {Boolean}
*/
isNonspendable() {
switch (this.type) {
case types.NONE:
case types.OPEN:
case types.REDEEM:
return false;
default:
return true;
}
}
/**
* Test whether a covenant should be considered "linked".
* @returns {Boolean}
*/
isLinked() {
return this.type >= types.REVEAL && this.type <= types.REVOKE;
}
/**
* Convert covenant to an array of buffers.
* @returns {Buffer[]}
*/
toArray() {
return this.items.slice();
}
/**
* Inject properties from an array of buffers.
* @private
* @param {Buffer[]} items
*/
fromArray(items) {
assert(Array.isArray(items));
this.items = items;
return this;
}
/**
* Test whether the covenant is unspendable.
* @returns {Boolean}
*/
isUnspendable() {
return this.type === types.REVOKE;
}
/**
* Convert the covenant to a string.
* @returns {String}
*/
toString() {
return this.encode().toString('hex', 1);
}
/**
* Inject properties from covenant.
* Used for cloning.
* @param {this} covenant
* @returns {this}
*/
inject(covenant) {
assert(covenant instanceof this.constructor);
this.type = covenant.type;
this.items = covenant.items.slice();
return this;
}
/**
* Test the covenant against a bloom filter.
* @param {BloomFilter} filter
* @returns {Boolean}
*/
test(filter) {
for (const item of this.items) {
if (item.length === 0)
continue;
if (filter.test(item))
return true;
}
return false;
}
/**
* Find a data element in a covenant.
* @param {Buffer} data - Data element to match against.
* @returns {Number} Index (`-1` if not present).
*/
indexOf(data) {
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i];
if (item.equals(data))
return i;
}
return -1;
}
/**
* Calculate size of the covenant
* excluding the varint size bytes.
* @returns {Number}
*/
getSize() {
let size = 0;
for (const item of this.items)
size += encoding.sizeVarBytes(item);
return size;
}
/**
* Calculate size of the covenant
* including the varint size bytes.
* @returns {Number}
*/
getVarSize() {
return 1 + encoding.sizeVarint(this.items.length) + this.getSize();
}
/**
* Write covenant to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU8(this.type);
bw.writeVarint(this.items.length);
for (const item of this.items)
bw.writeVarBytes(item);
return bw;
}
/**
* Encode covenant.
* @returns {Buffer}
*/
encode() {
const bw = bio.write(this.getVarSize());
this.write(bw);
return bw.render();
}
/**
* Convert covenant to a hex string.
*/
getJSON() {
const items = [];
for (const item of this.items)
items.push(item.toString('hex'));
return {
type: this.type,
action: typesByVal[this.type],
items
};
}
/**
* Inject properties from json object.
* @param {CovenantJSON} json
* @returns {this}
*/
fromJSON(json) {
assert(json && typeof json === 'object', 'Covenant must be an object.');
assert((json.type & 0xff) === json.type);
assert(Array.isArray(json.items));
this.type = json.type;
for (const str of json.items) {
const item = util.parseHex(str, -1);
this.items.push(item);
}
return this;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.type = br.readU8();
const count = br.readVarint();
if (count > consensus.MAX_SCRIPT_STACK)
throw new Error('Too many covenant items.');
for (let i = 0; i < count; i++)
this.items.push(br.readVarBytes());
return this;
}
/**
* Inject items from string.
* @param {String|String[]} items
* @returns {this}
*/
fromString(items) {
if (!Array.isArray(items)) {
assert(typeof items === 'string');
items = items.trim();
if (items.length === 0)
return this;
items = items.split(/\s+/);
}
for (const item of items)
this.items.push(util.parseHex(item, -1));
return this;
}
/**
* Inspect a covenant object.
* @returns {String}
*/
format() {
return `<Covenant: ${typesByVal[this.type]}:${this.toString()}>`;
}
/**
* Insantiate covenant from an array of buffers.
* @param {Buffer[]} items
* @returns {Covenant}
*/
static fromArray(items) {
return new this().fromArray(items);
}
/**
* Test an object to see if it is a covenant.
* @param {Object} obj
* @returns {Boolean}
*/
static isCovenant(obj) {
return obj instanceof Covenant;
}
}
Covenant.types = types;
/*
* Expose
*/
module.exports = Covenant;

View file

@ -0,0 +1,216 @@
/*!
* headers.js - headers object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const util = require('../utils/util');
const AbstractBlock = require('./abstractblock');
/** @typedef {import('bufio').BufferReader} BufferReader */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../protocol/network')} Network */
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
/** @typedef {import('../coins/coinview')} CoinView */
/** @typedef {import('./block')} Block */
/** @typedef {import('./merkleblock')} MerkleBlock */
/**
* Headers
* Represents block headers obtained
* from the network via `headers`.
* @alias module:primitives.Headers
* @extends AbstractBlock
*/
class Headers extends AbstractBlock {
/**
* Create headers.
* @constructor
* @param {Object} [options]
*/
constructor(options) {
super();
if (options)
this.parseOptions(options);
}
/**
* Perform non-contextual
* verification on the headers.
* @returns {Boolean}
*/
verifyBody() {
return true;
}
/**
* Get size of the headers.
* @returns {Number}
*/
getSize() {
return this.sizeHead();
}
/**
* Serialize the headers to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
this.writeHead(bw);
return bw;
}
/**
* Inject properties from buffer reader.
* @param {BufferReader} br
*/
read(br) {
this.readHead(br);
return this;
}
/**
* Instantiate headers from serialized data.
* @param {Buffer} data
* @returns {Headers}
*/
static fromHead(data) {
return new this().fromHead(data);
}
/**
* Instantiate headers from a chain entry.
* @param {ChainEntry} entry
* @returns {Headers}
*/
static fromEntry(entry) {
const headers = new this();
headers.version = entry.version;
headers.prevBlock = entry.prevBlock;
headers.merkleRoot = entry.merkleRoot;
headers.witnessRoot = entry.witnessRoot;
headers.treeRoot = entry.treeRoot;
headers.reservedRoot = entry.reservedRoot;
headers.time = entry.time;
headers.bits = entry.bits;
headers.nonce = entry.nonce;
headers.extraNonce = entry.extraNonce;
headers.mask = entry.mask;
headers._hash = entry.hash;
return headers;
}
/**
* Convert the block to a headers object.
* @returns {Headers}
*/
toHeaders() {
return this;
}
/**
* Convert the block to a headers object.
* @param {Block|MerkleBlock} block
* @returns {Headers}
*/
static fromBlock(block) {
const headers = new this(block);
headers._hash = block._hash;
return headers;
}
/**
* Convert the block to an object suitable
* for JSON serialization.
* @param {(NetworkType|Network)?} [network]
* @param {CoinView} [view]
* @param {Number} [height]
* @returns {Object}
*/
getJSON(network, view, height) {
return {
hash: this.hash().toString('hex'),
height: height,
version: this.version,
prevBlock: this.prevBlock.toString('hex'),
merkleRoot: this.merkleRoot.toString('hex'),
witnessRoot: this.witnessRoot.toString('hex'),
treeRoot: this.treeRoot.toString('hex'),
reservedRoot: this.reservedRoot.toString('hex'),
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex')
};
}
/**
* Inject properties from json object.
* @param {Object} json
*/
fromJSON(json) {
this.parseJSON(json);
return this;
}
/**
* Inspect the headers and return a more
* user-friendly representation of the data.
* @param {CoinView} [view]
* @param {Number} [height]
* @returns {Object}
*/
format(view, height) {
return {
hash: this.hash().toString('hex'),
height: height != null ? height : -1,
date: util.date(this.time),
version: this.version.toString(16),
prevBlock: this.prevBlock.toString('hex'),
merkleRoot: this.merkleRoot.toString('hex'),
witnessRoot: this.witnessRoot.toString('hex'),
treeRoot: this.treeRoot.toString('hex'),
reservedRoot: this.reservedRoot.toString('hex'),
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex')
};
}
/**
* Test an object to see if it is a Headers object.
* @param {Object} obj
* @returns {Boolean}
*/
static isHeaders(obj) {
return obj instanceof Headers;
}
}
/*
* Expose
*/
module.exports = Headers;

View file

@ -0,0 +1,29 @@
/*!
* primitives/index.js - handshake primitives for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/**
* @module primitives
*/
exports.AbstractBlock = require('./abstractblock');
exports.Address = require('./address');
exports.Block = require('./block');
exports.Claim = require('./claim');
exports.Coin = require('./coin');
exports.Covenant = require('./covenant');
exports.Headers = require('./headers');
exports.Input = require('./input');
exports.InvItem = require('./invitem');
exports.KeyRing = require('./keyring');
exports.MemBlock = require('./memblock');
exports.MerkleBlock = require('./merkleblock');
exports.MTX = require('./mtx');
exports.Outpoint = require('./outpoint');
exports.Output = require('./output');
exports.TX = require('./tx');
exports.TXMeta = require('./txmeta');

343
docs/js-primitives/input.js Normal file
View file

@ -0,0 +1,343 @@
/*!
* input.js - input object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const Network = require('../protocol/network');
const Witness = require('../script/witness');
const Outpoint = require('./outpoint');
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./tx')} TX */
/** @typedef {import('./coin')} Coin */
/** @typedef {import('./address')} Address */
/** @typedef {import('../wallet/path')} Path */
/** @typedef {ReturnType<Input['getJSON']>} InputJSON */
/**
* Input
* Represents a transaction input.
* @alias module:primitives.Input
* @property {Outpoint} prevout - Outpoint.
* @property {Script} script - Input script / scriptSig.
* @property {Number} sequence - nSequence.
* @property {Witness} witness - Witness (empty if not present).
*/
class Input extends bio.Struct {
/**
* Create transaction input.
* @constructor
* @param {Object?} [options]
*/
constructor(options) {
super();
this.prevout = new Outpoint();
this.witness = new Witness();
this.sequence = 0xffffffff;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
assert(options, 'Input data is required.');
this.prevout.fromOptions(options.prevout);
if (options.witness)
this.witness.fromOptions(options.witness);
if (options.sequence != null) {
assert((options.sequence >>> 0) === options.sequence,
'Sequence must be a uint32.');
this.sequence = options.sequence;
}
return this;
}
/**
* Clone the input.
* @param {this} input
* @returns {this}
*/
inject(input) {
this.prevout = input.prevout;
this.witness.inject(input.witness);
this.sequence = input.sequence;
return this;
}
/**
* Test equality against another input.
* @param {Input} input
* @returns {Boolean}
*/
equals(input) {
assert(Input.isInput(input));
return this.prevout.equals(input.prevout);
}
/**
* Compare against another input (BIP69).
* @param {Input} input
* @returns {Number}
*/
compare(input) {
assert(Input.isInput(input));
return this.prevout.compare(input.prevout);
}
/**
* Get the previous output script's address. Will "guess"
* based on the input script and/or witness if coin
* is not available.
* @param {Coin?} [coin]
* @returns {Address?} addr
*/
getAddress(coin) {
if (this.isCoinbase())
return null;
if (coin)
return coin.getAddress();
return this.witness.getInputAddress();
}
/**
* Get the address hash.
* @param {Coin?} [coin]
* @returns {Hash?} hash
*/
getHash(coin) {
const addr = this.getAddress(coin);
if (!addr)
return null;
return addr.getHash();
}
/**
* Test to see if nSequence is equal to uint32max.
* @returns {Boolean}
*/
isFinal() {
return this.sequence === 0xffffffff;
}
/**
* Test to see if outpoint is null.
* @returns {Boolean}
*/
isCoinbase() {
return this.prevout.isNull();
}
/**
* Convert the input to a more user-friendly object.
* @param {Coin?} coin
* @returns {Object}
*/
format(coin) {
return {
address: this.getAddress(coin),
prevout: this.prevout,
witness: this.witness,
sequence: this.sequence,
coin: coin || null
};
}
/**
* Convert the input to an object suitable
* for JSON serialization.
* @param {NetworkType|Network} [network]
* @param {Coin} [coin]
* @param {Path} [path]
*/
getJSON(network, coin, path) {
network = Network.get(network);
let addr;
if (!coin) {
addr = this.getAddress();
if (addr)
addr = addr.toString(network);
}
return {
prevout: this.prevout.toJSON(),
witness: this.witness.toJSON(),
sequence: this.sequence,
address: addr,
coin: coin ? coin.getJSON(network, true) : undefined,
path: path ? path.getJSON(network) : undefined
};
}
/**
* Inject properties from a JSON object.
* @param {InputJSON} json
* @returns {this}
*/
fromJSON(json) {
assert(json, 'Input data is required.');
assert((json.sequence >>> 0) === json.sequence,
'Sequence must be a uint32.');
this.prevout.fromJSON(json.prevout);
this.witness.fromJSON(json.witness);
this.sequence = json.sequence;
return this;
}
/**
* Calculate size of serialized input.
* @returns {Number}
*/
getSize() {
return 40;
}
/**
* Write the input to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
this.prevout.write(bw);
bw.writeU32(this.sequence);
return bw;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.prevout.read(br);
this.sequence = br.readU32();
return this;
}
/**
* Inject properties from outpoint.
* @param {Outpoint} outpoint
* @returns {this}
*/
fromOutpoint(outpoint) {
assert(Buffer.isBuffer(outpoint.hash));
assert(typeof outpoint.index === 'number');
this.prevout.hash = outpoint.hash;
this.prevout.index = outpoint.index;
return this;
}
/**
* Instantiate input from outpoint.
* @param {Outpoint} outpoint
* @returns {Input}
*/
static fromOutpoint(outpoint) {
return new this().fromOutpoint(outpoint);
}
/**
* Inject properties from coin.
* @private
* @param {Coin} coin
*/
fromCoin(coin) {
assert(Buffer.isBuffer(coin.hash));
assert(typeof coin.index === 'number');
this.prevout.hash = coin.hash;
this.prevout.index = coin.index;
return this;
}
/**
* Instantiate input from coin.
* @param {Coin} coin
* @returns {Input}
*/
static fromCoin(coin) {
return new this().fromCoin(coin);
}
/**
* Inject properties from transaction.
* @param {TX} tx
* @param {Number} index
*/
fromTX(tx, index) {
assert(tx);
assert(typeof index === 'number');
assert(index >= 0 && index < tx.outputs.length);
this.prevout.hash = tx.hash();
this.prevout.index = index;
return this;
}
/**
* Instantiate input from tx.
* @param {TX} tx
* @param {Number} index
* @returns {Input}
*/
static fromTX(tx, index) {
return new this().fromTX(tx, index);
}
/**
* Test an object to see if it is an Input.
* @param {Object} obj
* @returns {Boolean}
*/
static isInput(obj) {
return obj instanceof Input;
}
}
/*
* Expose
*/
module.exports = Input;

View file

@ -0,0 +1,161 @@
/*!
* invitem.js - inv item object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const bio = require('bufio');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/**
* Inv Item
* @alias module:primitives.InvItem
* @constructor
* @property {InvType} type
* @property {Hash} hash
*/
class InvItem extends bio.Struct {
/**
* Create an inv item.
* @constructor
* @param {InvItem.types} type
* @param {Hash} hash
*/
constructor(type, hash) {
super();
this.type = type;
this.hash = hash;
}
/**
* Write inv item to buffer writer.
* @returns {Number}
*/
getSize() {
return 36;
}
/**
* Write inv item to buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU32(this.type);
bw.writeHash(this.hash);
return bw;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.type = br.readU32();
this.hash = br.readHash();
return this;
}
/**
* Test whether the inv item is a block.
* @returns {Boolean}
*/
isBlock() {
switch (this.type) {
case InvItem.types.BLOCK:
case InvItem.types.FILTERED_BLOCK:
case InvItem.types.CMPCT_BLOCK:
return true;
default:
return false;
}
}
/**
* Test whether the inv item is a tx.
* @returns {Boolean}
*/
isTX() {
switch (this.type) {
case InvItem.types.TX:
return true;
default:
return false;
}
}
/**
* Test whether the inv item is a claim.
* @returns {Boolean}
*/
isClaim() {
switch (this.type) {
case InvItem.types.CLAIM:
return true;
default:
return false;
}
}
/**
* Test whether the inv item is an airdrop proof.
* @returns {Boolean}
*/
isAirdrop() {
switch (this.type) {
case InvItem.types.AIRDROP:
return true;
default:
return false;
}
}
}
/**
* Inv types.
* @enum {Number}
* @default
*/
InvItem.types = {
TX: 1,
BLOCK: 2,
FILTERED_BLOCK: 3,
CMPCT_BLOCK: 4,
CLAIM: 5,
AIRDROP: 6
};
/**
* Inv types by value.
* @const {Object}
*/
InvItem.typesByVal = {
1: 'TX',
2: 'BLOCK',
3: 'FILTERED_BLOCK',
4: 'CMPCT_BLOCK',
5: 'CLAIM',
6: 'AIRDROP'
};
/*
* Expose
*/
module.exports = InvItem;

View file

@ -0,0 +1,649 @@
/*!
* keyring.js - keyring object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const base58 = require('bcrypto/lib/encoding/base58');
const bio = require('bufio');
const blake2b = require('bcrypto/lib/blake2b');
const hash256 = require('bcrypto/lib/hash256');
const Network = require('../protocol/network');
const Script = require('../script/script');
const Address = require('./address');
const Output = require('./output');
const secp256k1 = require('bcrypto/lib/secp256k1');
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../types').Base58String} Base58String */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./tx')} TX */
/*
* Constants
*/
const ZERO_KEY = Buffer.alloc(33, 0x00);
/**
* Key Ring
* Represents a key ring which amounts to an address.
* @alias module:primitives.KeyRing
*/
class KeyRing extends bio.Struct {
/**
* Create a key ring.
* @constructor
* @param {Object?} [options]
*/
constructor(options) {
super();
this.publicKey = ZERO_KEY;
/** @type {Buffer?} */
this.privateKey = null;
/** @type {Script?} */
this.script = null;
/** @type {Hash?} */
this._keyHash = null;
/** @type {Address?} */
this._keyAddress = null;
/** @type {Hash?} */
this._scriptHash = null;
/** @type {Address?} */
this._scriptAddress = null;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
let key = toKey(options);
if (Buffer.isBuffer(key))
return this.fromKey(key);
key = toKey(options.key);
if (options.publicKey)
key = toKey(options.publicKey);
if (options.privateKey)
key = toKey(options.privateKey);
const script = options.script;
if (script)
return this.fromScript(key, script);
return this.fromKey(key);
}
/**
* Clear cached key/script hashes.
*/
refresh() {
this._keyHash = null;
this._keyAddress = null;
this._scriptHash = null;
this._scriptAddress = null;
return this;
}
/**
* Inject data from private key.
* @param {Buffer} key
* @returns {this}
*/
fromPrivate(key) {
assert(Buffer.isBuffer(key), 'Private key must be a buffer.');
assert(secp256k1.privateKeyVerify(key), 'Not a valid private key.');
this.privateKey = key;
this.publicKey = secp256k1.publicKeyCreate(key, true);
return this;
}
/**
* Instantiate keyring from a private key.
* @param {Buffer} key
* @returns {KeyRing}
*/
static fromPrivate(key) {
return new this().fromPrivate(key);
}
/**
* Inject data from public key.
* @param {Buffer} key
*/
fromPublic(key) {
assert(Buffer.isBuffer(key), 'Public key must be a buffer.');
assert(secp256k1.publicKeyVerify(key) && key.length === 33,
'Not a valid public key.');
this.publicKey = key;
return this;
}
/**
* Generate a keyring.
* @returns {KeyRing}
*/
generate() {
const key = secp256k1.privateKeyGenerate();
return this.fromKey(key);
}
/**
* Generate a keyring.
* @returns {KeyRing}
*/
static generate() {
return new this().generate();
}
/**
* Instantiate keyring from a public key.
* @param {Buffer} publicKey
* @returns {KeyRing}
*/
static fromPublic(publicKey) {
return new this().fromPublic(publicKey);
}
/**
* Inject data from public key.
* @param {Buffer} key
*/
fromKey(key) {
assert(Buffer.isBuffer(key), 'Key must be a buffer.');
if (key.length === 32)
return this.fromPrivate(key);
return this.fromPublic(key);
}
/**
* Instantiate keyring from a public key.
* @param {Buffer} key
* @returns {KeyRing}
*/
static fromKey(key) {
return new this().fromKey(key);
}
/**
* Inject data from script.
* @private
* @param {Buffer} key
* @param {Script} script
*/
fromScript(key, script) {
assert(script instanceof Script, 'Non-script passed into KeyRing.');
this.fromKey(key);
this.script = script;
return this;
}
/**
* Instantiate keyring from script.
* @param {Buffer} key
* @param {Script} script
* @returns {KeyRing}
*/
static fromScript(key, script) {
return new this().fromScript(key, script);
}
/**
* Calculate WIF serialization size.
* @returns {Number}
*/
getSecretSize() {
let size = 0;
size += 1;
size += this.privateKey.length;
size += 1;
size += 4;
return size;
}
/**
* Convert key to a secret.
* @param {(Network|NetworkType)?} network
* @returns {Base58String}
*/
toSecret(network) {
const size = this.getSecretSize();
const bw = bio.write(size);
assert(this.privateKey, 'Cannot serialize without private key.');
network = Network.get(network);
bw.writeU8(network.keyPrefix.privkey);
bw.writeBytes(this.privateKey);
bw.writeU8(1);
bw.writeChecksum(hash256.digest);
return base58.encode(bw.render());
}
/**
* Inject properties from serialized secret.
* @param {Base58String} data
* @param {(Network|NetworkType)?} [network]
*/
fromSecret(data, network) {
const br = bio.read(base58.decode(data), true);
const version = br.readU8();
Network.fromWIF(version, network);
const key = br.readBytes(32);
assert(br.readU8() === 1, 'Bad compression flag.');
br.verifyChecksum(hash256.digest);
return this.fromPrivate(key);
}
/**
* Instantiate a keyring from a serialized secret.
* @param {Base58String} data
* @param {(Network|NetworkType)?} network
* @returns {KeyRing}
*/
static fromSecret(data, network) {
return new this().fromSecret(data, network);
}
/**
* Get private key.
* @returns {Buffer} Private key.
*/
getPrivateKey() {
if (!this.privateKey)
return null;
return this.privateKey;
}
/**
* Get public key.
* @returns {Buffer}
*/
getPublicKey() {
return this.publicKey;
}
/**
* Get redeem script.
* @returns {Script}
*/
getScript() {
return this.script;
}
/**
* Get scripthash.
* @returns {Buffer}
*/
getScriptHash() {
if (!this.script)
return null;
if (!this._scriptHash)
this._scriptHash = this.script.sha3();
return this._scriptHash;
}
/**
* Get scripthash address.
* @returns {Address}
*/
getScriptAddress() {
if (!this.script)
return null;
if (!this._scriptAddress) {
const hash = this.getScriptHash();
const addr = Address.fromScripthash(hash);
this._scriptAddress = addr;
}
return this._scriptAddress;
}
/**
* Get public key hash.
* @returns {Buffer}
*/
getKeyHash() {
if (!this._keyHash)
this._keyHash = blake2b.digest(this.publicKey, 20);
return this._keyHash;
}
/**
* Get pubkeyhash address.
* @returns {Address}
*/
getKeyAddress() {
if (!this._keyAddress) {
const hash = this.getKeyHash();
const addr = Address.fromPubkeyhash(hash);
this._keyAddress = addr;
}
return this._keyAddress;
}
/**
* Get hash.
* @returns {Buffer}
*/
getHash() {
if (this.script)
return this.getScriptHash();
return this.getKeyHash();
}
/**
* Get base58 address.
* @returns {Address}
*/
getAddress() {
if (this.script)
return this.getScriptAddress();
return this.getKeyAddress();
}
/**
* Test an address hash against hash and program hash.
* @param {Buffer} hash
* @returns {Boolean}
*/
ownHash(hash) {
if (!hash)
return false;
if (hash.equals(this.getKeyHash()))
return true;
if (this.script) {
if (hash.equals(this.getScriptHash()))
return true;
}
return false;
}
/**
* Check whether transaction output belongs to this address.
* @param {TX|Output} tx - Transaction or Output.
* @param {Number?} [index] - Output index.
* @returns {Boolean}
*/
ownOutput(tx, index) {
let output;
if (tx instanceof Output) {
output = tx;
} else {
output = tx.outputs[index];
assert(output, 'Output does not exist.');
}
return this.ownHash(output.getHash());
}
/**
* Test a hash against script hashes to
* find the correct redeem script, if any.
* @param {Buffer} hash
* @returns {Script|null}
*/
getRedeem(hash) {
if (this.script) {
if (hash.equals(this.getScriptHash()))
return this.script;
}
return null;
}
/**
* Sign a message.
* @param {Buffer} msg
* @returns {Buffer} Signature in DER format.
*/
sign(msg) {
assert(this.privateKey, 'Cannot sign without private key.');
return secp256k1.sign(msg, this.privateKey);
}
/**
* Verify a message.
* @param {Buffer} msg
* @param {Buffer} sig - Signature in DER format.
* @returns {Boolean}
*/
verify(msg, sig) {
return secp256k1.verify(msg, sig, this.publicKey);
}
/**
* Get witness program version.
* @returns {Number}
*/
getVersion() {
return 0;
}
/**
* Inspect keyring.
* @returns {Object}
*/
format() {
return this.toJSON();
}
/**
* Convert an KeyRing to a more json-friendly object.
* @param {(NetworkType|Network)?} [network]
* @returns {Object}
*/
getJSON(network) {
return {
publicKey: this.publicKey.toString('hex'),
script: this.script ? this.script.toHex() : null,
address: this.getAddress().toString(network)
};
}
/**
* Inject properties from json object.
* @param {Object} json
*/
fromJSON(json) {
assert(json);
assert(typeof json.publicKey === 'string');
assert(!json.script || typeof json.script === 'string');
this.publicKey = Buffer.from(json.publicKey, 'hex');
if (json.script)
this.script = Script.fromHex(json.script);
return this;
}
/**
* Calculate serialization size.
* @returns {Number}
*/
getSize() {
let size = 0;
size += 1;
if (this.privateKey)
size += 32;
else
size += 33;
size += this.script
? this.script.getVarSize()
: 1;
return size;
}
/**
* Write the keyring to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
if (this.privateKey) {
bw.writeU8(0);
bw.writeBytes(this.privateKey);
} else {
bw.writeU8(1);
bw.writeBytes(this.publicKey);
}
if (this.script)
bw.writeVarBytes(this.script.encode());
else
bw.writeVarint(0);
return bw;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
*/
read(br) {
const type = br.readU8();
switch (type) {
case 0: {
const key = br.readBytes(32);
this.privateKey = key;
this.publicKey = secp256k1.publicKeyCreate(key, true);
break;
}
case 1: {
const key = br.readBytes(33);
assert(secp256k1.publicKeyVerify(key), 'Invalid public key.');
this.publicKey = key;
break;
}
default: {
throw new Error('Invalid key.');
}
}
const script = br.readVarBytes();
if (script.length > 0)
this.script = Script.decode(script);
return this;
}
/**
* Test whether an object is a KeyRing.
* @param {Object} obj
* @returns {Boolean}
*/
static isKeyRing(obj) {
return obj instanceof KeyRing;
}
}
/*
* Helpers
*/
function toKey(opt) {
if (!opt)
return opt;
if (opt.privateKey)
return opt.privateKey;
if (opt.publicKey)
return opt.publicKey;
return opt;
}
/*
* Expose
*/
module.exports = KeyRing;

View file

@ -0,0 +1,264 @@
/*!
* memblock.js - memblock block object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const AbstractBlock = require('./abstractblock');
const Block = require('./block');
const Headers = require('./headers');
const Input = require('./input');
const Output = require('./output');
const consensus = require('../protocol/consensus');
const DUMMY = Buffer.alloc(0);
/** @typedef {import('../types').BufioWriter} BufioWriter */
/**
* Mem Block
* A block object which is essentially a "placeholder"
* for a full {@link Block} object. The v8 garbage
* collector's head will explode if there is too much
* data on the javascript heap. Blocks can currently
* be up to 1mb in size. In the future, they may be
* 2mb, 8mb, or maybe 20mb, who knows? A MemBlock
* is an optimization which defers parsing of
* the serialized transactions (the block Buffer) until
* the block has passed through the chain queue and
* is about to enter the chain. This keeps a lot data
* off of the javascript heap for most of the time a
* block even exists in memory, and manages to keep a
* lot of strain off of the garbage collector. Having
* 500mb of blocks on the js heap would not be a good
* thing.
* @alias module:primitives.MemBlock
* @extends AbstractBlock
*/
class MemBlock extends AbstractBlock {
/**
* Create a mem block.
* @constructor
*/
constructor() {
super();
this._raw = DUMMY;
}
/**
* Test whether the block is a memblock.
* @returns {Boolean}
*/
isMemory() {
return true;
}
/**
* Retrieve deterministically random padding.
* @param {Number} size
* @returns {Buffer}
*/
padding(size) {
assert((size >>> 0) === size);
const pad = Buffer.alloc(size);
const prevBlock = this._raw.slice(12, 12 + 32);
const treeRoot = this._raw.slice(12 + 32, 12 + 32 + 32);
for (let i = 0; i < size; i++)
pad[i] = prevBlock[i % 32] ^ treeRoot[i % 32];
return pad;
}
/**
* Serialize the block headers.
* @returns {Buffer}
*/
toPrehead() {
return Headers.decode(this._raw).toPrehead();
}
/**
* Calculate PoW hash.
* @returns {Buffer}
*/
powHash() {
const hash = this.shareHash();
const mask = this._raw.slice(consensus.HEADER_SIZE - 32,
consensus.HEADER_SIZE);
for (let i = 0; i < 32; i++)
hash[i] ^= mask[i];
return hash;
}
/**
* Serialize the block headers.
* @returns {Buffer}
*/
toHead() {
return this._raw.slice(0, consensus.HEADER_SIZE);
}
/**
* Get the full block size.
* @returns {Number}
*/
getSize() {
return this._raw.length;
}
/**
* Verify the block.
* @returns {Boolean}
*/
verifyBody() {
return true;
}
/**
* Retrieve the coinbase height
* from the coinbase input script.
* @returns {Number} height (-1 if not present).
*/
getCoinbaseHeight() {
try {
return this.parseCoinbaseHeight();
} catch (e) {
return -1;
}
}
/**
* Parse the coinbase height
* from the coinbase input script.
* @private
* @returns {Number} height (-1 if not present).
*/
parseCoinbaseHeight() {
const br = bio.read(this._raw, true);
br.seek(consensus.HEADER_SIZE);
const txCount = br.readVarint();
if (txCount === 0)
return -1;
br.seek(4);
const inCount = br.readVarint();
for (let i = 0; i < inCount; i++)
Input.read(br);
const outCount = br.readVarint();
for (let i = 0; i < outCount; i++)
Output.read(br);
return br.readU32();
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
*/
read(br) {
assert(br.offset === 0);
this.readHead(br);
this._raw = br.data;
return this;
}
/**
* Inject properties from serialized data.
* @param {Buffer} data
*/
decode(data) {
const br = bio.read(data);
return this.read(br);
}
/**
* Return serialized block data.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeBytes(this._raw);
return bw;
}
/**
* Return serialized block data.
* @returns {Buffer}
*/
encode() {
return this._raw;
}
/**
* Parse the serialized block data
* and create an actual {@link Block}.
* @returns {Block}
* @throws Parse error
*/
toBlock() {
const block = Block.decode(this._raw);
block._hash = this._hash;
return block;
}
/**
* Convert the block to a headers object.
* @returns {Headers}
*/
toHeaders() {
return Headers.fromBlock(this);
}
/**
* Test whether an object is a MemBlock.
* @param {Object} obj
* @returns {Boolean}
*/
static isMemBlock(obj) {
return obj instanceof MemBlock;
}
}
/*
* Expose
*/
module.exports = MemBlock;

View file

@ -0,0 +1,611 @@
/*!
* merkleblock.js - merkleblock object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const blake2b = require('bcrypto/lib/blake2b');
const merkle = require('bcrypto/lib/mrkl');
const {BufferMap, BufferSet} = require('buffer-map');
const util = require('../utils/util');
const consensus = require('../protocol/consensus');
const AbstractBlock = require('./abstractblock');
const Headers = require('./headers');
const DUMMY = Buffer.from([0]);
const {encoding} = bio;
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../coins/coinview')} CoinView */
/** @typedef {import('../protocol/network')} Network */
/** @typedef {import('./block')} Block */
/** @typedef {import('./tx')} TX */
/**
* Merkle Block
* Represents a merkle (filtered) block.
* @alias module:primitives.MerkleBlock
* @extends AbstractBlock
*/
class MerkleBlock extends AbstractBlock {
/**
* Create a merkle block.
* @constructor
* @param {Object} options
*/
constructor(options) {
super();
/** @type {TX[]} */
this.txs = [];
/** @type {Hash[]} */
this.hashes = [];
this.flags = DUMMY;
this.totalTX = 0;
this._tree = null;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
this.parseOptions(options);
assert(options, 'MerkleBlock data is required.');
assert(Array.isArray(options.hashes));
assert(Buffer.isBuffer(options.flags));
assert((options.totalTX >>> 0) === options.totalTX);
if (options.hashes) {
for (const hash of options.hashes) {
assert(Buffer.isBuffer(hash));
this.hashes.push(hash);
}
}
if (options.flags) {
assert(Buffer.isBuffer(options.flags));
this.flags = options.flags;
}
if (options.totalTX != null) {
assert((options.totalTX >>> 0) === options.totalTX);
this.totalTX = options.totalTX;
}
return this;
}
/**
* Clear any cached values.
* @param {Boolean?} [all] - Clear transactions.
*/
refresh(all) {
this._refresh();
this._tree = null;
if (!all)
return this;
for (const tx of this.txs)
tx.refresh();
return this;
}
/**
* Test the block's _matched_ transaction vector against a hash.
* @param {Hash} hash
* @returns {Boolean}
*/
hasTX(hash) {
return this.indexOf(hash) !== -1;
}
/**
* Test the block's _matched_ transaction vector against a hash.
* @param {Hash} hash
* @returns {Number} Index.
*/
indexOf(hash) {
const tree = this.getTree();
const index = tree.map.get(hash);
if (index == null)
return -1;
return index;
}
/**
* Verify the partial merkletree.
* @returns {Boolean}
*/
verifyBody() {
const [valid] = this.checkBody();
return valid;
}
/**
* Verify the partial merkletree.
* @private
* @returns {Array} [valid, reason, score]
*/
checkBody() {
const tree = this.getTree();
if (!tree.root.equals(this.merkleRoot))
return [false, 'bad-txnmrklroot', 100];
return [true, 'valid', 0];
}
/**
* Extract the matches from partial merkle
* tree and calculate merkle root.
* @returns {Object}
*/
getTree() {
if (!this._tree) {
try {
this._tree = this.extractTree();
} catch (e) {
this._tree = new PartialTree();
}
}
return this._tree;
}
/**
* Extract the matches from partial merkle
* tree and calculate merkle root.
* @private
* @returns {Object}
*/
extractTree() {
const matches = [];
const indexes = [];
const map = new BufferMap();
const hashes = this.hashes;
const flags = this.flags;
const totalTX = this.totalTX;
const sentinel = merkle.hashEmpty(blake2b);
let bitsUsed = 0;
let hashUsed = 0;
let height = 0;
let failed = false;
const width = (height) => {
return (totalTX + (1 << height) - 1) >>> height;
};
const traverse = (height, pos) => {
if (bitsUsed >= flags.length * 8) {
failed = true;
return consensus.ZERO_HASH;
}
const parent = (flags[bitsUsed / 8 | 0] >>> (bitsUsed % 8)) & 1;
bitsUsed += 1;
if (height === 0 || !parent) {
if (hashUsed >= hashes.length) {
failed = true;
return consensus.ZERO_HASH;
}
const hash = hashes[hashUsed];
hashUsed += 1;
if (height === 0 && parent) {
matches.push(hash);
indexes.push(pos);
map.set(hash, pos);
return merkle.hashLeaf(blake2b, hash);
}
return hash;
}
const left = traverse(height - 1, pos * 2);
let right;
if (pos * 2 + 1 < width(height - 1))
right = traverse(height - 1, pos * 2 + 1);
else
right = sentinel;
return merkle.hashInternal(blake2b, left, right);
};
if (totalTX === 0)
throw new Error('Zero transactions.');
if (totalTX > consensus.MAX_BLOCK_SIZE / 60)
throw new Error('Too many transactions.');
if (hashes.length > totalTX)
throw new Error('Too many hashes.');
if (flags.length * 8 < hashes.length)
throw new Error('Flags too small.');
while (width(height) > 1)
height += 1;
const root = traverse(height, 0);
if (failed)
throw new Error('Mutated merkle tree.');
if (((bitsUsed + 7) / 8 | 0) !== flags.length)
throw new Error('Too many flag bits.');
if (hashUsed !== hashes.length)
throw new Error('Incorrect number of hashes.');
return new PartialTree(root, matches, indexes, map);
}
/**
* Extract the coinbase height (always -1).
* @returns {Number}
*/
getCoinbaseHeight() {
return -1;
}
/**
* Inspect the block and return a more
* user-friendly representation of the data.
* @param {CoinView} [view]
* @param {Number} [height]
* @returns {Object}
*/
format(view, height) {
return {
hash: this.hash().toString('hex'),
height: height != null ? height : -1,
date: util.date(this.time),
version: this.version.toString(16),
prevBlock: this.prevBlock.toString('hex'),
merkleRoot: this.merkleRoot.toString('hex'),
witnessRoot: this.witnessRoot.toString('hex'),
treeRoot: this.treeRoot.toString('hex'),
reservedRoot: this.reservedRoot.toString('hex'),
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex'),
totalTX: this.totalTX,
hashes: this.hashes.map((hash) => {
return hash.toString('hex');
}),
flags: this.flags,
map: this.getTree().map,
txs: this.txs.length
};
}
/**
* Get merkleblock size.
* @returns {Number} Size.
*/
getSize() {
let size = 0;
size += this.sizeHead();
size += 4;
size += encoding.sizeVarint(this.hashes.length);
size += this.hashes.length * 32;
size += encoding.sizeVarint(this.flags.length);
size += this.flags.length;
return size;
}
/**
* Write the merkleblock to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
this.writeHead(bw);
bw.writeU32(this.totalTX);
bw.writeVarint(this.hashes.length);
for (const hash of this.hashes)
bw.writeHash(hash);
bw.writeVarBytes(this.flags);
return bw;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
*/
read(br) {
this.readHead(br);
this.totalTX = br.readU32();
const count = br.readVarint();
for (let i = 0; i < count; i++)
this.hashes.push(br.readHash());
this.flags = br.readVarBytes();
return this;
}
/**
* Convert the block to an object suitable
* for JSON serialization.
* @param {Network} [network]
* @param {CoinView} [view]
* @param {Number} [height]
* @returns {Object}
*/
getJSON(network, view, height) {
return {
hash: this.hash().toString('hex'),
height: height,
version: this.version,
prevBlock: this.prevBlock,
merkleRoot: this.merkleRoot,
witnessRoot: this.witnessRoot,
treeRoot: this.treeRoot,
reservedRoot: this.reservedRoot,
time: this.time,
bits: this.bits,
nonce: this.nonce,
extraNonce: this.extraNonce.toString('hex'),
mask: this.mask.toString('hex'),
totalTX: this.totalTX,
hashes: this.hashes.map((hash) => {
return hash.toString('hex');
}),
flags: this.flags.toString('hex')
};
}
/**
* Inject properties from json object.
* @param {Object} json
*/
fromJSON(json) {
assert(json, 'MerkleBlock data is required.');
assert(Array.isArray(json.hashes));
assert(typeof json.flags === 'string');
assert((json.totalTX >>> 0) === json.totalTX);
this.parseJSON(json);
for (const hash of json.hashes)
this.hashes.push(Buffer.from(hash, 'hex'));
this.flags = Buffer.from(json.flags, 'hex');
this.totalTX = json.totalTX;
return this;
}
/**
* Create a merkleblock from a {@link Block} object, passing
* it through a filter first. This will build the partial
* merkle tree.
* @param {Block} block
* @param {BloomFilter} filter
* @returns {MerkleBlock}
*/
static fromBlock(block, filter) {
const matches = [];
for (const tx of block.txs)
matches.push(tx.testAndMaybeUpdate(filter) ? 1 : 0);
return this.fromMatches(block, matches);
}
/**
* Create a merkleblock from an array of txids.
* This will build the partial merkle tree.
* @param {Block} block
* @param {Hash[]} hashes
* @returns {MerkleBlock}
*/
static fromHashes(block, hashes) {
const filter = new BufferSet();
for (const hash of hashes)
filter.add(hash);
const matches = [];
for (const tx of block.txs) {
const hash = tx.hash();
matches.push(filter.has(hash) ? 1 : 0);
}
return this.fromMatches(block, matches);
}
/**
* Create a merkleblock from an array of matches.
* This will build the partial merkle tree.
* @param {Block} block
* @param {Number[]} matches
* @returns {MerkleBlock}
*/
static fromMatches(block, matches) {
const txs = [];
const leaves = [];
const bits = [];
const hashes = [];
const totalTX = block.txs.length;
const sentinel = merkle.hashEmpty(blake2b);
let height = 0;
const width = (height) => {
return (totalTX + (1 << height) - 1) >>> height;
};
const hash = (height, pos, leaves) => {
if (height === 0)
return merkle.hashLeaf(blake2b, leaves[pos]);
const left = hash(height - 1, pos * 2, leaves);
let right;
if (pos * 2 + 1 < width(height - 1))
right = hash(height - 1, pos * 2 + 1, leaves);
else
right = sentinel;
return merkle.hashInternal(blake2b, left, right);
};
const traverse = (height, pos, leaves, matches) => {
let parent = 0;
for (let p = pos << height; p < ((pos + 1) << height) && p < totalTX; p++)
parent |= matches[p];
bits.push(parent);
if (height === 0 && parent) {
hashes.push(leaves[pos]);
return;
}
if (height === 0 || !parent) {
hashes.push(hash(height, pos, leaves));
return;
}
traverse(height - 1, pos * 2, leaves, matches);
if (pos * 2 + 1 < width(height - 1))
traverse(height - 1, pos * 2 + 1, leaves, matches);
};
for (let i = 0; i < block.txs.length; i++) {
const tx = block.txs[i];
if (matches[i])
txs.push(tx);
leaves.push(tx.hash());
}
while (width(height) > 1)
height += 1;
traverse(height, 0, leaves, matches);
const flags = Buffer.allocUnsafe((bits.length + 7) / 8 | 0);
flags.fill(0);
for (let p = 0; p < bits.length; p++)
flags[p / 8 | 0] |= bits[p] << (p % 8);
const mblock = new this();
mblock._hash = block._hash;
mblock.version = block.version;
mblock.prevBlock = block.prevBlock;
mblock.merkleRoot = block.merkleRoot;
mblock.witnessRoot = block.witnessRoot;
mblock.treeRoot = block.treeRoot;
mblock.reservedRoot = block.reservedRoot;
mblock.time = block.time;
mblock.bits = block.bits;
mblock.nonce = block.nonce;
mblock.extraNonce = block.extraNonce;
mblock.mask = block.mask;
mblock.totalTX = totalTX;
mblock.hashes = hashes;
mblock.flags = flags;
mblock.txs = txs;
return mblock;
}
/**
* Test whether an object is a MerkleBlock.
* @param {Object} obj
* @returns {Boolean}
*/
static isMerkleBlock(obj) {
return obj instanceof MerkleBlock;
}
/**
* Convert the block to a headers object.
* @returns {Headers}
*/
toHeaders() {
return Headers.fromBlock(this);
}
}
/*
* Helpers
*/
class PartialTree {
constructor(root, matches, indexes, map) {
this.root = root || consensus.ZERO_HASH;
this.matches = matches || [];
this.indexes = indexes || [];
this.map = map || new BufferMap();
}
}
/*
* Expose
*/
module.exports = MerkleBlock;

1493
docs/js-primitives/mtx.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,284 @@
/*!
* outpoint.js - outpoint object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const consensus = require('../protocol/consensus');
const util = require('../utils/util');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').HexHash} HexHash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./tx')} TX */
/**
* @typedef {Object} OutpointJSON
* @property {HexHash} hash
* @property {Number} index
*/
/**
* Outpoint
* Represents a COutPoint.
* @alias module:primitives.Outpoint
* @property {Hash} hash
* @property {Number} index
*/
class Outpoint extends bio.Struct {
/**
* Create an outpoint.
* @constructor
* @param {Hash?} [hash]
* @param {Number?} [index]
*/
constructor(hash, index) {
super();
this.hash = consensus.ZERO_HASH;
this.index = 0xffffffff;
if (hash != null) {
assert(Buffer.isBuffer(hash));
assert((index >>> 0) === index, 'Index must be a uint32.');
this.hash = hash;
this.index = index;
}
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
assert(options, 'Outpoint data is required.');
assert(Buffer.isBuffer(options.hash));
assert((options.index >>> 0) === options.index, 'Index must be a uint32.');
this.hash = options.hash;
this.index = options.index;
return this;
}
/**
* Clone the outpoint.
* @param {this} prevout
* @returns {this}
*/
inject(prevout) {
assert(prevout instanceof this.constructor);
this.hash = prevout.hash;
this.index = prevout.index;
return this;
}
/**
* Test equality against another outpoint.
* @param {this} prevout
* @returns {Boolean}
*/
equals(prevout) {
assert(prevout instanceof this.constructor);
return this.hash.equals(prevout.hash)
&& this.index === prevout.index;
}
/**
* Compare against another outpoint (BIP69).
* @param {this} prevout
* @returns {Number}
*/
compare(prevout) {
assert(prevout instanceof this.constructor);
const cmp = this.hash.compare(prevout.hash);
if (cmp !== 0)
return cmp;
return this.index - prevout.index;
}
/**
* Test whether the outpoint is null (hash of zeroes
* with max-u32 index). Used to detect coinbases.
* @returns {Boolean}
*/
isNull() {
return this.index === 0xffffffff && this.hash.equals(consensus.ZERO_HASH);
}
/**
* Get little-endian hash.
* @returns {HexHash}
*/
txid() {
return this.hash.toString('hex');
}
/**
* Serialize outpoint to a key
* suitable for a hash table.
* @returns {Buffer}
*/
toKey() {
return Outpoint.toKey(this.hash, this.index);
}
/**
* Inject properties from hash table key.
* @param {Buffer} key
* @returns {Outpoint}
*/
fromKey(key) {
assert(Buffer.isBuffer(key) && key.length === 36);
this.hash = key.slice(0, 32);
this.index = bio.readU32(key, 32);
return this;
}
/**
* Instantiate outpoint from hash table key.
* @param {Buffer} key
* @returns {Outpoint}
*/
static fromKey(key) {
return new this().fromKey(key);
}
/**
* Write outpoint to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeHash(this.hash);
bw.writeU32(this.index);
return bw;
}
/**
* Calculate size of outpoint.
* @returns {Number}
*/
getSize() {
return 36;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
*/
read(br) {
this.hash = br.readHash();
this.index = br.readU32();
return this;
}
/**
* Inject properties from json object.
* @param {OutpointJSON} json
*/
fromJSON(json) {
assert(json, 'Outpoint data is required.');
assert(json.hash, 'Hash is required.');
assert((json.index >>> 0) === json.index, 'Index must be a uint32.');
this.hash = util.parseHex(json.hash, 32);
this.index = json.index;
return this;
}
/**
* Convert the outpoint to an object suitable
* for JSON serialization.
* @returns {OutpointJSON}
*/
getJSON() {
return {
hash: this.hash.toString('hex'),
index: this.index
};
}
/**
* Inject properties from tx.
* @private
* @param {TX} tx
* @param {Number} index
*/
fromTX(tx, index) {
assert(tx);
assert((index >>> 0) === index);
this.hash = tx.hash();
this.index = index;
return this;
}
/**
* Instantiate outpoint from tx.
* @param {TX} tx
* @param {Number} index
* @returns {Outpoint}
*/
static fromTX(tx, index) {
return new this().fromTX(tx, index);
}
/**
* Serialize outpoint to a key
* suitable for a hash table.
* @param {Hash} hash
* @param {Number} index
* @returns {Buffer}
*/
static toKey(hash, index) {
return new Outpoint(hash, index).encode();
}
/**
* Convert the outpoint to a user-friendly string.
* @returns {String}
*/
format() {
return `<Outpoint: ${this.hash.toString('hex')}/${this.index}>`;
}
/**
* Test an object to see if it is an outpoint.
* @param {Object} obj
* @returns {Boolean}
*/
static isOutpoint(obj) {
return obj instanceof Outpoint;
}
}
/*
* Expose
*/
module.exports = Outpoint;

View file

@ -0,0 +1,307 @@
/*!
* output.js - output object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const Amount = require('../ui/amount');
const Network = require('../protocol/network');
const Address = require('../primitives/address');
const consensus = require('../protocol/consensus');
const policy = require('../protocol/policy');
const util = require('../utils/util');
const Covenant = require('./covenant');
/** @typedef {import('../types').Amount} AmountValue */
/** @typedef {import('../types').Rate} Rate */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./covenant').CovenantJSON} CovenantJSON */
/**
* @typedef {Object} OutputJSON
* @property {AmountValue} value
* @property {String} address
* @property {CovenantJSON} covenant
*/
/**
* Represents a transaction output.
* @alias module:primitives.Output
* @property {AmountValue} value
* @property {Address} address
*/
class Output extends bio.Struct {
/**
* Create an output.
* @constructor
* @param {Object?} [options]
*/
constructor(options) {
super();
this.value = 0;
this.address = new Address();
this.covenant = new Covenant();
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
assert(options, 'Output data is required.');
if (options.value != null) {
assert(util.isU64(options.value), 'Value must be a uint64.');
this.value = options.value;
}
if (options.address)
this.address.fromOptions(options.address);
if (options.covenant)
this.covenant.fromOptions(options.covenant);
return this;
}
/**
* Inject properties from address/value pair.
* @param {Address} address
* @param {AmountValue} value
* @returns {Output}
*/
fromScript(address, value) {
assert(util.isU64(value), 'Value must be a uint64.');
this.address = Address.fromOptions(address);
this.value = value;
return this;
}
/**
* Instantiate output from address/value pair.
* @param {Address} address
* @param {AmountValue} value
* @returns {Output}
*/
static fromScript(address, value) {
return new this().fromScript(address, value);
}
/**
* Clone the output.
* @param {this} output
* @returns {this}
*/
inject(output) {
assert(output instanceof this.constructor);
this.value = output.value;
this.address.inject(output.address);
this.covenant.inject(output.covenant);
return this;
}
/**
* Test equality against another output.
* @param {Output} output
* @returns {Boolean}
*/
equals(output) {
assert(output instanceof this.constructor);
return this.value === output.value
&& this.address.equals(output.address);
}
/**
* Compare against another output (BIP69).
* @param {Output} output
* @returns {Number}
*/
compare(output) {
assert(output instanceof this.constructor);
const cmp = this.value - output.value;
if (cmp !== 0)
return cmp;
return this.address.compare(output.address);
}
/**
* Get the address.
* @returns {Address} address
*/
getAddress() {
return this.address;
}
/**
* Get the address hash.
* @returns {Hash}
*/
getHash() {
return this.address.getHash();
}
/**
* Convert the input to a more user-friendly object.
* @returns {Object}
*/
format() {
return {
value: Amount.coin(this.value),
address: this.address,
covenant: this.covenant
};
}
/**
* Convert the output to an object suitable
* for JSON serialization.
* @param {Network} [network]
* @returns {OutputJSON}
*/
getJSON(network) {
network = Network.get(network);
return {
value: this.value,
address: this.address.toString(network),
covenant: this.covenant.toJSON()
};
}
/**
* Calculate the dust threshold for this
* output, based on serialize size and rate.
* @param {Rate?} [rate]
* @returns {AmountValue}
*/
getDustThreshold(rate) {
if (!this.covenant.isDustworthy())
return 0;
if (this.address.isUnspendable())
return 0;
const scale = consensus.WITNESS_SCALE_FACTOR;
let size = this.getSize();
size += 32 + 4 + 1 + (107 / scale | 0) + 4;
return 3 * policy.getMinFee(size, rate);
}
/**
* Calculate size of serialized output.
* @returns {Number}
*/
getSize() {
return 8 + this.address.getSize() + this.covenant.getVarSize();
}
/**
* Test whether the output should be considered dust.
* @param {Rate?} [rate]
* @returns {Boolean}
*/
isDust(rate) {
return this.value < this.getDustThreshold(rate);
}
/**
* Test whether the output is unspendable.
* @returns {Boolean}
*/
isUnspendable() {
return this.address.isUnspendable() || this.covenant.isUnspendable();
}
/**
* Inject properties from a JSON object.
* @param {OutputJSON} json
*/
fromJSON(json) {
assert(json, 'Output data is required.');
assert(util.isU64(json.value), 'Value must be a uint64.');
this.value = json.value;
this.address.fromString(json.address);
if (json.covenant != null)
this.covenant.fromJSON(json.covenant);
return this;
}
/**
* Write the output to a buffer writer.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU64(this.value);
this.address.write(bw);
this.covenant.write(bw);
return bw;
}
/**
* Inject properties from buffer reader.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.value = br.readU64();
this.address.read(br);
this.covenant.read(br);
return this;
}
/**
* Test an object to see if it is an Output.
* @param {Object} obj
* @returns {Boolean}
*/
static isOutput(obj) {
return obj instanceof Output;
}
}
/*
* Expose
*/
module.exports = Output;

2096
docs/js-primitives/tx.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,265 @@
/*!
* txmeta.js - extended transaction object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const util = require('../utils/util');
const TX = require('./tx');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
/** @typedef {import('../coins/coinview')} CoinView */
/** @typedef {import('../protocol/network')} Network */
/**
* TXMeta
* An extended transaction object.
* @alias module:primitives.TXMeta
*/
class TXMeta extends bio.Struct {
/**
* Create an extended transaction.
* @constructor
* @param {Object?} [options]
*/
constructor(options) {
super();
this.tx = new TX();
this.mtime = util.now();
this.height = -1;
/** @type {Hash} */
this.block = null;
this.time = 0;
this.index = -1;
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
*/
fromOptions(options) {
if (options.tx) {
assert(options.tx instanceof TX);
this.tx = options.tx;
}
if (options.mtime != null) {
assert(util.isU64(options.mtime));
this.mtime = options.mtime;
}
if (options.height != null) {
assert(options.height === -1
|| (options.height >>> 0) === options.height);
this.height = options.height;
}
if (options.block !== undefined) {
assert(options.block === null || Buffer.isBuffer(options.block));
this.block = options.block;
}
if (options.time != null) {
assert(util.isU64(options.time));
this.time = options.time;
}
if (options.index != null) {
assert(options.index === -1 || (options.index >>> 0) === options.index);
this.index = options.index;
}
return this;
}
/**
* Inject properties from options object.
* @param {TX} tx
* @param {ChainEntry} entry
* @param {Number} index
*/
fromTX(tx, entry, index) {
this.tx = tx;
if (entry) {
this.height = entry.height;
this.block = entry.hash;
this.time = entry.time;
this.index = index;
}
return this;
}
/**
* Instantiate TXMeta from options.
* @param {TX} tx
* @param {ChainEntry} entry
* @param {Number} index
* @returns {TXMeta}
*/
static fromTX(tx, entry, index) {
return new this().fromTX(tx, entry, index);
}
/**
* Inspect the transaction.
* @param {CoinView} view
* @returns {Object}
*/
format(view) {
const data = this.tx.format(view, null, this.index);
data.mtime = this.mtime;
data.height = this.height;
data.block = this.block ? this.block.toString('hex') : null;
data.time = this.time;
return data;
}
/**
* Convert the transaction to an object suitable
* for JSON serialization.
* @param {Network} [network]
* @param {CoinView} [view]
* @param {Number} [chainHeight]
* @returns {Object}
*/
getJSON(network, view, chainHeight) {
const json = this.tx.getJSON(network, view, null, this.index);
json.mtime = this.mtime;
json.height = this.height;
json.block = this.block ? this.block.toString('hex') : null;
json.time = this.time;
json.confirmations = 0;
if (chainHeight != null && this.height !== -1)
json.confirmations = chainHeight - this.height + 1;
return json;
}
/**
* Inject properties from a json object.
* @param {Object} json
*/
fromJSON(json) {
this.tx.fromJSON(json);
assert(util.isU64(json.mtime));
assert(json.height === -1 || (json.height >>> 0) === json.height);
assert(!json.block || typeof json.block === 'string');
assert(util.isU64(json.time));
assert(json.index === -1 || (json.index >>> 0) === json.index);
this.mtime = json.mtime;
this.height = json.height;
this.block = json.block ? util.parseHex(json.block, 32) : null;
this.index = json.index;
return this;
}
/**
* Calculate serialization size.
* @returns {Number}
*/
getSize() {
let size = 0;
size += this.tx.getSize();
size += 4;
if (this.block) {
size += 1;
size += 32;
size += 4 * 3;
} else {
size += 1;
}
return size;
}
/**
* Serialize a transaction to "extended format".
* This is the serialization format we use internally
* to store transactions in the database. The extended
* serialization includes the height, block hash, index,
* timestamp, and pending-since time.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
this.tx.write(bw);
bw.writeU32(this.mtime);
if (this.block) {
bw.writeU8(1);
bw.writeHash(this.block);
bw.writeU32(this.height);
bw.writeU32(this.time);
bw.writeU32(this.index);
} else {
bw.writeU8(0);
}
return bw;
}
/**
* Inject properties from "extended" serialization format.
* @param {bio.BufferReader} br
*/
read(br) {
this.tx.read(br);
this.mtime = br.readU32();
if (br.readU8() === 1) {
this.block = br.readHash();
this.height = br.readU32();
this.time = br.readU32();
this.index = br.readU32();
if (this.index === 0xffffffff)
this.index = -1;
}
return this;
}
/**
* Test whether an object is an TXMeta.
* @param {Object} obj
* @returns {Boolean}
*/
static isTXMeta(obj) {
return obj instanceof TXMeta;
}
}
/*
* Expose
*/
module.exports = TXMeta;

904
docs/js-wallet/account.js Normal file
View file

@ -0,0 +1,904 @@
/*!
* account.js - account object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const binary = require('../utils/binary');
const Path = require('./path');
const common = require('./common');
const Script = require('../script/script');
const WalletKey = require('./walletkey');
const HDPublicKey = require('../hd/public');
/** @typedef {import('bdb').DB} DB */
/** @typedef {ReturnType<DB['batch']>} Batch */
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('./walletdb')} WalletDB */
/** @typedef {import('./masterkey')} MasterKey */
/** @typedef {import('../primitives/address')} Address */
/**
* Account
* Represents a BIP44 Account belonging to a {@link Wallet}.
* Note that this object does not enforce locks. Any method
* that does a write is internal API only and will lead
* to race conditions if used elsewhere.
* @alias module:wallet.Account
*/
class Account extends bio.Struct {
/**
* Create an account.
* @constructor
* @param {WalletDB} wdb
* @param {Object} options
*/
constructor(wdb, options) {
super();
assert(wdb, 'Database is required.');
/** @type {WalletDB} */
this.wdb = wdb;
this.network = wdb.network;
this.wid = 0;
/** @type {String|null} */
this.id = null;
this.accountIndex = 0;
/** @type {String|null} */
this.name = null;
this.initialized = false;
this.watchOnly = false;
/** @type {Account.types} */
this.type = Account.types.PUBKEYHASH;
this.m = 1;
this.n = 1;
this.receiveDepth = 0;
this.changeDepth = 0;
this.lookahead = 200;
/** @type {HDPublicKey|null} */
this.accountKey = null;
/** @type {HDPublicKey[]} */
this.keys = [];
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
* @returns {this}
*/
fromOptions(options) {
assert(options, 'Options are required.');
assert((options.wid >>> 0) === options.wid);
assert(common.isName(options.id), 'Bad Wallet ID.');
assert(HDPublicKey.isHDPublicKey(options.accountKey),
'Account key is required.');
assert((options.accountIndex >>> 0) === options.accountIndex,
'Account index is required.');
this.wid = options.wid;
this.id = options.id;
if (options.accountIndex != null) {
assert((options.accountIndex >>> 0) === options.accountIndex);
this.accountIndex = options.accountIndex;
}
if (options.name != null) {
assert(common.isName(options.name), 'Bad account name.');
this.name = options.name;
}
if (options.initialized != null) {
assert(typeof options.initialized === 'boolean');
this.initialized = options.initialized;
}
if (options.watchOnly != null) {
assert(typeof options.watchOnly === 'boolean');
this.watchOnly = options.watchOnly;
}
if (options.type != null) {
if (typeof options.type === 'string') {
this.type = Account.types[options.type.toUpperCase()];
assert(this.type != null);
} else {
assert(typeof options.type === 'number');
this.type = options.type;
assert(Account.typesByVal[this.type]);
}
}
if (options.m != null) {
assert((options.m & 0xff) === options.m);
this.m = options.m;
}
if (options.n != null) {
assert((options.n & 0xff) === options.n);
this.n = options.n;
}
if (options.receiveDepth != null) {
assert((options.receiveDepth >>> 0) === options.receiveDepth);
this.receiveDepth = options.receiveDepth;
}
if (options.changeDepth != null) {
assert((options.changeDepth >>> 0) === options.changeDepth);
this.changeDepth = options.changeDepth;
}
if (options.lookahead != null) {
assert((options.lookahead >>> 0) === options.lookahead);
assert(options.lookahead >= 0);
this.lookahead = options.lookahead;
}
this.accountKey = options.accountKey;
if (this.n > 1)
this.type = Account.types.MULTISIG;
if (!this.name)
this.name = this.accountIndex.toString(10);
if (this.m < 1 || this.m > this.n)
throw new Error('m ranges between 1 and n');
if (options.keys) {
assert(Array.isArray(options.keys));
for (const key of options.keys)
this.pushKey(key);
}
return this;
}
/**
* Inject properties from options object.
* @param {WalletDB} wdb
* @param {Object} options
*/
static fromOptions(wdb, options) {
return new this(wdb).fromOptions(options);
}
/**
* Attempt to intialize the account (generating
* the first addresses along with the lookahead
* addresses). Called automatically from the
* walletdb.
* @param {Batch} b
* @returns {Promise}
*/
async init(b) {
// Waiting for more keys.
if (this.keys.length !== this.n - 1) {
assert(!this.initialized);
this.save(b);
return;
}
assert(this.receiveDepth === 0);
assert(this.changeDepth === 0);
this.initialized = true;
await this.initDepth(b);
}
/**
* Add a public account key to the account (multisig).
* Does not update the database.
* @param {HDPublicKey} key - Account (bip44)
* key (can be in base58 form).
* @throws Error on non-hdkey/non-accountkey.
* @returns {Boolean} - Whether the key was added.
*/
pushKey(key) {
if (typeof key === 'string')
key = HDPublicKey.fromBase58(key, this.network);
if (!HDPublicKey.isHDPublicKey(key))
throw new Error('Must add HD keys to wallet.');
if (!key.isAccount())
throw new Error('Must add HD account keys to BIP44 wallet.');
if (this.type !== Account.types.MULTISIG)
throw new Error('Cannot add keys to non-multisig wallet.');
if (key.equals(this.accountKey))
throw new Error('Cannot add own key.');
const index = binary.insert(this.keys, key, cmp, true);
if (index === -1)
return false;
if (this.keys.length > this.n - 1) {
binary.remove(this.keys, key, cmp);
throw new Error('Cannot add more keys.');
}
return true;
}
/**
* Remove a public account key to the account (multisig).
* Does not update the database.
* @param {HDPublicKey} key - Account (bip44)
* key (can be in base58 form).
* @throws Error on non-hdkey/non-accountkey.
* @returns {Boolean} - Whether the key was removed.
*/
spliceKey(key) {
if (typeof key === 'string')
key = HDPublicKey.fromBase58(key, this.network);
if (!HDPublicKey.isHDPublicKey(key))
throw new Error('Must add HD keys to wallet.');
if (!key.isAccount())
throw new Error('Must add HD account keys to BIP44 wallet.');
if (this.type !== Account.types.MULTISIG)
throw new Error('Cannot remove keys from non-multisig wallet.');
if (this.keys.length === this.n - 1)
throw new Error('Cannot remove key.');
return binary.remove(this.keys, key, cmp);
}
/**
* Add a public account key to the account (multisig).
* Saves the key in the wallet database.
* @param {Batch} b
* @param {HDPublicKey} key
* @returns {Promise<Boolean>}
*/
async addSharedKey(b, key) {
const result = this.pushKey(key);
if (await this.hasDuplicate()) {
this.spliceKey(key);
throw new Error('Cannot add a key from another account.');
}
// Try to initialize again.
await this.init(b);
return result;
}
/**
* Ensure accounts are not sharing keys.
* @private
* @returns {Promise<Boolean>}
*/
async hasDuplicate() {
if (this.keys.length !== this.n - 1)
return false;
const ring = this.deriveReceive(0);
const hash = ring.getScriptHash();
return this.wdb.hasPath(this.wid, hash);
}
/**
* Remove a public account key from the account (multisig).
* Remove the key from the wallet database.
* @param {Batch} b
* @param {HDPublicKey} key
* @returns {Boolean}
*/
removeSharedKey(b, key) {
const result = this.spliceKey(key);
if (!result)
return false;
this.save(b);
return true;
}
/**
* Create a new receiving address (increments receiveDepth).
* @param {Batch} b
* @returns {Promise<WalletKey>}
*/
createReceive(b) {
return this.createKey(b, 0);
}
/**
* Create a new change address (increments changeDepth).
* @param {Batch} b
* @returns {Promise<WalletKey>}
*/
createChange(b) {
return this.createKey(b, 1);
}
/**
* Create a new address (increments depth).
* @param {Batch} b
* @param {Number} branch
* @returns {Promise<WalletKey>} - Returns {@link WalletKey}.
*/
async createKey(b, branch) {
let key, lookahead;
switch (branch) {
case 0:
key = this.deriveReceive(this.receiveDepth);
lookahead = this.deriveReceive(this.receiveDepth + this.lookahead);
await this.saveKey(b, lookahead);
this.receiveDepth += 1;
this.receive = key;
break;
case 1:
key = this.deriveChange(this.changeDepth);
lookahead = this.deriveChange(this.changeDepth + this.lookahead);
await this.saveKey(b, lookahead);
this.changeDepth += 1;
this.change = key;
break;
default:
throw new Error(`Bad branch: ${branch}.`);
}
this.save(b);
return key;
}
/**
* Derive a receiving address at `index`. Do not increment depth.
* @param {Number} index
* @param {MasterKey} [master]
* @returns {WalletKey}
*/
deriveReceive(index, master) {
return this.deriveKey(0, index, master);
}
/**
* Derive a change address at `index`. Do not increment depth.
* @param {Number} index
* @param {MasterKey} [master]
* @returns {WalletKey}
*/
deriveChange(index, master) {
return this.deriveKey(1, index, master);
}
/**
* Derive an address from `path` object.
* @param {Path} path
* @param {MasterKey} master
* @returns {WalletKey?}
*/
derivePath(path, master) {
switch (path.keyType) {
case Path.types.HD: {
return this.deriveKey(path.branch, path.index, master);
}
case Path.types.KEY: {
assert(this.type === Account.types.PUBKEYHASH);
let data = path.data;
if (path.encrypted) {
data = master.decipher(data, path.hash);
if (!data)
return null;
}
return WalletKey.fromImport(this, data);
}
case Path.types.ADDRESS: {
return null;
}
default: {
throw new Error('Bad key type.');
}
}
}
/**
* Derive an address at `index`. Do not increment depth.
* @param {Number} branch
* @param {Number} index
* @param {MasterKey} [master]
* @returns {WalletKey}
*/
deriveKey(branch, index, master) {
assert(typeof branch === 'number');
const keys = [];
let key;
if (master && master.key && !this.watchOnly) {
const type = this.network.keyPrefix.coinType;
key = master.key.deriveAccount(44, type, this.accountIndex);
key = key.derive(branch).derive(index);
} else {
key = this.accountKey.derive(branch).derive(index);
}
const ring = WalletKey.fromHD(this, key, branch, index);
switch (this.type) {
case Account.types.PUBKEYHASH:
break;
case Account.types.MULTISIG:
keys.push(key.publicKey);
for (const shared of this.keys) {
const key = shared.derive(branch).derive(index);
keys.push(key.publicKey);
}
ring.script = Script.fromMultisig(this.m, this.n, keys);
break;
}
return ring;
}
/**
* Save the account to the database. Necessary
* when address depth and keys change.
* @param {Batch} b
* @returns {void}
*/
save(b) {
return this.wdb.saveAccount(b, this);
}
/**
* Save addresses to path map.
* @param {Batch} b
* @param {WalletKey} ring
* @returns {Promise}
*/
saveKey(b, ring) {
return this.wdb.saveKey(b, this.wid, ring);
}
/**
* Save paths to path map.
* @param {Batch} b
* @param {Path} path
* @returns {Promise}
*/
savePath(b, path) {
return this.wdb.savePath(b, this.wid, path);
}
/**
* Initialize address depths (including lookahead).
* @param {Batch} b
* @returns {Promise}
*/
async initDepth(b) {
// Receive Address
this.receiveDepth = 1;
for (let i = 0; i <= this.lookahead; i++) {
const key = this.deriveReceive(i);
await this.saveKey(b, key);
}
// Change Address
this.changeDepth = 1;
for (let i = 0; i <= this.lookahead; i++) {
const key = this.deriveChange(i);
await this.saveKey(b, key);
}
this.save(b);
}
/**
* Allocate new lookahead addresses if necessary.
* @param {Batch} b
* @param {Number} receive
* @param {Number} change
* @returns {Promise<WalletKey?>}
*/
async syncDepth(b, receive, change) {
let derived = false;
let result = null;
if (receive > this.receiveDepth) {
const depth = this.receiveDepth + this.lookahead;
assert(receive <= depth + 1);
for (let i = depth; i < receive + this.lookahead; i++) {
const key = this.deriveReceive(i);
await this.saveKey(b, key);
result = key;
}
this.receiveDepth = receive;
derived = true;
}
if (change > this.changeDepth) {
const depth = this.changeDepth + this.lookahead;
assert(change <= depth + 1);
for (let i = depth; i < change + this.lookahead; i++) {
const key = this.deriveChange(i);
await this.saveKey(b, key);
}
this.changeDepth = change;
derived = true;
}
if (derived)
this.save(b);
return result;
}
/**
* Allocate new lookahead addresses.
* @param {Batch} b
* @param {Number} lookahead
* @returns {Promise}
*/
async setLookahead(b, lookahead) {
assert((lookahead >>> 0) === lookahead, 'Lookahead must be a number.');
if (lookahead === this.lookahead)
return;
if (lookahead < this.lookahead) {
const diff = this.lookahead - lookahead;
this.receiveDepth += diff;
this.changeDepth += diff;
this.lookahead = lookahead;
this.save(b);
return;
}
{
const depth = this.receiveDepth + this.lookahead;
const target = this.receiveDepth + lookahead;
for (let i = depth; i < target; i++) {
const key = this.deriveReceive(i);
await this.saveKey(b, key);
}
}
{
const depth = this.changeDepth + this.lookahead;
const target = this.changeDepth + lookahead;
for (let i = depth; i < target; i++) {
const key = this.deriveChange(i);
await this.saveKey(b, key);
}
}
this.lookahead = lookahead;
this.save(b);
}
/**
* Get current receive key.
* @returns {WalletKey?}
*/
receiveKey() {
if (!this.initialized)
return null;
return this.deriveReceive(this.receiveDepth - 1);
}
/**
* Get current change key.
* @returns {WalletKey?}
*/
changeKey() {
if (!this.initialized)
return null;
return this.deriveChange(this.changeDepth - 1);
}
/**
* Get current receive address.
* @returns {Address?}
*/
receiveAddress() {
const key = this.receiveKey();
if (!key)
return null;
return key.getAddress();
}
/**
* Get current change address.
* @returns {Address?}
*/
changeAddress() {
const key = this.changeKey();
if (!key)
return null;
return key.getAddress();
}
/**
* Convert the account to a more inspection-friendly object.
* @returns {Object}
*/
format() {
const receive = this.receiveAddress();
const change = this.changeAddress();
return {
id: this.id,
wid: this.wid,
name: this.name,
network: this.network.type,
initialized: this.initialized,
watchOnly: this.watchOnly,
type: Account.typesByVal[this.type].toLowerCase(),
m: this.m,
n: this.n,
accountIndex: this.accountIndex,
receiveDepth: this.receiveDepth,
changeDepth: this.changeDepth,
lookahead: this.lookahead,
receiveAddress: receive ? receive.toString(this.network) : null,
changeAddress: change ? change.toString(this.network) : null,
accountKey: this.accountKey.toBase58(this.network),
keys: this.keys.map(key => key.toBase58(this.network))
};
}
/**
* Convert the account to an object suitable for
* serialization.
* @param {Object} [balance=null]
* @returns {Object}
*/
getJSON(balance) {
const receive = this.receiveAddress();
const change = this.changeAddress();
return {
name: this.name,
initialized: this.initialized,
watchOnly: this.watchOnly,
type: Account.typesByVal[this.type].toLowerCase(),
m: this.m,
n: this.n,
accountIndex: this.accountIndex,
receiveDepth: this.receiveDepth,
changeDepth: this.changeDepth,
lookahead: this.lookahead,
receiveAddress: receive ? receive.toString(this.network) : null,
changeAddress: change ? change.toString(this.network) : null,
accountKey: this.accountKey.toBase58(this.network),
keys: this.keys.map(key => key.toBase58(this.network)),
balance: balance ? balance.toJSON(true) : null
};
}
/**
* Calculate serialization size.
* @returns {Number}
*/
getSize() {
let size = 0;
size += 91;
size += this.keys.length * 74;
return size;
}
/**
* Serialize the account.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
let flags = 0;
if (this.initialized)
flags |= 1;
bw.writeU8(flags);
bw.writeU8(this.type);
bw.writeU8(this.m);
bw.writeU8(this.n);
bw.writeU32(this.receiveDepth);
bw.writeU32(this.changeDepth);
bw.writeU32(this.lookahead);
writeKey(this.accountKey, bw);
bw.writeU8(this.keys.length);
for (const key of this.keys)
writeKey(key, bw);
return bw;
}
/**
* Inject properties from serialized data.
* @param {bio.BufferReader} br
*/
read(br) {
const flags = br.readU8();
this.initialized = (flags & 1) !== 0;
this.type = br.readU8();
this.m = br.readU8();
this.n = br.readU8();
this.receiveDepth = br.readU32();
this.changeDepth = br.readU32();
this.lookahead = br.readU32();
this.accountKey = readKey(br);
assert(this.type < Account.typesByVal.length);
const count = br.readU8();
for (let i = 0; i < count; i++) {
const key = readKey(br);
binary.insert(this.keys, key, cmp, true);
}
return this;
}
/**
* Decode account.
* @param {WalletDB} wdb
* @param {Buffer} data
* @returns {Account}
*/
static decode(wdb, data) {
return new this(wdb).decode(data);
}
/**
* Test an object to see if it is a Account.
* @param {Object} obj
* @returns {Boolean}
*/
static isAccount(obj) {
return obj instanceof Account;
}
}
/**
* Account types.
* @enum {Number}
* @default
*/
Account.types = {
PUBKEYHASH: 0,
MULTISIG: 1
};
/**
* Account types by value.
* @const {Object}
*/
Account.typesByVal = [
'PUBKEYHASH',
'MULTISIG'
];
/*
* Helpers
*/
function cmp(a, b) {
return a.compare(b);
}
/**
* @param {HDPublicKey} key
* @param {BufioWriter} bw
* @returns {void}
*/
function writeKey(key, bw) {
bw.writeU8(key.depth);
bw.writeU32BE(key.parentFingerPrint);
bw.writeU32BE(key.childIndex);
bw.writeBytes(key.chainCode);
bw.writeBytes(key.publicKey);
}
/**
* @param {bio.BufferReader} br
* @returns {HDPublicKey}
*/
function readKey(br) {
const key = new HDPublicKey();
key.depth = br.readU8();
key.parentFingerPrint = br.readU32BE();
key.childIndex = br.readU32BE();
key.chainCode = br.readBytes(32);
key.publicKey = br.readBytes(33);
return key;
}
/*
* Expose
*/
module.exports = Account;

186
docs/js-wallet/client.js Normal file
View file

@ -0,0 +1,186 @@
/*!
* client.js - http client for wallets
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const NodeClient = require('../client/node');
const TX = require('../primitives/tx');
const Coin = require('../primitives/coin');
const NameState = require('../covenants/namestate');
const {encoding} = require('bufio');
const parsers = {
'block connect': (entry, txs) => parseBlock(entry, txs),
'block disconnect': entry => [parseEntry(entry)],
'block rescan': (entry, txs) => parseBlock(entry, txs),
'block rescan interactive': (entry, txs) => parseBlock(entry, txs),
'chain reset': entry => [parseEntry(entry)],
'tx': tx => [TX.decode(tx)]
};
class WalletClient extends NodeClient {
constructor(options) {
super(options);
}
bind(event, handler) {
const parser = parsers[event];
if (!parser) {
super.bind(event, handler);
return;
}
super.bind(event, (...args) => {
return handler(...parser(...args));
});
}
hook(event, handler) {
const parser = parsers[event];
if (!parser) {
super.hook(event, handler);
return;
}
super.hook(event, (...args) => {
return handler(...parser(...args));
});
}
async getTip() {
return parseEntry(await super.getTip());
}
async getEntry(block) {
if (Buffer.isBuffer(block))
block = block.toString('hex');
return parseEntry(await super.getEntry(block));
}
/**
* Get entries.
* @param {Number} [start=-1]
* @param {Number} [end=-1]
* @returns {Promise<Object[]>}
*/
async getEntries(start, end) {
const entries = await super.getEntries(start, end);
return entries.map(parseEntry);
}
async send(tx) {
return super.send(tx.encode());
}
async sendClaim(claim) {
return super.sendClaim(claim.encode());
}
async setFilter(filter) {
return super.setFilter(filter.encode());
}
/**
* Rescan for any missed transactions.
* @param {Number|Hash} start - Start block.
* @returns {Promise}
*/
async rescan(start) {
return super.rescan(start);
}
/**
* Rescan interactive for any missed transactions.
* @param {Number|Hash} start - Start block.
* @param {Boolean} [fullLock=false]
* @returns {Promise}
*/
async rescanInteractive(start, fullLock) {
return super.rescanInteractive(start, null, fullLock);
}
async getNameStatus(nameHash) {
const json = await super.getNameStatus(nameHash);
return NameState.fromJSON(json);
}
/**
* @param {Hash} hash
* @param {Number} index
* @returns {Promise<Coin>}
*/
async getCoin(hash, index) {
const json = super.getCoin(hash, index);
return Coin.fromJSON(json);
}
}
/*
* Helpers
*/
function parseEntry(data) {
if (!data)
return null;
// 32 hash
// 4 height
// 4 nonce
// 8 time
// 32 prev
// 32 tree
// 24 extranonce
// 32 reserved
// 32 witness
// 32 merkle
// 4 version
// 4 bits
// 32 mask
// 32 chainwork
// 304 TOTAL
assert(Buffer.isBuffer(data));
// Just enough to read the three data below
assert(data.length >= 80);
const hash = data.slice(0, 32);
const height = encoding.readU32(data, 32);
// skip nonce 4.
const time = encoding.readU64(data, 40);
const prevBlock = data.slice(48, 80);
return {
hash,
height,
time,
prevBlock
};
}
function parseBlock(entry, txs) {
const block = parseEntry(entry);
assert(block);
const out = [];
for (const tx of txs)
out.push(TX.decode(tx));
return [block, out];
}
/*
* Expose
*/
module.exports = WalletClient;

164
docs/js-wallet/common.js Normal file
View file

@ -0,0 +1,164 @@
/*!
* common.js - commonly required functions for wallet.
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const {BufferMap} = require('buffer-map');
/** @typedef {import('../primitives/tx')} TX */
/** @typedef {import('../primitives/txmeta')} TXMeta */
/** @typedef {import('../primitives/coin')} Coin */
/**
* @exports wallet/common
*/
const common = exports;
/**
* Test whether a string is eligible
* to be used as a name or ID.
* @param {String} key
* @returns {Boolean}
*/
common.isName = function isName(key) {
if (typeof key !== 'string')
return false;
if (key.length === 0)
return false;
if (!/^[\-\._0-9A-Za-z]+$/.test(key))
return false;
// Prevents __proto__
// from being used.
switch (key[0]) {
case '_':
case '-':
case '.':
return false;
}
switch (key[key.length - 1]) {
case '_':
case '-':
case '.':
return false;
}
return key.length >= 1 && key.length <= 40;
};
/**
* Sort an array of transactions by time.
* @param {TXMeta[]} txs
* @returns {TXMeta[]}
*/
common.sortTX = function sortTX(txs) {
return txs.sort((a, b) => {
return a.mtime - b.mtime;
});
};
/**
* Sort an array of coins by height.
* @param {Coin[]} coins
* @returns {Coin[]}
*/
common.sortCoins = function sortCoins(coins) {
return coins.sort((a, b) => {
const ah = a.height === -1 ? 0x7fffffff : a.height;
const bh = b.height === -1 ? 0x7fffffff : b.height;
return ah - bh;
});
};
/**
* Sort an array of transactions in dependency order.
* @param {TX[]} txs
* @returns {TX[]}
*/
common.sortDeps = function sortDeps(txs) {
const map = new BufferMap();
for (const tx of txs) {
const hash = tx.hash();
map.set(hash, tx);
}
const depMap = new BufferMap();
const depCount = new BufferMap();
const top = [];
for (const [hash, tx] of map) {
depCount.set(hash, 0);
let hasDeps = false;
for (const input of tx.inputs) {
const prev = input.prevout.hash;
if (!map.has(prev))
continue;
const count = depCount.get(hash);
depCount.set(hash, count + 1);
hasDeps = true;
if (!depMap.has(prev))
depMap.set(prev, []);
depMap.get(prev).push(tx);
}
if (hasDeps)
continue;
top.push(tx);
}
const result = [];
for (const tx of top) {
const hash = tx.hash();
const deps = depMap.get(hash);
result.push(tx);
if (!deps)
continue;
for (const tx of deps) {
const hash = tx.hash();
let count = depCount.get(hash);
if (--count === 0)
top.push(tx);
depCount.set(hash, count);
}
}
return result;
};
/**
* Wallet coin selection types.
* @enum {String}
*/
common.coinSelectionTypes = {
DB_ALL: 'db-all',
DB_VALUE: 'db-value',
DB_SWEEPDUST: 'db-sweepdust',
DB_AGE: 'db-age'
};

2004
docs/js-wallet/http.js Normal file

File diff suppressed because it is too large Load diff

28
docs/js-wallet/index.js Normal file
View file

@ -0,0 +1,28 @@
/*!
* wallet/index.js - wallet for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/**
* @module wallet
*/
exports.Account = require('./account');
exports.Client = require('./client');
exports.common = require('./common');
exports.HTTP = require('./http');
exports.layout = require('./layout');
exports.MasterKey = require('./masterkey');
exports.NodeClient = require('./nodeclient');
exports.Path = require('./path');
exports.plugin = require('./plugin');
exports.records = require('./records');
exports.RPC = require('./rpc');
exports.Node = require('./node');
exports.TXDB = require('./txdb');
exports.WalletDB = require('./walletdb');
exports.Wallet = require('./wallet');
exports.WalletKey = require('./walletkey');

229
docs/js-wallet/layout.js Normal file
View file

@ -0,0 +1,229 @@
/*!
* layout.js - data layout for wallets
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const bdb = require('bdb');
/*
* Wallet Database Layout:
* WDB State
* ---------
* V -> db version
* O -> flags
* D -> wallet id depth
* M -> migration state
*
* Chain Sync
* ----------
* R -> chain sync state
* h[height] -> block hash
*
* WID mappings
* --------
* b[height] -> block->wid map
* T[tx-hash] -> tx->wid map
* o[tx-hash][index] -> outpoint->wid map
* p[addr-hash] -> address->wid map
* N[name-hash] -> name->wid map
*
* Wallet
* ------
* l[id] -> wid
* w[wid] -> wallet
* W[wid] -> wallet id
*
* Wallet Account
* --------------
* a[wid][index] -> account
* i[wid][name] -> account index
* n[wid][index] -> account name
*
* Wallet Path
* -----------
* P[wid][addr-hash] -> path data
* r[wid][index][addr-hash] -> dummy (addr by account)
*
* TXDB
* ----
* t[wid]* -> txdb
*/
exports.wdb = {
// WDB State
V: bdb.key('V'),
O: bdb.key('O'),
D: bdb.key('D'),
M: bdb.key('M'),
// Chain Sync
R: bdb.key('R'),
h: bdb.key('h', ['uint32']),
// WID Mappings
b: bdb.key('b', ['uint32']),
T: bdb.key('T', ['hash256']),
p: bdb.key('p', ['hash']),
o: bdb.key('o', ['hash256', 'uint32']),
N: bdb.key('N', ['hash256']),
// Wallet
l: bdb.key('l', ['ascii']),
w: bdb.key('w', ['uint32']),
W: bdb.key('W', ['uint32']),
// Wallet Account
a: bdb.key('a', ['uint32', 'uint32']),
i: bdb.key('i', ['uint32', 'ascii']),
n: bdb.key('n', ['uint32', 'uint32']),
// Wallet Path
P: bdb.key('P', ['uint32', 'hash']),
r: bdb.key('r', ['uint32', 'uint32', 'hash']),
// TXDB
t: bdb.key('t', ['uint32'])
};
/*
* TXDB Database Layout:
* Balance
* -------
* R -> wallet balance
* r[account] -> account balance
*
* Coin
* ----
* c[tx-hash][index] -> coin
* C[account][tx-hash][index] -> dummy (coin by account)
* d[tx-hash][index] -> undo coin
* s[tx-hash][index] -> spent by hash
*
* Transaction
* -----------
* t[tx-hash] -> extended tx
* T[account][tx-hash] -> dummy (tx by account)
*
* Confirmed
* ---------
* b[height] -> block record
* h[height][tx-hash] -> dummy (tx by height)
* H[account][height][tx-hash] -> dummy (tx by height + account)
*
* Unconfirmed
* -----------
* p[hash] -> dummy (pending tx)
* P[account][tx-hash] -> dummy (pending tx by account)
*
* Coin Selection
* --------------
* Sv[value][tx-hash][index] -> dummy (confirmed coins by Value)
* SV[account][value][tx-hash][index] -> dummy
* (confirmed coins by account + Value)
*
* Su[value][tx-hash][index] -> dummy (Unconfirmed coins by value)
* SU[account][value][tx-hash][index] -> dummy
* (Unconfirmed coins by account + value)
*
* Sh[tx-hash][index] -> dummy (coins by account + Height)
* SH[account][height][tx-hash][index] -> dummy
* (coins by account + Height)
*
* Count and Time Index
* --------------------
* Ol - Latest Unconfirmed Index
* Oc[hash] - count (count for tx)
* Ou[hash] - undo count (unconfirmed count for tx)
* Ot[height][index] -> tx hash (tx by count)
* OT[account][height][index] -> tx hash (tx by count + account)
*
* Count and Time Index - Confirmed
* --------------------
* Oi[time][height][index][hash] -> dummy (tx by time)
* OI[account][time][height][index][hash] -> dummy (tx by time + account)
*
* Count and Time Index - Unconfirmed
* --------------------
* Om[time][count][hash] -> dummy (tx by time)
* OM[account][time][count][hash] -> dummy (tx by time + account)
* Oe[hash] -> undo time (unconfirmed time for tx)
*
* Names
* -----
* A[name-hash] -> name record (name record by name hash)
* U[tx-hash] -> name undo record (name undo record by tx hash)
* i[name-hash][tx-hash][index] -> bid (BlindBid by name + tx + index)
* B[name-hash][tx-hash][index] -> reveal (BidReveal by name + tx + index)
* E[name-hash][tx-hash][index] - bid to reveal out (by bid txhash + index)
* v[blind-hash] -> blind (Blind Value by blind hash)
* o[name-hash] -> tx hash OPEN only (tx hash by name hash)
*/
exports.txdb = {
prefix: bdb.key('t', ['uint32']),
// Balance
R: bdb.key('R'),
r: bdb.key('r', ['uint32']),
// Coin
c: bdb.key('c', ['hash256', 'uint32']),
C: bdb.key('C', ['uint32', 'hash256', 'uint32']),
d: bdb.key('d', ['hash256', 'uint32']),
s: bdb.key('s', ['hash256', 'uint32']),
// Coin Selector
// confirmed by Value
Sv: bdb.key('Sv', ['uint64', 'hash256', 'uint32']),
// confirmed by account + Value
SV: bdb.key('SV', ['uint32', 'uint64', 'hash256', 'uint32']),
// Unconfirmed by value
Su: bdb.key('Su', ['uint64', 'hash256', 'uint32']),
// Unconfirmed by account + value
SU: bdb.key('SU', ['uint32', 'uint64', 'hash256', 'uint32']),
// by Height
Sh: bdb.key('Sh', ['uint32', 'hash256', 'uint32']),
// by account + Height
SH: bdb.key('SH', ['uint32', 'uint32', 'hash256', 'uint32']),
// Transaction
t: bdb.key('t', ['hash256']),
T: bdb.key('T', ['uint32', 'hash256']),
// Confirmed
b: bdb.key('b', ['uint32']),
h: bdb.key('h', ['uint32', 'hash256']),
H: bdb.key('H', ['uint32', 'uint32', 'hash256']),
// Unconfirmed
p: bdb.key('p', ['hash256']),
P: bdb.key('P', ['uint32', 'hash256']),
// Count and Time Index. (O for general prefix.)
Ol: bdb.key('Ol'),
Oc: bdb.key('Oc', ['hash256']),
Ou: bdb.key('Ou', ['hash256']),
Ot: bdb.key('Ot', ['uint32', 'uint32']),
OT: bdb.key('OT', ['uint32', 'uint32', 'uint32']),
// Count and Time Index - Confirmed
Oi: bdb.key('Oi', ['uint32', 'uint32', 'uint32', 'hash256']),
OI: bdb.key('OI', ['uint32', 'uint32', 'uint32', 'uint32', 'hash256']),
// Count and Time Index - Unconfirmed
Om: bdb.key('Om', ['uint32', 'uint32', 'hash256']),
OM: bdb.key('OM', ['uint32', 'uint32', 'uint32', 'hash256']),
Oe: bdb.key('Oe', ['hash256']),
// Names
A: bdb.key('A', ['hash256']),
U: bdb.key('U', ['hash256']),
i: bdb.key('i', ['hash256', 'hash256', 'uint32']),
B: bdb.key('B', ['hash256', 'hash256', 'uint32']),
E: bdb.key('E', ['hash256', 'hash256', 'uint32']),
v: bdb.key('v', ['hash256']),
o: bdb.key('o', ['hash256'])
};

692
docs/js-wallet/masterkey.js Normal file
View file

@ -0,0 +1,692 @@
/*!
* masterkey.js - master bip32 key object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const {Lock} = require('bmutex');
const random = require('bcrypto/lib/random');
const cleanse = require('bcrypto/lib/cleanse');
const aes = require('bcrypto/lib/aes');
const sha256 = require('bcrypto/lib/sha256');
const secp256k1 = require('bcrypto/lib/secp256k1');
const pbkdf2 = require('bcrypto/lib/pbkdf2');
const scrypt = require('bcrypto/lib/scrypt');
const util = require('../utils/util');
const HDPrivateKey = require('../hd/private');
const Mnemonic = require('../hd/mnemonic');
const pkg = require('../pkg');
const {encoding} = bio;
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../protocol/network')} Network */
/**
* Master Key
* Master BIP32 key which can exist
* in a timed out encrypted state.
* @alias module:wallet.MasterKey
*/
class MasterKey extends bio.Struct {
/**
* Create a master key.
* @constructor
* @param {Object} options
*/
constructor(options) {
super();
this.encrypted = false;
this.iv = null;
this.ciphertext = null;
this.key = null;
this.mnemonic = null;
this.alg = MasterKey.alg.PBKDF2;
this.n = 50000;
this.r = 0;
this.p = 0;
this.aesKey = null;
this.timer = null;
this.until = 0;
this.locker = new Lock();
if (options)
this.fromOptions(options);
}
/**
* Inject properties from options object.
* @param {Object} options
* @returns {this}
*/
fromOptions(options) {
assert(options);
if (options.encrypted != null) {
assert(typeof options.encrypted === 'boolean');
this.encrypted = options.encrypted;
}
if (options.iv) {
assert(Buffer.isBuffer(options.iv));
this.iv = options.iv;
}
if (options.ciphertext) {
assert(Buffer.isBuffer(options.ciphertext));
this.ciphertext = options.ciphertext;
}
if (options.key) {
assert(HDPrivateKey.isHDPrivateKey(options.key));
this.key = options.key;
}
if (options.mnemonic) {
assert(options.mnemonic instanceof Mnemonic);
this.mnemonic = options.mnemonic;
}
if (options.alg != null) {
if (typeof options.alg === 'string') {
this.alg = MasterKey.alg[options.alg.toUpperCase()];
assert(this.alg != null, 'Unknown algorithm.');
} else {
assert(typeof options.alg === 'number');
assert(MasterKey.algByVal[options.alg]);
this.alg = options.alg;
}
}
if (options.rounds != null) {
assert((options.rounds >>> 0) === options.rounds);
this.rounds = options.rounds;
}
if (options.n != null) {
assert((options.n >>> 0) === options.n);
this.n = options.n;
}
if (options.r != null) {
assert((options.r >>> 0) === options.r);
this.r = options.r;
}
if (options.p != null) {
assert((options.p >>> 0) === options.p);
this.p = options.p;
}
assert(this.encrypted ? !this.key : this.key);
return this;
}
/**
* Decrypt the key and set a timeout to destroy decrypted data.
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Number} [timeout=60] timeout in seconds.
* @returns {Promise<HDPrivateKey>}
*/
async unlock(passphrase, timeout) {
const _unlock = await this.locker.lock();
try {
return await this._unlock(passphrase, timeout);
} finally {
_unlock();
}
}
/**
* Decrypt the key without a lock.
* @private
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Number} [timeout=60] timeout in seconds.
* @returns {Promise<HDPrivateKey>}
*/
async _unlock(passphrase, timeout) {
if (this.key) {
if (this.encrypted) {
assert(this.timer != null);
this.start(timeout);
}
return this.key;
}
if (!passphrase)
throw new Error('No passphrase.');
assert(this.encrypted);
const key = await this.derive(passphrase);
const data = aes.decipher(this.ciphertext, key, this.iv);
this.readKey(data);
this.start(timeout);
this.aesKey = key;
return this.key;
}
/**
* Start the destroy timer.
* @private
* @param {Number} [timeout=60] timeout in seconds.
*/
start(timeout) {
if (!timeout)
timeout = 60;
this.stop();
if (timeout === -1)
return;
assert((timeout >>> 0) === timeout);
this.until = util.now() + timeout;
this.timer = setTimeout(() => this.lock(), timeout * 1000);
}
/**
* Stop the destroy timer.
* @private
*/
stop() {
if (this.timer != null) {
clearTimeout(this.timer);
this.timer = null;
this.until = 0;
}
}
/**
* Derive an aes key based on params.
* @param {String|Buffer} passwd
* @returns {Promise<Buffer>}
*/
async derive(passwd) {
const salt = MasterKey.SALT;
const n = this.n;
const r = this.r;
const p = this.p;
if (typeof passwd === 'string')
passwd = Buffer.from(passwd, 'utf8');
switch (this.alg) {
case MasterKey.alg.PBKDF2:
return pbkdf2.deriveAsync(sha256, passwd, salt, n, 32);
case MasterKey.alg.SCRYPT:
return scrypt.deriveAsync(passwd, salt, n, r, p, 32);
default:
throw new Error(`Unknown algorithm: ${this.alg}.`);
}
}
/**
* Encrypt data with in-memory aes key.
* @param {Buffer} data
* @param {Buffer} iv
* @returns {Buffer}
*/
encipher(data, iv) {
if (!this.aesKey)
return null;
return aes.encipher(data, this.aesKey, iv.slice(0, 16));
}
/**
* Decrypt data with in-memory aes key.
* @param {Buffer} data
* @param {Buffer} iv
* @returns {Buffer}
*/
decipher(data, iv) {
if (!this.aesKey)
return null;
return aes.decipher(data, this.aesKey, iv.slice(0, 16));
}
/**
* Destroy the key by zeroing the
* privateKey and chainCode. Stop
* the timer if there is one.
* @returns {Promise}
*/
async lock() {
const unlock = await this.locker.lock();
try {
return this._lock();
} finally {
unlock();
}
}
/**
* Destroy the key by zeroing the
* privateKey and chainCode. Stop
* the timer if there is one.
*/
_lock() {
if (!this.encrypted) {
assert(this.timer == null);
assert(this.key);
return;
}
this.stop();
if (this.key) {
this.key.destroy(true);
this.key = null;
}
if (this.aesKey) {
cleanse(this.aesKey);
this.aesKey = null;
}
}
/**
* Destroy the key permanently.
*/
async destroy() {
await this.lock();
this.locker.destroy();
}
/**
* Decrypt the key permanently.
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Boolean} [clean=false]
* @returns {Promise<Buffer|null>}
*/
async decrypt(passphrase, clean) {
const unlock = await this.locker.lock();
try {
return await this._decrypt(passphrase, clean);
} finally {
unlock();
}
}
/**
* Decrypt the key permanently without a lock.
* @private
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Boolean} [clean=false]
* @returns {Promise<Buffer|null>}
*/
async _decrypt(passphrase, clean) {
if (!this.encrypted)
throw new Error('Master key is not encrypted.');
if (!passphrase)
throw new Error('No passphrase provided.');
this._lock();
const key = await this.derive(passphrase);
const data = aes.decipher(this.ciphertext, key, this.iv);
this.readKey(data);
this.encrypted = false;
this.iv = null;
this.ciphertext = null;
if (!clean) {
cleanse(key);
return null;
}
return key;
}
/**
* Encrypt the key permanently.
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Boolean} [clean=false]
* @returns {Promise<Buffer|null>}
*/
async encrypt(passphrase, clean) {
const unlock = await this.locker.lock();
try {
return await this._encrypt(passphrase, clean);
} finally {
unlock();
}
}
/**
* Encrypt the key permanently without a lock.
* @private
* @param {Buffer|String} passphrase - Zero this yourself.
* @param {Boolean} [clean=false]
* @returns {Promise<Buffer|null>}
*/
async _encrypt(passphrase, clean) {
if (this.encrypted)
throw new Error('Master key is already encrypted.');
if (!passphrase)
throw new Error('No passphrase provided.');
const raw = this.writeKey();
const iv = random.randomBytes(16);
this.stop();
const key = await this.derive(passphrase);
const data = aes.encipher(raw, key, iv);
this.key = null;
this.mnemonic = null;
this.encrypted = true;
this.iv = iv;
this.ciphertext = data;
if (!clean) {
cleanse(key);
return null;
}
return key;
}
/**
* Calculate key serialization size.
* @returns {Number}
*/
keySize() {
let size = 0;
size += 64;
size += 1;
if (this.mnemonic)
size += this.mnemonic.getSize();
return size;
}
/**
* Serialize key and menmonic to a single buffer.
* @returns {Buffer}
*/
writeKey() {
const bw = bio.write(this.keySize());
bw.writeBytes(this.key.chainCode);
bw.writeBytes(this.key.privateKey);
if (this.mnemonic) {
bw.writeU8(1);
this.mnemonic.write(bw);
} else {
bw.writeU8(0);
}
return bw.render();
}
/**
* Inject properties from serialized key.
* @param {Buffer} data
*/
readKey(data) {
const br = bio.read(data);
this.key = new HDPrivateKey();
this.key.chainCode = br.readBytes(32);
this.key.privateKey = br.readBytes(32);
this.key.publicKey = secp256k1.publicKeyCreate(this.key.privateKey, true);
if (br.readU8() === 1)
this.mnemonic = Mnemonic.read(br);
return this;
}
/**
* Calculate serialization size.
* @returns {Number}
*/
getSize() {
let size = 0;
if (this.encrypted) {
size += 1;
size += encoding.sizeVarBytes(this.iv);
size += encoding.sizeVarBytes(this.ciphertext);
size += 13;
return size;
}
size += 1;
size += this.keySize();
return size;
}
/**
* Serialize the key in the form of:
* `[enc-flag][iv?][ciphertext?][extended-key?]`
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
if (this.encrypted) {
bw.writeU8(1);
bw.writeVarBytes(this.iv);
bw.writeVarBytes(this.ciphertext);
bw.writeU8(this.alg);
bw.writeU32(this.n);
bw.writeU32(this.r);
bw.writeU32(this.p);
return bw;
}
bw.writeU8(0);
bw.writeBytes(this.key.chainCode);
bw.writeBytes(this.key.privateKey);
if (this.mnemonic) {
bw.writeU8(1);
this.mnemonic.write(bw);
} else {
bw.writeU8(0);
}
return bw;
}
/**
* Inject properties from serialized data.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.encrypted = br.readU8() === 1;
if (this.encrypted) {
this.iv = br.readVarBytes();
this.ciphertext = br.readVarBytes();
this.alg = br.readU8();
assert(this.alg < MasterKey.algByVal.length);
this.n = br.readU32();
this.r = br.readU32();
this.p = br.readU32();
return this;
}
this.key = new HDPrivateKey();
this.key.chainCode = br.readBytes(32);
this.key.privateKey = br.readBytes(32);
this.key.publicKey = secp256k1.publicKeyCreate(this.key.privateKey, true);
if (br.readU8() === 1)
this.mnemonic = Mnemonic.read(br);
return this;
}
/**
* Inject properties from an HDPrivateKey.
* @param {HDPrivateKey} key
* @param {Mnemonic?} mnemonic
*/
fromKey(key, mnemonic) {
this.encrypted = false;
this.iv = null;
this.ciphertext = null;
this.key = key;
this.mnemonic = mnemonic || null;
return this;
}
/**
* Instantiate master key from an HDPrivateKey.
* @param {HDPrivateKey} key
* @param {Mnemonic?} mnemonic
* @returns {MasterKey}
*/
static fromKey(key, mnemonic) {
return new this().fromKey(key, mnemonic);
}
/**
* Convert master key to a jsonifiable object.
* @param {Network?} [network]
* @param {Boolean?} [unsafe] - Whether to include
* the key data in the JSON.
* @returns {Object}
*/
getJSON(network, unsafe) {
if (this.encrypted) {
return {
encrypted: true,
until: this.until,
iv: this.iv.toString('hex'),
ciphertext: unsafe ? this.ciphertext.toString('hex') : undefined,
algorithm: MasterKey.algByVal[this.alg].toLowerCase(),
n: this.n,
r: this.r,
p: this.p
};
}
return {
encrypted: false,
key: unsafe ? this.key.getJSON(network) : undefined,
mnemonic: unsafe && this.mnemonic ? this.mnemonic.toJSON() : undefined
};
}
/**
* Inspect the key.
* @returns {Object}
*/
format() {
const json = this.getJSON(null, true);
if (this.key)
json.key = this.key.toJSON();
if (this.mnemonic)
json.mnemonic = this.mnemonic.toJSON();
return json;
}
/**
* Test whether an object is a MasterKey.
* @param {Object} obj
* @returns {Boolean}
*/
static isMasterKey(obj) {
return obj instanceof MasterKey;
}
}
/**
* Key derivation salt.
* @const {Buffer}
* @default
*/
MasterKey.SALT = Buffer.from(pkg.name, 'ascii');
/**
* Key derivation algorithms.
* @enum {Number}
* @default
*/
MasterKey.alg = {
PBKDF2: 0,
SCRYPT: 1
};
/**
* Key derivation algorithms by value.
* @enum {String}
* @default
*/
MasterKey.algByVal = [
'PBKDF2',
'SCRYPT'
];
/*
* Expose
*/
module.exports = MasterKey;

1773
docs/js-wallet/migrations.js Normal file

File diff suppressed because it is too large Load diff

147
docs/js-wallet/node.js Normal file
View file

@ -0,0 +1,147 @@
/*!
* server.js - wallet server for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const Node = require('../node/node');
const WalletDB = require('./walletdb');
const HTTP = require('./http');
const Client = require('./client');
const RPC = require('./rpc');
const pkg = require('../pkg');
/**
* Wallet Node
* @extends Node
*/
class WalletNode extends Node {
/**
* Create a wallet node.
* @constructor
* @param {Object?} options
*/
constructor(options) {
super(pkg.name, 'hsw.conf', 'wallet.log', options);
this.opened = false;
this.client = new Client({
network: this.network,
url: this.config.str('node-url'),
host: this.config.str('node-host'),
port: this.config.uint('node-port', this.network.rpcPort),
ssl: this.config.bool('node-ssl'),
apiKey: this.config.str('node-api-key')
});
this.wdb = new WalletDB({
network: this.network,
logger: this.logger,
workers: this.workers,
client: this.client,
prefix: this.config.prefix,
memory: this.config.bool('memory'),
maxFiles: this.config.uint('max-files'),
cacheSize: this.config.mb('cache-size'),
wipeNoReally: this.config.bool('wipe-no-really'),
spv: this.config.bool('spv'),
walletMigrate: this.config.uint('migrate'),
icannlockup: this.config.bool('icannlockup', true),
migrateNoRescan: this.config.bool('migrate-no-rescan', false),
preloadAll: this.config.bool('preload-all', false),
maxHistoryTXs: this.config.uint('max-history-txs', 100)
});
this.rpc = new RPC(this);
this.http = new HTTP({
network: this.network,
logger: this.logger,
node: this,
prefix: this.config.prefix,
ssl: this.config.bool('ssl'),
keyFile: this.config.path('ssl-key'),
certFile: this.config.path('ssl-cert'),
host: this.config.str('http-host'),
port: this.config.uint('http-port'),
apiKey: this.config.str('api-key'),
walletAuth: this.config.bool('wallet-auth'),
noAuth: this.config.bool('no-auth'),
cors: this.config.bool('cors'),
adminToken: this.config.str('admin-token')
});
this.init();
}
/**
* Initialize the node.
* @private
*/
init() {
this.wdb.on('error', err => this.error(err));
this.http.on('error', err => this.error(err));
this.loadPlugins();
}
/**
* Open the node and all its child objects,
* wait for the database to load.
* @returns {Promise}
*/
async open() {
assert(!this.opened, 'WalletNode is already open.');
this.opened = true;
await this.handlePreopen();
await this.wdb.open();
this.rpc.wallet = this.wdb.primary;
await this.openPlugins();
await this.http.open();
await this.wdb.connect();
await this.handleOpen();
this.logger.info('Wallet node is loaded.');
this.emit('open');
}
/**
* Close the node, wait for the database to close.
* @returns {Promise}
*/
async close() {
assert(this.opened, 'WalletNode is not open.');
this.opened = false;
await this.handlePreclose();
await this.http.close();
await this.closePlugins();
this.rpc.wallet = null;
await this.wdb.disconnect();
await this.wdb.close();
await this.handleClose();
this.emit('close');
}
}
/*
* Expose
*/
module.exports = WalletNode;

View file

@ -0,0 +1,360 @@
/*!
* nodeclient.js - node client for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const blacklist = require('bsock/lib/blacklist');
const AsyncEmitter = require('bevent');
/** @typedef {import('bfilter').BloomFilter} BloomFilter */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../primitives/tx')} TX */
/** @typedef {import('../primitives/claim')} Claim */
/** @typedef {import('../covenants/namestate')} NameState */
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
/** @typedef {import('../node/fullnode')} FullNode */
/** @typedef {import('../node/spvnode')} SPVNode */
/** @typedef {FullNode|SPVNode} Node */
/**
* Node Client
* @alias module:node.NodeClient
*/
class NodeClient extends AsyncEmitter {
/**
* Create a node client.
* @constructor
* @param {Node} node
*/
constructor(node) {
super();
/** @type {Node} */
this.node = node;
this.network = node.network;
this.filter = null;
this.opened = false;
this.hooks = new Map();
this.init();
}
/**
* Initialize the client.
*/
init() {
this.node.chain.on('connect', async (entry, block) => {
if (!this.opened)
return;
await this.emitAsync('block connect', entry, block.txs);
});
this.node.chain.on('disconnect', async (entry, block) => {
if (!this.opened)
return;
await this.emitAsync('block disconnect', entry);
});
this.node.on('tx', (tx) => {
if (!this.opened)
return;
this.emit('tx', tx);
});
this.node.on('reset', (tip) => {
if (!this.opened)
return;
this.emit('chain reset', tip);
});
}
/**
* Open the client.
* @returns {Promise<void>}
*/
async open() {
assert(!this.opened, 'NodeClient is already open.');
this.opened = true;
setImmediate(() => this.emit('connect'));
}
/**
* Close the client.
* @returns {Promise<void>}
*/
async close() {
assert(this.opened, 'NodeClient is not open.');
this.opened = false;
setImmediate(() => this.emit('disconnect'));
}
/**
* Add a listener.
* @param {String} type
* @param {Function} handler
*/
bind(type, handler) {
return this.on(type, handler);
}
/**
* Add a hook.
* @param {String} event
* @param {Function} handler
*/
hook(event, handler) {
assert(typeof event === 'string', 'Event must be a string.');
assert(typeof handler === 'function', 'Handler must be a function.');
assert(!this.hooks.has(event), 'Hook already bound.');
assert(!Object.prototype.hasOwnProperty.call(blacklist, event),
'Blacklisted event.');
this.hooks.set(event, handler);
}
/**
* Remove a hook.
* @param {String} event
*/
unhook(event) {
assert(typeof event === 'string', 'Event must be a string.');
assert(!Object.prototype.hasOwnProperty.call(blacklist, event),
'Blacklisted event.');
this.hooks.delete(event);
}
/**
* Call a hook.
* @param {String} event
* @param {...Object} args
* @returns {Promise}
*/
handleCall(event, ...args) {
const hook = this.hooks.get(event);
if (!hook)
throw new Error('No hook available.');
return hook(...args);
}
/**
* Get chain tip.
* @returns {Promise<ChainEntry>}
*/
async getTip() {
return this.node.chain.tip;
}
/**
* Get chain entry.
* @param {Hash} hash
* @returns {Promise<ChainEntry?>}
*/
async getEntry(hash) {
const entry = await this.node.chain.getEntry(hash);
if (!entry)
return null;
if (!await this.node.chain.isMainChain(entry))
return null;
return entry;
}
/**
* Send a transaction. Do not wait for promise.
* @param {TX} tx
* @returns {Promise<void>}
*/
async send(tx) {
this.node.relay(tx);
}
/**
* Send a claim. Do not wait for promise.
* @param {Claim} claim
* @returns {Promise<void>}
*/
async sendClaim(claim) {
this.node.relayClaim(claim);
}
/**
* Set bloom filter.
* @param {BloomFilter} filter
* @returns {Promise<void>}
*/
async setFilter(filter) {
this.filter = filter;
this.node.pool.setFilter(filter);
}
/**
* Add data to filter.
* @param {Buffer} data
* @returns {Promise<void>}
*/
async addFilter(data) {
// `data` is ignored because pool.spvFilter === walletDB.filter
// and therefore is already updated.
// Argument is kept here to be consistent with API in
// wallet/client.js (client/node.js) and wallet/nullclient.js
this.node.pool.queueFilterLoad();
}
/**
* Reset filter.
* @returns {Promise<void>}
*/
async resetFilter() {
this.node.pool.queueFilterLoad();
}
/**
* Esimate smart fee.
* @param {Number?} blocks
* @returns {Promise<Number>}
*/
async estimateFee(blocks) {
if (!this.node.fees)
return this.network.feeRate;
return this.node.fees.estimateFee(blocks);
}
/**
* Get hash range.
* @param {Number} start
* @param {Number} end
* @returns {Promise<Hash[]>}
*/
async getHashes(start = -1, end = -1) {
return this.node.chain.getHashes(start, end);
}
/**
* Get entries range.
* @param {Number} start
* @param {Number} end
* @returns {Promise<ChainEntry[]>}
*/
async getEntries(start = -1, end = -1) {
return this.node.chain.getEntries(start, end);
}
/**
* Rescan for any missed transactions.
* @param {Number|Hash} start - Start block.
* @returns {Promise<void>}
*/
async rescan(start) {
if (this.node.spv)
return this.node.chain.reset(start);
return this.node.chain.scan(start, this.filter, (entry, txs) => {
return this.handleCall('block rescan', entry, txs);
});
}
/**
* Rescan interactive for any missed transactions.
* @param {Number|Hash} start - Start block.
* @param {Boolean} [fullLock=false]
* @returns {Promise<void>}
*/
async rescanInteractive(start, fullLock = true) {
if (this.node.spv)
return this.node.chain.reset(start);
const iter = async (entry, txs) => {
return await this.handleCall('block rescan interactive', entry, txs);
};
try {
return await this.node.scanInteractive(
start,
this.filter,
iter,
fullLock
);
} catch (e) {
await this.handleCall('block rescan interactive abort', e.message);
throw e;
}
}
/**
* Get name state.
* @param {Buffer} nameHash
* @returns {Promise<NameState>}
*/
async getNameStatus(nameHash) {
return this.node.getNameStatus(nameHash);
}
/**
* Get UTXO.
* @param {Hash} hash
* @param {Number} index
* @returns {Object}
*/
async getCoin(hash, index) {
return this.node.getCoin(hash, index);
}
/**
* Get block header.
* @param {Hash|Number} block
* @returns {Promise<ChainEntry>}
*/
async getBlockHeader(block) {
if (typeof block === 'string')
block = Buffer.from(block, 'hex');
const entry = await this.node.chain.getEntry(block);
if (!entry)
return null;
return entry.toJSON();
}
}
/*
* Expose
*/
module.exports = NodeClient;

View file

@ -0,0 +1,229 @@
/*!
* nullclient.js - node client for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const EventEmitter = require('events');
const NameState = require('../covenants/namestate');
const Block = require('../primitives/block');
/**
* Null Client
* Sort of a fake local client for separation of concerns.
* @alias module:node.NullClient
*/
class NullClient extends EventEmitter {
/**
* Create a client.
* @constructor
*/
constructor(wdb) {
super();
this.wdb = wdb;
this.network = wdb.network;
this.opened = false;
}
/**
* Open the client.
* @returns {Promise}
*/
async open() {
assert(!this.opened, 'NullClient is already open.');
this.opened = true;
setImmediate(() => this.emit('connect'));
}
/**
* Close the client.
* @returns {Promise}
*/
async close() {
assert(this.opened, 'NullClient is not open.');
this.opened = false;
setImmediate(() => this.emit('disconnect'));
}
/**
* Add a listener.
* @param {String} type
* @param {Function} handler
*/
bind(type, handler) {
return this.on(type, handler);
}
/**
* Add a listener.
* @param {String} type
* @param {Function} handler
*/
hook(type, handler) {
return this.on(type, handler);
}
/**
* Get chain tip.
* @returns {Promise}
*/
async getTip() {
const {hash, height, time} = this.network.genesis;
return { hash, height, time };
}
/**
* Get chain entry.
* @param {Hash} hash
* @returns {Promise}
*/
async getEntry(hash) {
return { hash, height: 0, time: 0 };
}
/**
* Send a transaction. Do not wait for promise.
* @param {TX} tx
* @returns {Promise}
*/
async send(tx) {
this.wdb.emit('send', tx);
}
/**
* Send a claim. Do not wait for promise.
* @param {Claim} claim
* @returns {Promise}
*/
async sendClaim(claim) {
this.wdb.emit('send claim', claim);
}
/**
* Set bloom filter.
* @param {Bloom} filter
* @returns {Promise}
*/
async setFilter(filter) {
this.wdb.emit('set filter', filter);
}
/**
* Add data to filter.
* @param {Buffer} data
* @returns {Promise}
*/
async addFilter(data) {
this.wdb.emit('add filter', data);
}
/**
* Reset filter.
* @returns {Promise}
*/
async resetFilter() {
this.wdb.emit('reset filter');
}
/**
* Esimate smart fee.
* @param {Number?} blocks
* @returns {Promise}
*/
async estimateFee(blocks) {
return this.network.feeRate;
}
/**
* Get hash range.
* @param {Number} start
* @param {Number} end
* @returns {Promise}
*/
async getHashes(start = -1, end = -1) {
return [this.network.genesis.hash];
}
/**
* Get entries.
* @param {Number} [start=-1]
* @param {Number} [end=-1]
* @returns {Promise}
*/
async getEntries(start = -1, end = -1) {
const genesisBlock = Block.decode(this.network.genesisBlock);
const entry = {
hash: genesisBlock.hash(),
height: 0,
time: genesisBlock.time
};
return [entry];
}
/**
* Rescan for any missed transactions.
* @param {Number|Hash} start - Start block.
* @returns {Promise}
*/
async rescan(start) {
;
}
/**
* Rescan interactive for any missed transactions.
* @param {Number|Hash} start - Start block.
* @param {Boolean} [fullLock=false]
* @returns {Promise}
*/
async rescanInteractive(start, fullLock) {
;
}
/**
* Get opening bid height.
* @param {Buffer} nameHash
* @returns {Object}
*/
async getNameStatus(nameHash) {
return new NameState();
}
/**
* Get block header.
* @param {Hash|Number} block
* @returns {Promise<ChainEntry?>}
*/
async getBlockHeader(block) {
return null;
}
}
/*
* Expose
*/
module.exports = NullClient;

346
docs/js-wallet/path.js Normal file
View file

@ -0,0 +1,346 @@
/*!
* path.js - path object for wallets
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const bio = require('bufio');
const Address = require('../primitives/address');
const Network = require('../protocol/network');
const {encoding} = bio;
/** @typedef {import('../types').NetworkType} NetworkType */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('./account')} Account */
/**
* Path
* @alias module:wallet.Path
* @property {String} name - Account name.
* @property {Number} account - Account index.
* @property {Number} branch - Branch index.
* @property {Number} index - Address index.
*/
class Path extends bio.Struct {
/**
* Create a path.
* @constructor
* @param {Object} [options]
*/
constructor(options) {
super();
this.keyType = Path.types.HD;
/** @type {String|null} */
this.name = null; // Passed in by caller.
this.account = 0;
this.version = 0;
this.branch = -1;
this.index = -1;
this.encrypted = false;
this.data = null;
/** @type {Hash|null} */
this.hash = null; // Passed in by caller.
if (options)
this.fromOptions(options);
}
/**
* Instantiate path from options object.
* @param {Object} options
* @returns {this}
*/
fromOptions(options) {
this.keyType = options.keyType;
this.name = options.name;
this.account = options.account;
this.branch = options.branch;
this.index = options.index;
this.encrypted = options.encrypted;
this.data = options.data;
this.version = options.version;
this.hash = options.hash;
return this;
}
/**
* Clone the path object.
* @param {this} path
* @returns {this}
*/
inject(path) {
this.keyType = path.keyType;
this.name = path.name;
this.account = path.account;
this.branch = path.branch;
this.index = path.index;
this.encrypted = path.encrypted;
this.data = path.data;
this.version = path.version;
this.hash = path.hash;
return this;
}
/**
* Inject properties from serialized data.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.account = br.readU32();
this.keyType = br.readU8();
this.version = br.readU8();
switch (this.keyType) {
case Path.types.HD:
this.branch = br.readU32();
this.index = br.readU32();
break;
case Path.types.KEY:
this.encrypted = br.readU8() === 1;
this.data = br.readVarBytes();
break;
case Path.types.ADDRESS:
// Hash will be passed in by caller.
break;
default:
assert(false);
break;
}
return this;
}
/**
* Calculate serialization size.
* @returns {Number}
*/
getSize() {
let size = 0;
size += 6;
switch (this.keyType) {
case Path.types.HD:
size += 8;
break;
case Path.types.KEY:
size += 1;
size += encoding.sizeVarBytes(this.data);
break;
}
return size;
}
/**
* Serialize path.
* @param {bio.BufferWriter} bw
* @returns {bio.BufferWriter}
*/
write(bw) {
bw.writeU32(this.account);
bw.writeU8(this.keyType);
bw.writeU8(this.version);
switch (this.keyType) {
case Path.types.HD:
assert(!this.data);
assert(this.index !== -1);
bw.writeU32(this.branch);
bw.writeU32(this.index);
break;
case Path.types.KEY:
assert(this.data);
assert(this.index === -1);
bw.writeU8(this.encrypted ? 1 : 0);
bw.writeVarBytes(this.data);
break;
case Path.types.ADDRESS:
assert(!this.data);
assert(this.index === -1);
break;
default:
assert(false);
break;
}
return bw;
}
/**
* Inject properties from address.
* @param {Account} account
* @param {Address} address
*/
fromAddress(account, address) {
this.keyType = Path.types.ADDRESS;
this.name = account.name;
this.account = account.accountIndex;
this.version = address.version;
this.hash = address.getHash();
return this;
}
/**
* Instantiate path from address.
* @param {Account} account
* @param {Address} address
* @returns {Path}
*/
static fromAddress(account, address) {
return new this().fromAddress(account, address);
}
/**
* Convert path object to string derivation path.
* @param {(NetworkType|Network)?} [network] - Network type.
* @returns {String}
*/
toPath(network) {
if (this.keyType !== Path.types.HD)
return null;
let prefix = 'm';
if (network) {
const purpose = 44;
network = Network.get(network);
prefix += `/${purpose}'/${network.keyPrefix.coinType}'`;
}
return `${prefix}/${this.account}'/${this.branch}/${this.index}`;
}
/**
* Convert path object to an address (currently unused).
* @returns {Address}
*/
toAddress() {
return Address.fromHash(this.hash, this.version);
}
/**
* Convert path to a json-friendly object.
* @param {(NetworkType|Network)?} [network] - Network type.
* @returns {Object}
*/
getJSON(network) {
return {
name: this.name,
account: this.account,
change: this.branch === 1,
derivation: this.toPath(network)
};
}
/**
* Inject properties from a json object.
* @param {Object} json
* @returns {Path}
*/
static fromJSON(json) {
return new this().fromJSON(json);
}
/**
* Inject properties from a json object.
* @param {Object} json
* @returns {this}
*/
fromJSON(json) {
assert(json && typeof json === 'object');
assert(json.derivation && typeof json.derivation === 'string');
// Note: this object is mutated below.
const path = json.derivation.split('/');
// Note: "m/X'/X'/X'/X/X" or "m/X'/X/X".
assert (path.length === 4 || path.length === 6);
const index = parseInt(path.pop(), 10);
const branch = parseInt(path.pop(), 10);
const account = parseInt(path.pop(), 10);
assert(account === json.account);
assert(branch === 0 || branch === 1);
assert(Boolean(branch) === json.change);
assert((index >>> 0) === index);
this.name = json.name;
this.account = account;
this.branch = branch;
this.index = index;
return this;
}
/**
* Inspect the path.
* @returns {String}
*/
format() {
return `<Path: ${this.name}:${this.toPath()}>`;
}
}
/**
* Path types.
* @enum {Number}
* @default
*/
Path.types = {
HD: 0,
KEY: 1,
ADDRESS: 2
};
/**
* Path types.
* @enum {Number}
* @default
*/
Path.typesByVal = [
'HD',
'KEY',
'ADDRESS'
];
/**
* Expose
*/
module.exports = Path;

95
docs/js-wallet/paths.js Normal file
View file

@ -0,0 +1,95 @@
/*!
* paths.js - paths object for hsd
* Copyright (c) 2019, Boyma Fahnbulleh (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
/** @typedef {import('./path')} Path */
/**
* Paths
* Represents the HD paths for coins in a single transaction.
* @alias module:wallet.Paths
* @property {Map[]} outputs - Paths.
*/
class Paths {
/**
* Create paths
* @constructor
*/
constructor() {
this.paths = new Map();
}
/**
* Add a single entry to the collection.
* @param {Number} index
* @param {Path} path
* @returns {Path}
*/
add(index, path) {
assert((index >>> 0) === index);
assert(path);
this.paths.set(index, path);
return path;
}
/**
* Test whether the collection has a path.
* @param {Number} index
* @returns {Boolean}
*/
has(index) {
return this.paths.has(index);
}
/**
* Get a path.
* @param {Number} index
* @returns {Path|null}
*/
get(index) {
return this.paths.get(index) || null;
}
/**
* Remove a path and return it.
* @param {Number} index
* @returns {Path|null}
*/
remove(index) {
const path = this.get(index);
if (!path)
return null;
this.paths.delete(index);
return path;
}
/**
* Test whether there are paths.
* @returns {Boolean}
*/
isEmpty() {
return this.paths.size === 0;
}
}
/*
* Expose
*/
module.exports = Paths;

128
docs/js-wallet/plugin.js Normal file
View file

@ -0,0 +1,128 @@
/*!
* plugin.js - wallet plugin for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const EventEmitter = require('events');
const WalletDB = require('./walletdb');
const NodeClient = require('./nodeclient');
const HTTP = require('./http');
const RPC = require('./rpc');
/** @typedef {import('../node/fullnode')} FullNode */
/** @typedef {import('../node/spvnode')} SPVNode */
/** @typedef {FullNode|SPVNode} Node */
/**
* @exports wallet/plugin
*/
const plugin = exports;
/**
* Plugin
* @extends EventEmitter
*/
class Plugin extends EventEmitter {
/**
* Create a plugin.
* @constructor
* @param {Node} node
*/
constructor(node) {
super();
this.config = node.config.filter('wallet', {
// Allow configurations to propagate from the hsd.conf
// with 'wallet-' prefix.
data: true
});
this.config.open('hsw.conf');
this.network = node.network;
this.logger = node.logger;
this.client = new NodeClient(node);
this.wdb = new WalletDB({
network: this.network,
logger: this.logger,
workers: this.workers,
client: this.client,
prefix: this.config.prefix,
memory: this.config.bool('memory', node.memory),
maxFiles: this.config.uint('max-files'),
cacheSize: this.config.mb('cache-size'),
wipeNoReally: this.config.bool('wipe-no-really'),
spv: node.spv,
walletMigrate: this.config.uint('migrate'),
icannlockup: this.config.bool('icannlockup', true),
migrateNoRescan: this.config.bool('migrate-no-rescan', false),
preloadAll: this.config.bool('preload-all', false),
maxHistoryTXs: this.config.uint('max-history-txs', 100)
});
this.rpc = new RPC(this);
this.http = new HTTP({
network: this.network,
logger: this.logger,
node: this,
ssl: this.config.bool('ssl'),
keyFile: this.config.path('ssl-key'),
certFile: this.config.path('ssl-cert'),
host: this.config.str('http-host'),
port: this.config.uint('http-port'),
apiKey: this.config.str('api-key', node.config.str('api-key')),
walletAuth: this.config.bool('wallet-auth'),
noAuth: this.config.bool('no-auth'),
cors: this.config.bool('cors'),
adminToken: this.config.str('admin-token')
});
this.init();
}
init() {
this.wdb.on('error', err => this.emit('error', err));
this.http.on('error', err => this.emit('error', err));
}
async open() {
await this.wdb.open();
this.rpc.wallet = this.wdb.primary;
await this.http.open();
await this.wdb.connect();
}
async close() {
await this.http.close();
this.rpc.wallet = null;
await this.wdb.disconnect();
await this.wdb.close();
}
}
/**
* Plugin name.
* @const {String}
*/
plugin.id = 'walletdb';
/**
* Plugin initialization.
* @param {Node} node
* @returns {Plugin}
*/
plugin.init = function init(node) {
return new Plugin(node);
};
plugin.Plugin = Plugin;

533
docs/js-wallet/records.js Normal file
View file

@ -0,0 +1,533 @@
/*!
* records.js - walletdb records
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
/**
* @module wallet/records
*/
const assert = require('bsert');
const bio = require('bufio');
const util = require('../utils/util');
const TX = require('../primitives/tx');
const consensus = require('../protocol/consensus');
/** @typedef {import('../types').BufioWriter} BufioWriter */
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../blockchain/chainentry')} ChainEntry */
/**
* Chain State
*/
class ChainState extends bio.Struct {
/**
* Create a chain state.
* @constructor
*/
constructor() {
super();
this.startHeight = 0;
this.startHash = consensus.ZERO_HASH;
this.height = 0;
this.marked = false;
}
/**
* Clone the state.
* @param {ChainState} state
* @returns {this}
*/
inject(state) {
this.startHeight = state.startHeight;
this.startHash = state.startHash;
this.height = state.height;
this.marked = state.marked;
return this;
}
/**
* Calculate size.
* @returns {Number}
*/
getSize() {
return 41;
}
/**
* Inject properties from serialized data.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.startHeight = br.readU32();
this.startHash = br.readHash();
this.height = br.readU32();
this.marked = br.readU8() === 1;
return this;
}
/**
* Serialize the chain state.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU32(this.startHeight);
bw.writeHash(this.startHash);
bw.writeU32(this.height);
bw.writeU8(this.marked ? 1 : 0);
return bw;
}
}
/**
* Block Meta
*/
class BlockMeta extends bio.Struct {
/**
* Create block meta.
* @constructor
* @param {Hash} [hash]
* @param {Number} [height]
* @param {Number} [time]
*/
constructor(hash, height, time) {
super();
this.hash = hash || consensus.ZERO_HASH;
this.height = height != null ? height : -1;
this.time = time || 0;
}
/**
* Clone the block.
* @param {BlockMeta} meta
* @returns {this}
*/
inject(meta) {
this.hash = meta.hash;
this.height = meta.height;
this.time = meta.time;
return this;
}
/**
* Encode hash and time.
* @returns {Buffer}
*/
toHashAndTime() {
const data = Buffer.allocUnsafe(32 + 8);
bio.writeBytes(data, this.hash, 0);
bio.writeU64(data, this.time, 32);
return data;
}
/**
* Decode hash and time.
* @param {Buffer} data
* @param {Number} height
* @returns {BlockMeta}
*/
fromHashAndTime(data, height) {
this.hash = data.slice(0, 32);
this.time = bio.readU64(data, 32);
this.height = height;
return this;
}
/**
* Instantiate block meta from hash and time.
* @param {Buffer} data
* @param {Number} height
* @returns {BlockMeta}
*/
static fromHashAndTime(data, height) {
return new this().fromHashAndTime(data, height);
}
/**
* Instantiate block meta from chain entry.
* @private
* @param {ChainEntry} entry
*/
fromEntry(entry) {
this.hash = entry.hash;
this.height = entry.height;
this.time = entry.time;
return this;
}
/**
* Instantiate block meta from chain entry.
* @param {ChainEntry} entry
* @returns {BlockMeta}
*/
static fromEntry(entry) {
return new this().fromEntry(entry);
}
/**
* Instantiate block meta from serialized tip data.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.hash = br.readHash();
this.height = br.readU32();
this.time = br.readU64();
return this;
}
/**
* Calculate size.
* @returns {Number}
*/
getSize() {
return 44;
}
/**
* Serialize the block meta.
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeHash(this.hash);
bw.writeU32(this.height);
bw.writeU64(this.time);
return bw;
}
/**
* Instantiate block meta from json object.
* @param {Object} json
*/
fromJSON(json) {
this.hash = util.parseHex(json.hash, 32);
this.height = json.height;
this.time = json.time;
return this;
}
/**
* Convert the block meta to a more json-friendly object.
* @returns {Object}
*/
getJSON() {
return {
hash: this.hash.toString('hex'),
height: this.height,
time: this.time
};
}
}
/**
* TX Record
*/
class TXRecord extends bio.Struct {
/**
* Create tx record.
* @constructor
* @param {Number} mtime
* @param {TX} [tx]
* @param {BlockMeta} [block]
*/
constructor(mtime, tx, block) {
super();
if (mtime == null)
mtime = util.now();
assert(typeof mtime === 'number');
this.tx = null;
this.hash = null;
this.mtime = mtime;
this.height = -1;
/** @type {Hash?} */
this.block = null;
this.index = -1;
this.time = 0;
if (tx)
this.fromTX(tx, block);
}
/**
* Inject properties from tx and block.
* @param {TX} tx
* @param {BlockMeta} [block]
* @returns {TXRecord}
*/
fromTX(tx, block) {
this.tx = tx;
this.hash = tx.hash();
if (block)
this.setBlock(block);
return this;
}
/**
* Instantiate tx record from tx and block.
* @param {TX} [tx]
* @param {BlockMeta} [block]
* @param {Number} [mtime]
* @returns {TXRecord}
*/
static fromTX(tx, block, mtime) {
return new this(mtime).fromTX(tx, block);
}
/**
* Set block data (confirm).
* @param {BlockMeta} block
*/
setBlock(block) {
this.height = block.height;
this.block = block.hash;
this.time = block.time;
return this;
}
/**
* Unset block (unconfirm).
*/
unsetBlock() {
this.height = -1;
this.block = null;
this.time = 0;
return this;
}
/**
* Convert tx record to a block meta.
* @returns {BlockMeta?}
*/
getBlock() {
if (this.height === -1)
return null;
return new BlockMeta(this.block, this.height, this.time);
}
/**
* Calculate current number of transaction confirmations.
* @param {Number} height - Current chain height.
* @returns {Number} confirmations
*/
getDepth(height) {
assert(typeof height === 'number', 'Must pass in height.');
if (this.height === -1)
return 0;
if (height < this.height)
return 0;
return height - this.height + 1;
}
/**
* Get serialization size.
* @returns {Number}
*/
getSize() {
let size = 0;
size += this.tx.getSize();
size += 4;
if (this.block) {
size += 1;
size += 32;
size += 4 * 3;
} else {
size += 1;
}
return size;
}
/**
* Serialize a transaction to "extended format".
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
let index = this.index;
this.tx.write(bw);
bw.writeU32(this.mtime);
if (this.block) {
if (index === -1)
index = 0x7fffffff;
bw.writeU8(1);
bw.writeHash(this.block);
bw.writeU32(this.height);
bw.writeU32(this.time);
bw.writeU32(index);
} else {
bw.writeU8(0);
}
return bw;
}
/**
* Inject properties from "extended" format.
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
this.tx = new TX();
this.tx.read(br);
this.hash = this.tx.hash();
this.mtime = br.readU32();
if (br.readU8() === 1) {
this.block = br.readHash();
this.height = br.readU32();
this.time = br.readU32();
this.index = br.readU32();
if (this.index === 0x7fffffff)
this.index = -1;
}
return this;
}
}
/**
* Map Record
*/
class MapRecord extends bio.Struct {
/**
* Create map record.
* @constructor
*/
constructor() {
super();
/** @type {Set<Number>} */
this.wids = new Set();
}
/**
* @param {Number} wid
* @returns {Boolean} - Whether the map did not contain the wid.
*/
add(wid) {
if (this.wids.has(wid))
return false;
this.wids.add(wid);
return true;
}
/**
* @param {Number} wid
* @returns {Boolean} - Whether the map contained the wid.
*/
remove(wid) {
return this.wids.delete(wid);
}
/**
* @param {Number} wid
* @returns {Boolean} - Whether the map contains the wid.
*/
has(wid) {
return this.wids.has(wid);
}
/**
* @param {BufioWriter} bw
* @returns {BufioWriter}
*/
write(bw) {
bw.writeU32(this.wids.size);
for (const wid of this.wids)
bw.writeU32(wid);
return bw;
}
/**
* @returns {Number}
*/
getSize() {
return 4 + this.wids.size * 4;
}
/**
* @param {bio.BufferReader} br
* @returns {this}
*/
read(br) {
const count = br.readU32();
for (let i = 0; i < count; i++)
this.wids.add(br.readU32());
return this;
}
}
/*
* Expose
*/
exports.ChainState = ChainState;
exports.BlockMeta = BlockMeta;
exports.TXRecord = TXRecord;
exports.MapRecord = MapRecord;

2937
docs/js-wallet/rpc.js Normal file

File diff suppressed because it is too large Load diff

5305
docs/js-wallet/txdb.js Normal file

File diff suppressed because it is too large Load diff

5894
docs/js-wallet/wallet.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,203 @@
/*!
* walletcoinview.js - wallet coin viewpoint object for hsd
* Copyright (c) 2019, Boyma Fahnbulleh (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const assert = require('bsert');
const {BufferMap} = require('buffer-map');
const Paths = require('./paths');
const CoinView = require('../coins/coinview');
/** @typedef {import('../types').Hash} Hash */
/** @typedef {import('../primitives/outpoint')} Outpoint */
/** @typedef {import('../primitives/input')} Input */
/** @typedef {import('../primitives/coin')} Coin */
/** @typedef {import('../coins/coins')} Coins */
/** @typedef {import('./path')} Path */
/**
* Wallet Coin View
* Represents a wallet, coin viewpoint: a snapshot of {@link Coins} objects
* and the HD paths for their associated keys.
* @alias module:wallet.WalletCoinView
*/
class WalletCoinView extends CoinView {
/**
* Create a wallet coin view.
* @constructor
*/
constructor() {
super();
this.paths = new BufferMap();
}
/**
* Inject properties from coin view object.
* @private
* @param {CoinView} view
*/
fromCoinView(view) {
assert(view instanceof CoinView, 'View must be instance of CoinView');
this.map = view.map;
this.undo = view.undo;
this.bits = view.bits;
return this;
}
/**
* Instantiate wallet coin view from coin view.
* @param {CoinView} view
* @returns {WalletCoinView}
*/
static fromCoinView(view) {
return new this().fromCoinView(view);
}
/**
* Add paths to the collection.
* @param {Hash} hash
* @param {Paths} paths
* @returns {Paths|null}
*/
addPaths(hash, paths) {
this.paths.set(hash, paths);
return paths;
}
/**
* Get paths.
* @param {Hash} hash
* @returns {Paths} paths
*/
getPaths(hash) {
return this.paths.get(hash);
}
/**
* Test whether the view has a paths entry.
* @param {Hash} hash
* @returns {Boolean}
*/
hasPaths(hash) {
return this.paths.has(hash);
}
/**
* Ensure existence of paths object in the collection.
* @param {Hash} hash
* @returns {Paths}
*/
ensurePaths(hash) {
const paths = this.paths.get(hash);
if (paths)
return paths;
return this.addPaths(hash, new Paths());
}
/**
* Remove paths from the collection.
* @param {Hash} hash
* @returns {Paths|null}
*/
removePaths(hash) {
const paths = this.paths.get(hash);
if (!paths)
return null;
this.paths.delete(hash);
return paths;
}
/**
* Add an HD path to the collection.
* @param {Outpoint} prevout
* @param {Path} path
* @returns {Path|null}
*/
addPath(prevout, path) {
const {hash, index} = prevout;
const paths = this.ensurePaths(hash);
return paths.add(index, path);
}
/**
* Get an HD path by prevout.
* @param {Outpoint} prevout
* @returns {Path|null}
*/
getPath(prevout) {
const {hash, index} = prevout;
const paths = this.getPaths(hash);
if (!paths)
return null;
return paths.get(index);
}
/**
* Remove an HD path.
* @param {Outpoint} prevout
* @returns {Path|null}
*/
removePath(prevout) {
const {hash, index} = prevout;
const paths = this.getPaths(hash);
if (!paths)
return null;
return paths.remove(index);
}
/**
* Test whether the view has a path by prevout.
* @param {Outpoint} prevout
* @returns {Boolean}
*/
hasPath(prevout) {
const {hash, index} = prevout;
const paths = this.getPaths(hash);
if (!paths)
return false;
return paths.has(index);
}
/**
* Get a single path by input.
* @param {Input} input
* @returns {Path|null}
*/
getPathFor(input) {
return this.getPath(input.prevout);
}
}
/*
* Expose
*/
module.exports = WalletCoinView;

3084
docs/js-wallet/walletdb.js Normal file

File diff suppressed because it is too large Load diff

190
docs/js-wallet/walletkey.js Normal file
View file

@ -0,0 +1,190 @@
/*!
* walletkey.js - walletkey object for hsd
* Copyright (c) 2017-2018, Christopher Jeffrey (MIT License).
* https://github.com/handshake-org/hsd
*/
'use strict';
const KeyRing = require('../primitives/keyring');
const Path = require('./path');
/** @typedef {import('../hd/private')} HDPrivateKey */
/** @typedef {import('../hd/public')} HDPublicKey */
/** @typedef {import('../protocol/network')} Network */
/** @typedef {import('./account')} Account */
/**
* Wallet Key
* Represents a key ring which amounts to an address.
* @alias module:wallet.WalletKey
* @extends KeyRing
*/
class WalletKey extends KeyRing {
/**
* Create a wallet key.
* @constructor
* @param {Object?} options
*/
constructor(options) {
super(options);
this.keyType = Path.types.HD;
this.name = null;
this.account = -1;
this.branch = -1;
this.index = -1;
}
/**
* Convert an WalletKey to a more json-friendly object.
* @param {Network} [network]
* @returns {Object}
*/
getJSON(network) {
return {
name: this.name,
account: this.account,
branch: this.branch,
index: this.index,
publicKey: this.publicKey.toString('hex'),
script: this.script ? this.script.toHex() : null,
address: this.getAddress().toString(network)
};
}
/**
* Inject properties from hd key.
* @param {Account} account
* @param {HDPrivateKey|HDPublicKey} key
* @param {Number} branch
* @param {Number} index
* @returns {this}
*/
fromHD(account, key, branch, index) {
this.keyType = Path.types.HD;
this.name = account.name;
this.account = account.accountIndex;
this.branch = branch;
this.index = index;
if (key.privateKey)
return this.fromPrivate(key.privateKey);
return this.fromPublic(key.publicKey);
}
/**
* Instantiate a wallet key from hd key.
* @param {Account} account
* @param {HDPrivateKey|HDPublicKey} key
* @param {Number} branch
* @param {Number} index
* @returns {WalletKey}
*/
static fromHD(account, key, branch, index) {
return new this().fromHD(account, key, branch, index);
}
/**
* Inject properties from imported data.
* @param {Account} account
* @param {Buffer} data
* @returns {WalletKey}
*/
fromImport(account, data) {
this.keyType = Path.types.KEY;
this.name = account.name;
this.account = account.accountIndex;
return this.decode(data);
}
/**
* Instantiate a wallet key from imported data.
* @param {Account} account
* @param {Buffer} data
* @returns {WalletKey}
*/
static fromImport(account, data) {
return new this().fromImport(account, data);
}
/**
* Inject properties from key.
* @private
* @param {Account} account
* @param {KeyRing} ring
* @returns {WalletKey}
*/
fromRing(account, ring) {
this.keyType = Path.types.KEY;
this.name = account.name;
this.account = account.accountIndex;
return this.fromOptions(ring);
}
/**
* Instantiate a wallet key from regular key.
* @param {Account} account
* @param {KeyRing} ring
* @returns {WalletKey}
*/
static fromRing(account, ring) {
return new this().fromRing(account, ring);
}
/**
* Convert wallet key to a path.
* @returns {Path}
*/
toPath() {
const path = new Path();
path.name = this.name;
path.account = this.account;
switch (this.keyType) {
case Path.types.HD:
path.branch = this.branch;
path.index = this.index;
break;
case Path.types.KEY:
path.data = this.encode();
break;
}
path.keyType = this.keyType;
path.version = this.getVersion();
path.hash = this.getHash();
return path;
}
/**
* Test whether an object is a WalletKey.
* @param {Object} obj
* @returns {Boolean}
*/
static isWalletKey(obj) {
return obj instanceof WalletKey;
}
}
/*
* Expose
*/
module.exports = WalletKey;