@node-lightning/wire
Version:
Lightning Network Wire Protocol
226 lines • 11 kB
JavaScript
"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