import { ShortChannelId } from "@node-lightning/core" ; import { ILogger } from "@node-lightning/logger" ; import { QueryChannelRangeMessage } from "../messages/QueryChannelRangeMessage" ; import { ReplyChannelRangeMessage } from "../messages/ReplyChannelRangeMessage" ; import { IMessageSender } from "../Peer" ; import { GossipError , GossipErrorCode } from "./GossipError" ; export enum ChannelRangeQueryState { Idle , Active , Complete , Failed } /**
 * 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
 */ export class ChannelRangeQuery { private _state : ChannelRangeQueryState ; private _isLegacy = false ; private _query : QueryChannelRangeMessage ; private _results : ShortChannelId [ ] = [ ] ; private _error : GossipError ; private _resolve : ( scids : ShortChannelId [ ] ) => void ; private _reject : ( reason : unknown ) => void ; constructor ( readonly chainHash , readonly messageSender , readonly logger , isLegacy = false ) { 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.
     */ public get isLegacy ( ) : boolean { return this . _isLegacy ; } /**
     * Gets the current state of the Query object
     */ public get state ( ) : ChannelRangeQueryState { return this . _state ; } /**
     * Results found
     */ public get results ( ) : ShortChannelId [ ] { return this . _results ; } /**
     * Gets the error that was encountered during processing
     */ public get error ( ) : GossipError { return this . _error ; } /**
     * Handles a reply_channel_range message. May initiate a message
     * broadcast.
     */ public handleReplyChannelRange ( msg ) { if ( ! this . _isLegacy && this . _isLegacyReply ( msg ) ) { this . _isLegacy = true ; this . logger . info ( "using legacy LND query_channel_range technique" ) ; } if ( this . _isLegacy ) { this . _handleLegacyReply ( msg ) ; } else { this . _handleReply ( msg ) ; } } /**
     * Resolves when the query has completed, otherwise it will reject on an
     * error.
     */ public queryRange ( firstBlock = 0 , numBlocks = 4294967295 - firstBlock ) { return new Promise ( ( resolve , reject ) => { this . _state = ChannelRangeQueryState . Active ; this . _sendQuery ( firstBlock , numBlocks ) ; this . _resolve = resolve ; this . _reject = reject ; } ) ; } param error private _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
     */ private _transitionComplete ( ) { if ( this . _state !== ChannelRangeQueryState . Active ) return ; this . _state = ChannelRangeQueryState . Complete ; this . _resolve ( this . _results ) ; } param firstBlock param numBlocks private _sendQuery ( firstBlock , numBlocks ) { this . logger . info ( "sending query_channel_range start_block=%d end_block=%d" , firstBlock , firstBlock + numBlocks - 1 ) ; const msg = new QueryChannelRangeMessage ( ) ; msg . chainHash = this . chainHash ; msg . firstBlocknum = firstBlock ; msg . numberOfBlocks = numBlocks ; this . messageSender . sendMessage ( msg ) ; this . _query = msg ; } param msg private _isLegacyReply ( msg ) { return ! msg . fullInformation && msg . shortChannelIds . length > 0 ; } param msg private _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 ) ; if ( msg . shortChannelIds . length ) { this . _results . push ( ... msg . shortChannelIds ) ; } if ( ! msg . fullInformation ) { const error = new GossipError ( GossipErrorCode . ReplyChannelRangeNoInformation , msg ) ; this . _transitionFailed ( error ) ; return ; } 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 ; } } param msg private _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 ) ; if ( msg . shortChannelIds . length ) { this . results . push ( ... msg . shortChannelIds ) ; } if ( msg . fullInformation && ! this . results . length ) { const error = new GossipError ( GossipErrorCode . ReplyChannelRangeNoInformation , msg ) ; this . _transitionFailed ( error ) ; return ; } if ( msg . fullInformation ) { this . _transitionComplete ( ) ; return ; } } }