@node-dlc/wire
Version:
Lightning Network Wire Protocol
246 lines • 10.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChannelRangeQuery = exports.ChannelRangeQueryState = void 0;
const QueryChannelRangeMessage_1 = require("../messages/QueryChannelRangeMessage");
const GossipError_1 = require("./GossipError");
var ChannelRangeQueryState;
(function (ChannelRangeQueryState) {
ChannelRangeQueryState[ChannelRangeQueryState["Idle"] = 0] = "Idle";
ChannelRangeQueryState[ChannelRangeQueryState["Active"] = 1] = "Active";
ChannelRangeQueryState[ChannelRangeQueryState["Complete"] = 2] = "Complete";
ChannelRangeQueryState[ChannelRangeQueryState["Failed"] = 3] = "Failed";
})(ChannelRangeQueryState = exports.ChannelRangeQueryState || (exports.ChannelRangeQueryState = {}));
/**
* Performs a single query_channel_range operation and encapsulates the state
* machine performed during the query operations.
*
* A single query_channel_range may be too large to fit in a single
* reply_channel_range response. When this happens, there will be multiple
* reply_channel_range responses to address the query_channel_range message.
*
* There are two modes of replies: Legacy LND and Standard.
*
* Standard was defined in changes to BOLT7 that were merged in with #730. These
* changes clarified the meaning of the reply fields to ensure the recipient
* of replies knows when the query has successfully completed. This includes:
*
* - a node must not submit a new query_channel_range until the full set of
* reply_channel_range messages covering the range are received.
*
* - clarified the meaning of full_information (previously complete) to
* indicate that the responding node contains complete information for the
* chain_hash, not that the reply is complete.
*
* - enforces that when there are multiple reply_channel_range msgs, the
* range of blocks must be strictly increasing until the full query range is
* covered. This range may exceed (both below and above) the request range.
* This means:
* - the first reply first_blocknum must be <= requests first_blocknum
* - subsequent first_blocknum will be strictly increasing from the
* previous reply's first_blocknum+number_of_blocks
* - the final first_blocknum+number_of_blocks must be >= the query's
* first_blocknum+number_of_blocks
*
* This is functionality is a clarification from Legacy LND mode where
* gossip_queries was implemented in a different manner.
*
* - full_information indicated a multipart message that was incomplete
* - lack of short_channel_ids and full_information=false can be treated
* as a failure condition
*/
class ChannelRangeQuery {
constructor(chainHash, messageSender, logger, isLegacy = false) {
this.chainHash = chainHash;
this.messageSender = messageSender;
this.logger = logger;
this._isLegacy = false;
this._results = [];
this._state = ChannelRangeQueryState.Idle;
this._isLegacy = isLegacy;
}
/**
* Returns true if we detect this is using the legacy querying gossip_queries
* mechanism that was originally implemented in LND. This code may be able to
* be removed eventually.
*/
get isLegacy() {
return this._isLegacy;
}
/**
* Gets the current state of the Query object
*/
get state() {
return this._state;
}
/**
* Results found
*/
get results() {
return this._results;
}
/**
* Gets the error that was encountered during processing
*/
get error() {
return this._error;
}
/**
* Handles a reply_channel_range message. May initiate a message
* broadcast.
*/
handleReplyChannelRange(msg) {
// check the incoming message to see if we need to transition to legacy
// mode. If it is determined to be in legacy mode, we will switch the
// strategy that is used to handle the reply.
if (!this._isLegacy && this._isLegacyReply(msg)) {
this._isLegacy = true;
this.logger.info('using legacy LND query_channel_range technique');
}
// handle the message according to which state the reply system is working
if (this._isLegacy) {
this._handleLegacyReply(msg);
}
else {
this._handleReply(msg);
}
}
/**
* Resolves when the query has completed, otherwise it will reject on an
* error.
*/
queryRange(firstBlock = 0, numBlocks = 4294967295 - firstBlock) {
// Construct a promise that will be resolved after all query logic has
// succeeded or failed. This is a slightly different pattern in that
// we use an private event emitter to signal completion or failure
// asynchronously. We use those handlers to resolve the promise. As a
// result, the external interface is very clean, but we can have
// complicated internal operations
return new Promise((resolve, reject) => {
// transition the state to active
this._state = ChannelRangeQueryState.Active;
// send the query message and start the process
this._sendQuery(firstBlock, numBlocks);
// capture the promise methods so we can invoke them from
// within our state machine.
this._resolve = resolve;
this._reject = reject;
});
}
/**
* Idempotent method that marks the state machine failed
* @param error
*/
_transitionFailed(error) {
if (this._state !== ChannelRangeQueryState.Active)
return;
this._error = error;
this._state = ChannelRangeQueryState.Failed;
this._reject(error);
}
/**
* Idempotent method that marks the state machine complete
*/
_transitionComplete() {
if (this._state !== ChannelRangeQueryState.Active)
return;
this._state = ChannelRangeQueryState.Complete;
this._resolve(this._results);
}
/**
* Constructs and sends the query message the remote peer.
* @param firstBlock
* @param numBlocks
*/
_sendQuery(firstBlock, numBlocks) {
this.logger.info('sending query_channel_range start_block=%d end_block=%d', firstBlock, firstBlock + numBlocks - 1);
// send message
const msg = new QueryChannelRangeMessage_1.QueryChannelRangeMessage();
msg.chainHash = this.chainHash;
msg.firstBlocknum = firstBlock;
msg.numberOfBlocks = numBlocks;
this.messageSender.sendMessage(msg);
// capture the active query to check reply if it is a legacy reply
this._query = msg;
}
/**
* Check if this has the signature of a legacy reply. We can detect this by
* looking at a complete=false and that scids exist.
* @param msg
*/
_isLegacyReply(msg) {
return !msg.fullInformation && msg.shortChannelIds.length > 0;
}
/**
* Handles a reply_channel_range message which ensures that the entire queried
* range has been received. The responder can reply with pre-sized ranges
* which means the reply range may not be the EXACT range requested but will
* include the queried range.
*
* For a query range with first_blocknum and number_of_blocks arguments,
* we can expect messages to have the following:
*
* - first reply first_blocknum <= requested first_blocknum
* - intermediate replies sequentially ordered so that first_blocknum is the
* first_blocknum + number_of_blocks from previous reply (strictly ordered)
* - last reply has fist_blocknum + number_of_blocks >= the queries
* first_blocknum + number_of_blocks
*
* This ordering allows us to know when a message is complete. If a reply has
* full_information=false, then the remote peer does not maintain a
* up-to-date information for the supplied chain_hash.
* @param msg
*/
_handleReply(msg) {
this.logger.debug('received reply_channel_range - full_info=%d start_block=%d end_block=%d scid_count=%d', msg.fullInformation, msg.firstBlocknum, msg.firstBlocknum + msg.numberOfBlocks - 1, msg.shortChannelIds.length);
// enqueues any scids to be processed by a query_short_chan_id message
if (msg.shortChannelIds.length) {
this._results.push(...msg.shortChannelIds);
}
// The full_information flag should only return false when the remote peer
// does not maintain up-to-date information for the request chain_hash
if (!msg.fullInformation) {
const error = new GossipError_1.GossipError(GossipError_1.GossipErrorCode.ReplyChannelRangeNoInformation, msg);
this._transitionFailed(error);
return;
}
// We can finished when we have received a reply that covers the full range
// of requested data. We know the final block height will be the querie's
// first_blocknum + number_of_blocks.
const currentHeight = msg.firstBlocknum + msg.numberOfBlocks;
const targetHeight = this._query.firstBlocknum + this._query.numberOfBlocks;
if (currentHeight >= targetHeight) {
this.logger.debug('received final reply_channel_range height %d >= query_channel_range height %d', currentHeight, targetHeight);
this._transitionComplete();
return;
}
}
/**
* Handles a reply_channel_range message using the legacy strategy. This
* code will error if fullInformation=0 scids=[] and will be considered
* complete when fullInformation=1.
* @param msg
*/
_handleLegacyReply(msg) {
this.logger.debug('received reply_channel_range - full_info=%d start_block=%d end_block=%d scid_count=%d', msg.fullInformation, msg.firstBlocknum, msg.firstBlocknum + msg.numberOfBlocks - 1, msg.shortChannelIds.length);
// enqueues any scids to be processed by a query_short_chan_id message
if (msg.shortChannelIds.length) {
this.results.push(...msg.shortChannelIds);
}
// Check the complete flag and the existance of SCIDs. Unfortunately,
// non-confirming implementations are incorrectly using the completion
// flag to a multi-message reply.
if (msg.fullInformation && !this.results.length) {
const error = new GossipError_1.GossipError(GossipError_1.GossipErrorCode.ReplyChannelRangeNoInformation, msg);
this._transitionFailed(error);
return;
}
// If we see a fullInformation flag then we have received all parts of
// the multipart message and are complete.
if (msg.fullInformation) {
this._transitionComplete();
return;
}
}
}
exports.ChannelRangeQuery = ChannelRangeQuery;
//# sourceMappingURL=ChannelRangeQuery.js.map