UNPKG

@node-dlc/wire

Version:
246 lines 10.9 kB
"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