UNPKG

@node-dlc/wire

Version:
294 lines (256 loc) 10.5 kB
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]); } }