Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Neutrino Client #1163

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
5 changes: 4 additions & 1 deletion bin/bcoin
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ for arg in "$@"; do
--daemon)
daemon=1
;;
--spv)
--neutrino)
cmd='neutrino'
;;
--spv)
cmd='spvnode'
;;
esac
Expand Down
20 changes: 20 additions & 0 deletions bin/bcoin-cli
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,22 @@ class CLI {
this.log(filter);
}

async getFilterHeader() {
let hash = this.config.str(0, '');

if (hash.length !== 64)
hash = parseInt(hash, 10);

const filterHeader = await this.client.getFilterHeader(hash);

if (!filterHeader) {
this.log('Filter header not found.');
return;
}

this.log(filterHeader);
}

async estimateFee() {
const blocks = this.config.uint(0, 1);

Expand Down Expand Up @@ -246,6 +262,9 @@ class CLI {
case 'filter':
await this.getFilter();
break;
case 'filterheader':
await this.getFilterHeader();
break;
case 'fee':
await this.estimateFee();
break;
Expand All @@ -263,6 +282,7 @@ class CLI {
this.log(' $ coin [hash+index/address]: View coins.');
this.log(' $ fee [target]: Estimate smart fee.');
this.log(' $ filter [hash/height]: View filter.');
this.log(' $ filterheader [hash/height]: View filter header.');
this.log(' $ header [hash/height]: View block header.');
this.log(' $ info: Get server info.');
this.log(' $ mempool: Get mempool snapshot.');
Expand Down
43 changes: 43 additions & 0 deletions bin/neutrino
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env node

'use strict';

console.log('Starting bcoin');
process.title = 'bcoin';
const Neutrino = require('../lib/node/neutrino');

const node = new Neutrino({
file: true,
argv: true,
env: true,
logFile: true,
logConsole: true, // todo: remove
logLevel: 'debug', // todo: remove
db: 'leveldb',
memory: false,
workers: true,
loader: require
});

if (!node.config.bool('no-wallet') && !node.has('walletdb')) {
const plugin = require('../lib/wallet/plugin');
node.use(plugin);
}

(async () => {
await node.ensure();
await node.open();
await node.connect();
node.startSync();
})().catch((err) => {
console.error(err.stack);
process.exit(1);
});

process.on('unhandledRejection', (err, promise) => {
throw err;
});

process.on('SIGINT', async () => {
await node.close();
});
1 change: 1 addition & 0 deletions lib/bcoin-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ bcoin.node = require('./node');
bcoin.Node = require('./node/node');
bcoin.FullNode = require('./node/fullnode');
bcoin.SPVNode = require('./node/spvnode');
bcoin.Neutrino = require('./node/neutrino');

// Primitives
bcoin.primitives = require('./primitives');
Expand Down
1 change: 1 addition & 0 deletions lib/bcoin.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ bcoin.define('node', './node');
bcoin.define('Node', './node/node');
bcoin.define('FullNode', './node/fullnode');
bcoin.define('SPVNode', './node/spvnode');
bcoin.define('Neutrino', './node/neutrino');

// Primitives
bcoin.define('primitives', './primitives');
Expand Down
78 changes: 72 additions & 6 deletions lib/blockchain/chain.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class Chain extends AsyncEmitter {

this.orphanMap = new BufferMap();
this.orphanPrev = new BufferMap();

this.getPrunedMap = new BufferMap();
}

/**
Expand Down Expand Up @@ -1368,7 +1370,17 @@ class Chain extends AsyncEmitter {
}

// Do we already have this block?
if (await this.hasEntry(hash)) {
const existingEntry = await this.getEntry(hash);

if (existingEntry && this.getPrunedMap.has(hash)) {
block = block.toBlock();
await this.db.updateNeutrinoSave();
await this.db.save(existingEntry, block, new CoinView());
await this.db.updateNeutrinoSave();
return existingEntry;
}

if (existingEntry) {
this.logger.debug('Already have block: %h.', block.hash());
throw new VerifyError(block, 'duplicate', 'duplicate', 0);
}
Expand Down Expand Up @@ -1791,6 +1803,24 @@ class Chain extends AsyncEmitter {
return this.hasEntry(hash);
}

async getCFHeaderHeight() {
return await this.db.getCFHeaderHeight();
}

async saveCFHeaderHeight(height) {
this.db.neutrinoState.headerHeight = height;
await this.db.saveNeutrinoState();
}

async getCFilterHeight() {
return await this.db.getCFilterHeight();
}

async saveCFilterHeight(height) {
this.db.neutrinoState.filterHeight = height;
await this.db.saveNeutrinoState();
}

/**
* Find the corresponding block entry by hash or height.
* @param {Hash|Number} hash/height
Expand Down Expand Up @@ -1925,6 +1955,33 @@ class Chain extends AsyncEmitter {
return this.db.getBlock(hash);
}

async getBlockPeer(hash) {
let block = await this.db.getBlock(hash);
if (block) {
const entry = await this.getEntry(hash);
assert(entry.hash.equals(hash));
return block;
} else {
this.logger.warning('Block not found, attempting to download');

// Ensure hash not height
hash = await this.db.getHash(hash);

const wait = new Promise((resolve, reject) => {
this.getPrunedMap.set(hash, resolve);
});

await this.emitAsync('getprunedblock', hash);
await wait;
block = await this.db.getBlock(hash);
const entry = await this.getEntry(hash);
assert(entry.hash.equals(hash));
Comment on lines +1974 to +1978
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK! Clever. We should make sure this isn't blocking the event loop etc

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we check that?


this.emit('getblockpeer', entry, block);
return block;
}
}

/**
* Retrieve a block from the database (not filled with coins).
* @param {Hash} block
Expand Down Expand Up @@ -2003,19 +2060,22 @@ class Chain extends AsyncEmitter {
if (this.synced)
return;

if (this.options.neutrino && this.getProgress() < 1)
return;
if (this.options.checkpoints) {
if (this.height < this.network.lastCheckpoint)
return;
}

if (this.tip.time < util.now() - this.network.block.maxTipAge)
} else if (!this.options.neutrino &&
this.tip.time < util.now() - this.network.block.maxTipAge)
return;

if (!this.hasChainwork())
return;

this.synced = true;
this.emit('full');
if (this.options.neutrino)
this.emit('headersFull');
else
this.emit('full');
}

/**
Expand Down Expand Up @@ -2616,6 +2676,7 @@ class ChainOptions {
this.compression = true;

this.spv = false;
this.neutrino = false;
this.bip91 = false;
this.bip148 = false;
this.prune = false;
Expand Down Expand Up @@ -2662,6 +2723,11 @@ class ChainOptions {
this.spv = options.spv;
}

if (options.neutrino != null) {
assert(typeof options.neutrino === 'boolean');
this.neutrino = options.neutrino;
}

if (options.prefix != null) {
assert(typeof options.prefix === 'string');
this.prefix = options.prefix;
Expand Down
74 changes: 72 additions & 2 deletions lib/blockchain/chaindb.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class ChainDB {
this.state = new ChainState();
this.pending = null;
this.current = null;
this.neutrinoState = null;
this.neutrinoSave = false;

this.cacheHash = new LRU(this.options.entryCache, null, BufferMap);
this.cacheHeight = new LRU(this.options.entryCache);
Expand Down Expand Up @@ -90,6 +92,11 @@ class ChainDB {
this.logger.info('ChainDB successfully initialized.');
}

if (this.options.neutrino) {
if (!this.neutrinoState)
this.neutrinoState = await this.getNeutrinoState();
}

this.logger.info(
'Chain State: hash=%h tx=%d coin=%d value=%s.',
this.state.tip,
Expand Down Expand Up @@ -1001,7 +1008,7 @@ class ChainDB {
*/

async getRawBlock(block) {
if (this.options.spv)
if (this.options.spv && !this.options.neutrino)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm what's wrong with this line? We can't get raw blocks in neutrino mode

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need this in order to check if a requested block by wallet already exists in the database or not, in order to skip fetching it if it exists.

return null;

const hash = await this.getHash(block);
Expand Down Expand Up @@ -1150,6 +1157,14 @@ class ChainDB {
* @returns {Promise}
*/

async updateNeutrinoSave () {
if (this.neutrinoSave) {
this.neutrinoSave = false;
} else {
this.neutrinoSave = true;
}
}

Comment on lines +1160 to +1167
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its to update the neutrinoSave flag. So that whenever we are not saving a pruned block, it skips saveBlock

async save(entry, block, view) {
this.start();
try {
Expand Down Expand Up @@ -1478,7 +1493,7 @@ class ChainDB {
async saveBlock(entry, block, view) {
const hash = block.hash();

if (this.options.spv)
if (this.options.spv && !this.neutrinoSave)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question here - SPV mode does just fine without calling saveBlock() Neutrino should be the same. Don't we save ChainEntry (headers) outside this function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we get a pruned block on request by wallet, without the neutrinoSave flag, the chain skips saving the block.

return;

// Write actual block data.
Expand Down Expand Up @@ -1670,6 +1685,39 @@ class ChainDB {
b.put(layout.O.encode(), flags.toRaw());
return b.write();
}

/**
* Get Neutrino State
* @returns {Promise<NeutrinoState>} - Returns neutrino state
*/

async getNeutrinoState() {
const data = await this.db.get(layout.N.encode());
if (!data)
return new NeutrinoState();
return NeutrinoState.fromRaw(data);
}

async getCFHeaderHeight() {
const state = await this.getNeutrinoState();
return state.headerHeight;
}

async getCFilterHeight() {
const state = await this.getNeutrinoState();
return state.filterHeight;
}

/**
* Save Neutrino State
* @returns {void}
*/
async saveNeutrinoState() {
const state = this.neutrinoState.toRaw();
const b = this.db.batch();
b.put(layout.N.encode(), state);
return b.write();
}
Comment on lines +1715 to +1720
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

depending on when we call this function, we might want to accept a batch as the argument, assuming it will be committed later along with other data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean. Could you please explain further?

}

/**
Expand Down Expand Up @@ -1952,6 +2000,28 @@ function fromU32(num) {
return data;
}

class NeutrinoState {
constructor() { // TODO: do we add support for multiple filters?
this.headerHeight = 0;
this.filterHeight = 0;
}

toRaw() {
const bw = bio.write(8);
bw.writeU32(this.headerHeight);
bw.writeU32(this.filterHeight);
return bw.render();
}

static fromRaw(data) {
const state = new NeutrinoState();
const br = bio.read(data);
state.headerHeight = br.readU32();
state.filterHeight = br.readU32();
return state;
}
}

/*
* Expose
*/
Expand Down
3 changes: 3 additions & 0 deletions lib/blockchain/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const bdb = require('bdb');
* O -> chain options
* R -> tip hash
* D -> versionbits deployments
* N -> neutrino state
* e[hash] -> entry
* h[hash] -> height
* H[height] -> hash
Expand All @@ -33,6 +34,8 @@ const layout = {
O: bdb.key('O'),
R: bdb.key('R'),
D: bdb.key('D'),
N: bdb.key('N'),
F: bdb.key('H', ['hash256']),
e: bdb.key('e', ['hash256']),
h: bdb.key('h', ['hash256']),
H: bdb.key('H', ['uint32']),
Expand Down
Loading