UNPKG

@node-lightning/wire

Version:
226 lines 11 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GossipFilter = exports.Result = void 0; const core_1 = require("@node-lightning/core"); const ChannelAnnouncementMessage_1 = require("../messages/ChannelAnnouncementMessage"); const ChannelUpdateMessage_1 = require("../messages/ChannelUpdateMessage"); const ExtendedChannelAnnouncementMessage_1 = require("../messages/ExtendedChannelAnnouncementMessage"); const NodeAnnouncementMessage_1 = require("../messages/NodeAnnouncementMessage"); const MessageType_1 = require("../MessageType"); const ScriptUtils_1 = require("../ScriptUtils"); const WireError_1 = require("../WireError"); class Result { constructor(value, error) { this.value = value; this.error = error; } static err(error) { return new Result(undefined, error); } static ok(value) { return new Result(value); } get isOk() { return this.value !== undefined; } get isErr() { return this.error !== undefined; } } exports.Result = Result; /** * GossipFilter recieves messages from peers and performs validation * on the messages to ensure that they are valid messages. These validation * follow messaging rules defined in Bolt #7 and include things like * signature checks, on-chain validation, and message sequencing requirements. * Successful message validation results in messages being written to an * instance of IGossipStore and returned as results * * The GossipFilter will also store pending messages, such as channel_update * message arriving before the channel_announcement. */ class GossipFilter { constructor(gossipStore, pendingStore, chainClient) { this.gossipStore = gossipStore; this.pendingStore = pendingStore; this.chainClient = chainClient; } /** * Validates a message and writes it to the appropriate store. * A fully processed messages (or releated messages) will be * returned when a message is successfully processed. * */ async validateMessage(msg) { switch (msg.type) { case MessageType_1.MessageType.ChannelAnnouncement: return await this._validateChannelAnnouncement(msg); case MessageType_1.MessageType.NodeAnnouncement: return await this._validateNodeAnnouncement(msg); case MessageType_1.MessageType.ChannelUpdate: return await this._validateChannelUpdate(msg); } return Result.ok([]); } /** * Validate a node announcement message by checking to see if the * message is newer than the prior timestamp and if the message * has a valid signature from the corresponding node */ async _validateNodeAnnouncement(msg) { // get or construct a node const existing = await this.gossipStore.findNodeAnnouncement(msg.nodeId); // check if the message is newer than the last update if (existing && existing.timestamp >= msg.timestamp) return Result.ok([]); // queue node if we don't have any channels const scids = await this.gossipStore.findChannelsForNode(msg.nodeId); if (!scids.length) { await this.pendingStore.saveNodeAnnouncement(msg); return Result.ok([]); } // validate message signature if (!NodeAnnouncementMessage_1.NodeAnnouncementMessage.verifySignatures(msg)) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.nodeAnnSigFailed, [msg])); } // save the announcement await this.gossipStore.saveNodeAnnouncement(msg); // broadcast valid message return Result.ok([msg]); } /** * Validates a ChannelAnnouncementMessage by verifying the signatures * and validating the transaction on chain work. This message will */ async _validateChannelAnnouncement(msg) { // attempt to find the existing chan_ann message const existing = await this.gossipStore.findChannelAnnouncement(msg.shortChannelId); // If the message is an extended message and it has populated the outpoint and capacity we // can skip message processing becuase there is nothing left to do. if (existing && existing instanceof ExtendedChannelAnnouncementMessage_1.ExtendedChannelAnnouncementMessage) { return Result.ok([]); } // If there is an existing message and we don't have a chain_client then we want // to abort processing to prevent from populating the gossip store with chan_ann // messages that do not have a extended information. Alterntaively, if we DO have an // an existing message that is just a chan_ann and not ext_chan_ann and there is a // chain_client, we want to update the chan_ann with onchain information if (existing && !this.chainClient) { return Result.ok([]); } // validate signatures for message if (!ChannelAnnouncementMessage_1.ChannelAnnouncementMessage.verifySignatures(msg)) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanAnnSigFailed, [msg])); } if (this.chainClient) { // load the block hash for the block height const blockHash = await this.chainClient.getBlockHash(msg.shortChannelId.block); if (!blockHash) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanBadBlockHash, [msg])); } // load the block details so we can find the tx const block = await this.chainClient.getBlock(blockHash); if (!block) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanBadBlock, [msg, blockHash])); } // load the txid from the block details const txId = block.tx[msg.shortChannelId.txIdx]; if (!txId) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanAnnBadTx, [msg])); } // obtain a UTXO to verify the tx hasn't been spent yet const utxo = await this.chainClient.getUtxo(txId, msg.shortChannelId.voutIdx); if (!utxo) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanUtxoSpent, [msg])); } // verify the tx script is a p2ms const expectedScript = ScriptUtils_1.fundingScript([msg.bitcoinKey1, msg.bitcoinKey2]); const actualScript = Buffer.from(utxo.scriptPubKey.hex, "hex"); if (!expectedScript.equals(actualScript)) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanBadScript, [msg, expectedScript, actualScript])); } // construct outpoint const outpoint = new core_1.OutPoint(core_1.HashValue.fromRpc(txId), msg.shortChannelId.voutIdx); // calculate capacity in satoshi // Not sure about this code. MAX_SAFE_INTEGER is still safe // for Bitcoin satoshi. const capacity = BigInt(Math.round(utxo.value * 10 ** 8)); // overright msg with extended channel_announcement const extended = ExtendedChannelAnnouncementMessage_1.ExtendedChannelAnnouncementMessage.fromMessage(msg); extended.outpoint = outpoint; extended.capacity = capacity; msg = extended; } // save channel_ann await this.gossipStore.saveChannelAnnouncement(msg); // broadcast valid message const results = [msg]; // process outstanding node messages const pendingUpdates = [ await this.pendingStore.findChannelUpdate(msg.shortChannelId, 0), await this.pendingStore.findChannelUpdate(msg.shortChannelId, 1), ]; for (const pendingUpdate of pendingUpdates) { if (pendingUpdate) { await this.pendingStore.deleteChannelUpdate(pendingUpdate.shortChannelId, pendingUpdate.direction); const result = await this._validateChannelUpdate(pendingUpdate); if (result.isErr) return result; else results.push(...result.value); } } // process outstanding node messages const pendingNodeAnns = [ await this.pendingStore.findNodeAnnouncement(msg.nodeId1), await this.pendingStore.findNodeAnnouncement(msg.nodeId2), ]; for (const pendingNodeAnn of pendingNodeAnns) { if (pendingNodeAnn) { await this.pendingStore.deleteNodeAnnouncement(pendingNodeAnn.nodeId); const result = await this._validateNodeAnnouncement(pendingNodeAnn); if (result.isErr) return result; else results.push(...result.value); } } return Result.ok(results); } /** * Validates a channel_update message by ensuring that: * - the channel_announcement has already been recieved * - the channel_update is not old * - the channel_update is correctly signed by the node */ async _validateChannelUpdate(msg) { // Ensure a channel announcement exists. If it does not, // we need to queue the update message until the channel announcement // can be adequately processed. Technically according to the specification in // Bolt 07, we should ignore channel_update message if we not processed // a valid channel_announcement. In reality, we may end up in a situation // where a channel_update makes it to our peer prior to the channel_announcement // being received. const channelMessage = await this.gossipStore.findChannelAnnouncement(msg.shortChannelId); if (!channelMessage) { await this.pendingStore.saveChannelUpdate(msg); return Result.ok([]); } // ignore out of date message const existingUpdate = await this.gossipStore.findChannelUpdate(msg.shortChannelId, msg.direction); if (existingUpdate && existingUpdate.timestamp >= msg.timestamp) { return Result.ok([]); } // validate message signature for the node in const nodeId = msg.direction === 0 ? channelMessage.nodeId1 : channelMessage.nodeId2; if (!ChannelUpdateMessage_1.ChannelUpdateMessage.validateSignature(msg, nodeId)) { return Result.err(new WireError_1.WireError(WireError_1.WireErrorCode.chanUpdSigFailed, [msg, nodeId])); } // save the message await this.gossipStore.saveChannelUpdate(msg); // broadcast valid message return Result.ok([msg]); } } exports.GossipFilter = GossipFilter; //# sourceMappingURL=GossipFilter.js.map