@node-dlc/wire
Version:
Lightning Network Wire Protocol
294 lines (256 loc) • 10.5 kB
text/typescript
import { HashValue, OutPoint } from '@node-dlc/bitcoin';
import { ChannelAnnouncementMessage } from '../messages/ChannelAnnouncementMessage';
import { ChannelUpdateMessage } from '../messages/ChannelUpdateMessage';
import { ExtendedChannelAnnouncementMessage } from '../messages/ExtendedChannelAnnouncementMessage';
import { IWireMessage } from '../messages/IWireMessage';
import { NodeAnnouncementMessage } from '../messages/NodeAnnouncementMessage';
import { MessageType } from '../MessageType';
import { fundingScript } from '../ScriptUtils';
import { WireError, WireErrorCode } from '../WireError';
import { IGossipStore } from './GossipStore';
import { IGossipFilterChainClient } from './IGossipFilterChainClient';
export class Result<V, E> {
public static err<V, E>(error: E): Result<V, E> {
return new Result<V, E>(undefined, error);
}
public static ok<V, E>(value: V): Result<V, E> {
return new Result<V, E>(value);
}
constructor(readonly value?: V, readonly error?: E) {}
public get isOk(): boolean {
return this.value !== undefined;
}
public get isErr(): boolean {
return this.error !== undefined;
}
}
export type GossipFilterResult = Result<IWireMessage[], WireError>;
/**
* 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.
*/
export class GossipFilter {
constructor(
readonly gossipStore: IGossipStore,
readonly pendingStore: IGossipStore,
readonly chainClient?: IGossipFilterChainClient,
) {}
/**
* 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.
*
*/
public async validateMessage(msg: IWireMessage): Promise<GossipFilterResult> {
switch (msg.type) {
case MessageType.ChannelAnnouncement:
return await this._validateChannelAnnouncement(
msg as ChannelAnnouncementMessage,
);
case MessageType.NodeAnnouncement:
return await this._validateNodeAnnouncement(
msg as NodeAnnouncementMessage,
);
case MessageType.ChannelUpdate:
return await this._validateChannelUpdate(msg as ChannelUpdateMessage);
}
return Result.ok([] as IWireMessage[]);
}
/**
* 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
*/
private async _validateNodeAnnouncement(
msg: NodeAnnouncementMessage,
): Promise<GossipFilterResult> {
// 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([] as IWireMessage[]);
// 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([] as IWireMessage[]);
}
// validate message signature
if (!NodeAnnouncementMessage.verifySignatures(msg)) {
return Result.err(new WireError(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
*/
private async _validateChannelAnnouncement(
msg: ChannelAnnouncementMessage,
): Promise<GossipFilterResult> {
// 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) {
return Result.ok([] as IWireMessage[]);
}
// 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([] as IWireMessage[]);
}
// validate signatures for message
if (!ChannelAnnouncementMessage.verifySignatures(msg)) {
return Result.err(new WireError(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(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(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(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(WireErrorCode.chanUtxoSpent, [msg]));
}
// verify the tx script is a p2ms
const expectedScript = fundingScript([msg.bitcoinKey1, msg.bitcoinKey2]);
const actualScript = Buffer.from(utxo.scriptPubKey.hex, 'hex');
if (!expectedScript.equals(actualScript)) {
return Result.err(
new WireError(WireErrorCode.chanBadScript, [
msg,
expectedScript,
actualScript,
]),
);
}
// construct outpoint
const outpoint = new OutPoint(
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.fromMessage(msg);
extended.outpoint = outpoint;
extended.capacity = capacity;
msg = extended;
}
// save channel_ann
await this.gossipStore.saveChannelAnnouncement(msg);
// broadcast valid message
const results: IWireMessage[] = [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
*/
private async _validateChannelUpdate(
msg: ChannelUpdateMessage,
): Promise<GossipFilterResult> {
// 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([] as IWireMessage[]);
}
// ignore out of date message
const existingUpdate = await this.gossipStore.findChannelUpdate(
msg.shortChannelId,
msg.direction,
);
if (existingUpdate && existingUpdate.timestamp >= msg.timestamp) {
return Result.ok([] as IWireMessage[]);
}
// validate message signature for the node in
const nodeId =
msg.direction === 0 ? channelMessage.nodeId1 : channelMessage.nodeId2;
if (!ChannelUpdateMessage.validateSignature(msg, nodeId)) {
return Result.err(
new WireError(WireErrorCode.chanUpdSigFailed, [msg, nodeId]),
);
}
// save the message
await this.gossipStore.saveChannelUpdate(msg);
// broadcast valid message
return Result.ok([msg]);
}
}