@node-dlc/messaging
Version:
DLC Messaging Protocol
328 lines (275 loc) • 9.79 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Tx } from '@node-dlc/bitcoin';
import { Block, Transaction } from 'bitcoinjs-lib';
import { EventEmitter } from 'events';
import { DlcTransactions } from '../messages/DlcTransactions';
import { sleep } from '../util';
import { IDlcStore } from './DlcStore';
import {
HasHash,
HasHeight,
IChainFilterChainClient,
} from './IChainFilterChainClient';
export enum SyncState {
Unsynced,
Syncing,
Synced,
}
export interface ILogger {
area: string;
instance: string;
trace(...args: any[]): void;
debug(...args: any[]): void;
info(...args: any[]): void;
warn(...args: any[]): void;
error(...args: any[]): void;
sub(area: string, instance?: string): ILogger;
}
/**
* ChainManager validates and stores dlc txs
* from chain updates
*/
export class ChainManager extends EventEmitter {
public blockHeight: number;
public started = false;
public syncState: SyncState;
public isSynchronizing: boolean;
public chainClient: IChainFilterChainClient;
public logger: ILogger;
public dlcStore: IDlcStore;
public dlcTxsList: DlcTransactions[];
constructor(
logger: ILogger,
chainClient: IChainFilterChainClient,
dlcStore: IDlcStore,
) {
super();
this.logger = logger.sub('dlcmgr');
this.dlcStore = dlcStore;
this.chainClient = chainClient;
}
/**
* Starts the chain manager. This method will load information
* from the chain store, determine when the last information
* was obtained, validate the existing messages
*/
public async start(blockHeight = 0): Promise<void> {
this.logger.info('starting dlc state manager');
// wait for chain sync to complete
if (this.chainClient) {
this.logger.info('waiting for chain sync');
await this.chainClient.waitForSync();
this.logger.info('chain sync complete');
}
await this._restoreState(blockHeight);
this.syncState = SyncState.Synced;
// flag that the manager has now started
this.started = true;
}
public async updateFundBroadcast(dlcTxs: DlcTransactions): Promise<void> {
const info = await this.chainClient.getBlockchainInfo();
const block = await this.chainClient.getBlock(info.bestblockhash);
dlcTxs.fundBroadcastHeight = block.height;
await this.dlcStore.saveDlcTransactions(dlcTxs);
}
public async updateCloseBroadcast(dlcTxs: DlcTransactions): Promise<void> {
const info = await this.chainClient.getBlockchainInfo();
const block = await this.chainClient.getBlock(info.bestblockhash);
dlcTxs.closeBroadcastHeight = block.height;
await this.dlcStore.saveDlcTransactions(dlcTxs);
}
public async updateFundEpoch(
dlcTxs: DlcTransactions,
block: HasHeight & HasHash,
): Promise<void> {
this.logger.info(
`updating fund epoch on dlc ${dlcTxs.contractId.toString('hex')}`,
);
dlcTxs.fundEpoch.hash = Buffer.from(block.hash, 'hex');
dlcTxs.fundEpoch.height = block.height;
if (dlcTxs.fundBroadcastHeight === 0) {
dlcTxs.fundBroadcastHeight = block.height - 1;
}
await this.dlcStore.saveDlcTransactions(dlcTxs);
this.logger.info(
`fund epoch updated on dlc ${dlcTxs.contractId.toString('hex')}`,
);
}
public async updateCloseEpoch(
dlcTxs: DlcTransactions,
tx: Tx,
block: HasHeight & HasHash,
): Promise<void> {
this.logger.info(
`updating close epoch on dlc ${dlcTxs.contractId.toString('hex')}`,
);
dlcTxs.closeEpoch.hash = Buffer.from(block.hash, 'hex');
dlcTxs.closeEpoch.height = Number(block.height);
dlcTxs.closeTxHash = Buffer.from(tx.txId.toString(), 'hex');
dlcTxs.closeType = 3; // Default to cooperative close if txid not refund or cet txid
const _dlcTxs = await this.dlcStore.findDlcTransactions(dlcTxs.contractId);
// figure out if it's execute, refund or mutual close
if (tx.txId.toString() === dlcTxs.refundTx.txId.toString()) {
dlcTxs.closeType = 2;
} else {
const cetIndex = _dlcTxs.cets.findIndex(
(cet) => tx.txId.toString() === cet.txId.toString(),
);
if (cetIndex >= 0) dlcTxs.closeType = 1;
}
if (dlcTxs.closeBroadcastHeight === 0) {
dlcTxs.closeBroadcastHeight = block.height - 1;
}
await this.dlcStore.saveDlcTransactions(dlcTxs);
this.logger.info(
`close epoch updated on dlc ${dlcTxs.contractId.toString('hex')}`,
);
}
private async _restoreState(blockHeight = 0): Promise<void> {
this.logger.info('retrieving dlc state from store');
this.blockHeight = blockHeight;
this.syncState = SyncState.Syncing;
this.dlcTxsList = await this.dlcStore.findDlcTransactionsList();
this.logger.info('found %d dlcs', this.dlcTxsList.length);
if (blockHeight === 0) {
// find best block height
for (const dlcTxs of this.dlcTxsList) {
this.blockHeight = Math.max(
Math.max(this.blockHeight, dlcTxs.fundEpoch.height),
dlcTxs.closeEpoch.height,
);
}
}
this.logger.info("highest block %d found from %d dlcs", this.blockHeight, this.dlcTxsList.length); // prettier-ignore
// validate all utxos
await this._validateUtxos(this.dlcTxsList);
}
private async _validateUtxos(_dlcTxsList: DlcTransactions[]) {
if (!this.chainClient) {
this.logger.info('skipping utxo validation, no chain_client configured');
return;
}
const dlcTxsList = _dlcTxsList.filter(
(dlcTxs) => dlcTxs.closeEpoch.height === 0,
);
const dlcTxsCount = dlcTxsList.reduce(
(acc, msg) => acc + (msg instanceof DlcTransactions ? 1 : 0),
0,
);
this.logger.info('validating %d funding utxos', dlcTxsCount);
if (!dlcTxsCount) return;
const dlcTxsToVerify: DlcTransactions[] = [];
const oct = Math.trunc(dlcTxsCount / 16);
for (let i = 0; i < dlcTxsList.length; i++) {
const dlcTxs = dlcTxsList[i];
if ((i + 1) % oct === 0) {
this.logger.info(
'validating funding utxos %s% complete',
(((i + 1) / dlcTxsCount) * 100).toFixed(2),
);
}
if (dlcTxs instanceof DlcTransactions) {
const utxo = await this.chainClient.getUtxo(
dlcTxs.fundTx.txId.toString(),
dlcTxs.fundTxVout,
);
try {
const tx = await this.chainClient.getTransaction(
dlcTxs.fundTx.txId.toString(),
);
if (!utxo) dlcTxsToVerify.push(dlcTxs);
if (utxo && Number(utxo.confirmations) === 0)
this.updateFundBroadcast(dlcTxs);
if (
utxo &&
Number(utxo.confirmations) > 0 &&
dlcTxs.fundEpoch.height === 0
) {
const block = await this.chainClient.getBlock(tx.blockhash);
await this.updateFundEpoch(dlcTxs, block);
}
} catch (e) {
/**
* tx doesn't exist
* fund tx wasn't broadcast in the first place
*/
}
}
}
this.logger.info('validating funding utxos 100% complete');
if (dlcTxsToVerify.length === 0) {
this.logger.info('no closing utxos to validate');
return;
}
await this._validateClosingUtxos(dlcTxsToVerify);
}
private async _validateClosingUtxos(dlcTxsList: DlcTransactions[]) {
let info = await this.chainClient.getBlockchainInfo();
if (this.blockHeight === 0) {
this.logger.info('cannot sync from block height 0');
return;
}
let numBlocksToSync = Math.max(info.blocks - this.blockHeight, 0);
this.logger.info('validating %d blocks for closing utxos', numBlocksToSync);
const oct = Math.trunc(numBlocksToSync / 16);
let i = 0;
while (info.blocks > this.blockHeight) {
await sleep(10);
if ((i + 1) % oct === 0) {
this.logger.info(
'validating block %s, closing utxos %s% complete',
this.blockHeight,
(((i + 1) / numBlocksToSync) * 100).toFixed(2),
);
}
// Log every 10 blocks
if (this.blockHeight % 10 === 0) {
this.logger.info(
'Validating block %s for closing utxos',
this.blockHeight,
);
}
this.blockHeight += 1;
const blockHash = await this.chainClient.getBlockHash(this.blockHeight);
const blockBuf = await this.chainClient.getRawBlock(blockHash);
const block = Block.fromBuffer(blockBuf);
for (const transaction of block.transactions) {
try {
const bjsTx = Transaction.fromBuffer(transaction.toBuffer());
if (!bjsTx.isCoinbase()) {
// ignore coinbase txs for outpoint check
const tx = Tx.fromBuffer(transaction.toBuffer());
await this._checkOutpoints(dlcTxsList, tx, block.getId());
}
} catch (e) {
this.logger.error(
'Invalid tx for validating closing utxo: %s',
transaction.toHex(),
);
this.logger.trace('Error: ', e);
}
}
if (info.blocks === this.blockHeight) {
info = await this.chainClient.getBlockchainInfo();
if (info.blocks === this.blockHeight) break;
numBlocksToSync += info.blocks - this.blockHeight;
}
i++;
}
this.logger.info('validating closing utxos 100% complete');
}
private async _checkOutpoints(
dlcTxsList: DlcTransactions[],
tx: Tx,
blockHash: string,
) {
for (const dlcTxs of dlcTxsList) {
for (const input of tx.inputs) {
if (dlcTxs.fundTx.txId.toString() === input.outpoint.txid.toString()) {
const block = await this.chainClient.getBlock(blockHash);
await this.updateCloseEpoch(dlcTxs, tx, block);
}
}
}
}
}