optimize data structures for advanced GBT algorithm

This commit is contained in:
Mononaut 2023-05-02 17:46:48 -06:00
parent 07d9315bbe
commit 428d4fc6ab
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
4 changed files with 142 additions and 62 deletions

View File

@ -1,5 +1,5 @@
import logger from '../logger'; import logger from '../logger';
import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor } from '../mempool.interfaces'; import { MempoolBlock, TransactionExtended, ThreadTransaction, TransactionStripped, MempoolBlockWithTransactions, MempoolBlockDelta, Ancestor, CompactThreadTransaction } from '../mempool.interfaces';
import { Common } from './common'; import { Common } from './common';
import config from '../config'; import config from '../config';
import { Worker } from 'worker_threads'; import { Worker } from 'worker_threads';
@ -10,6 +10,9 @@ class MempoolBlocks {
private mempoolBlockDeltas: MempoolBlockDelta[] = []; private mempoolBlockDeltas: MempoolBlockDelta[] = [];
private txSelectionWorker: Worker | null = null; private txSelectionWorker: Worker | null = null;
private nextUid: number = 1;
private uidMap: Map<number, string> = new Map(); // map short numerical uids to full txids
constructor() {} constructor() {}
public getMempoolBlocks(): MempoolBlock[] { public getMempoolBlocks(): MempoolBlock[] {
@ -175,18 +178,26 @@ class MempoolBlocks {
} }
public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> { public async $makeBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, saveResults: boolean = false): Promise<MempoolBlockWithTransactions[]> {
// reset mempool short ids
this.resetUids();
for (const tx of Object.values(newMempool)) {
this.setUid(tx);
}
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const strippedMempool: { [txid: string]: ThreadTransaction } = {}; const strippedMempool: Map<number, CompactThreadTransaction> = new Map();
Object.values(newMempool).forEach(entry => { Object.values(newMempool).forEach(entry => {
strippedMempool[entry.txid] = { if (entry.uid != null) {
txid: entry.txid, strippedMempool.set(entry.uid, {
fee: entry.fee, uid: entry.uid,
weight: entry.weight, fee: entry.fee,
feePerVsize: entry.fee / (entry.weight / 4), weight: entry.weight,
effectiveFeePerVsize: entry.fee / (entry.weight / 4), feePerVsize: entry.fee / (entry.weight / 4),
vin: entry.vin.map(v => v.txid), effectiveFeePerVsize: entry.fee / (entry.weight / 4),
}; inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
});
}
}); });
// (re)initialize tx selection worker thread // (re)initialize tx selection worker thread
@ -205,7 +216,7 @@ class MempoolBlocks {
// run the block construction algorithm in a separate thread, and wait for a result // run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener; let threadErrorListener;
try { try {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => { const workerResultPromise = new Promise<{ blocks: CompactThreadTransaction[][], clusters: Map<number, number[]> }>((resolve, reject) => {
threadErrorListener = reject; threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => { this.txSelectionWorker?.once('message', (result): void => {
resolve(result); resolve(result);
@ -213,7 +224,7 @@ class MempoolBlocks {
this.txSelectionWorker?.once('error', reject); this.txSelectionWorker?.once('error', reject);
}); });
this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool }); this.txSelectionWorker.postMessage({ type: 'set', mempool: strippedMempool });
let { blocks, clusters } = await workerResultPromise; let { blocks, clusters } = this.convertResultTxids(await workerResultPromise);
// filter out stale transactions // filter out stale transactions
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0); const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool))); blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
@ -232,37 +243,42 @@ class MempoolBlocks {
return this.mempoolBlocks; return this.mempoolBlocks;
} }
public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: string[], saveResults: boolean = false): Promise<void> { public async $updateBlockTemplates(newMempool: { [txid: string]: TransactionExtended }, added: TransactionExtended[], removed: TransactionExtended[], saveResults: boolean = false): Promise<void> {
if (!this.txSelectionWorker) { if (!this.txSelectionWorker) {
// need to reset the worker // need to reset the worker
await this.$makeBlockTemplates(newMempool, saveResults); await this.$makeBlockTemplates(newMempool, saveResults);
return; return;
} }
for (const tx of Object.values(added)) {
this.setUid(tx);
}
const removedUids = removed.map(tx => this.getUid(tx)).filter(uid => uid != null) as number[];
// prepare a stripped down version of the mempool with only the minimum necessary data // prepare a stripped down version of the mempool with only the minimum necessary data
// to reduce the overhead of passing this data to the worker thread // to reduce the overhead of passing this data to the worker thread
const addedStripped: ThreadTransaction[] = added.map(entry => { const addedStripped: CompactThreadTransaction[] = added.filter(entry => entry.uid != null).map(entry => {
return { return {
txid: entry.txid, uid: entry.uid || 0,
fee: entry.fee, fee: entry.fee,
weight: entry.weight, weight: entry.weight,
feePerVsize: entry.fee / (entry.weight / 4), feePerVsize: entry.fee / (entry.weight / 4),
effectiveFeePerVsize: entry.fee / (entry.weight / 4), effectiveFeePerVsize: entry.fee / (entry.weight / 4),
vin: entry.vin.map(v => v.txid), inputs: entry.vin.map(v => this.getUid(newMempool[v.txid])).filter(uid => uid != null) as number[],
}; };
}); });
// run the block construction algorithm in a separate thread, and wait for a result // run the block construction algorithm in a separate thread, and wait for a result
let threadErrorListener; let threadErrorListener;
try { try {
const workerResultPromise = new Promise<{ blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } }>((resolve, reject) => { const workerResultPromise = new Promise<{ blocks: CompactThreadTransaction[][], clusters: Map<number, number[]> }>((resolve, reject) => {
threadErrorListener = reject; threadErrorListener = reject;
this.txSelectionWorker?.once('message', (result): void => { this.txSelectionWorker?.once('message', (result): void => {
resolve(result); resolve(result);
}); });
this.txSelectionWorker?.once('error', reject); this.txSelectionWorker?.once('error', reject);
}); });
this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed }); this.txSelectionWorker.postMessage({ type: 'update', added: addedStripped, removed: removedUids });
let { blocks, clusters } = await workerResultPromise; let { blocks, clusters } = this.convertResultTxids(await workerResultPromise);
// filter out stale transactions // filter out stale transactions
const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0); const unfilteredCount = blocks.reduce((total, block) => { return total + block.length; }, 0);
blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool))); blocks = blocks.map(block => block.filter(tx => (tx.txid && tx.txid in newMempool)));
@ -271,6 +287,8 @@ class MempoolBlocks {
logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from updateBlockTemplates`); logger.warn(`tx selection worker thread returned ${unfilteredCount - filteredCount} stale transactions from updateBlockTemplates`);
} }
this.removeUids(removedUids);
// clean up thread error listener // clean up thread error listener
this.txSelectionWorker?.removeListener('error', threadErrorListener); this.txSelectionWorker?.removeListener('error', threadErrorListener);
@ -280,7 +298,7 @@ class MempoolBlocks {
} }
} }
private processBlockTemplates(mempool, blocks, clusters, saveResults): MempoolBlockWithTransactions[] { private processBlockTemplates(mempool, blocks: ThreadTransaction[][], clusters, saveResults): MempoolBlockWithTransactions[] {
// update this thread's mempool with the results // update this thread's mempool with the results
blocks.forEach((block, blockIndex) => { blocks.forEach((block, blockIndex) => {
let runningVsize = 0; let runningVsize = 0;
@ -371,6 +389,54 @@ class MempoolBlocks {
transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)), transactions: fitTransactions.map((tx) => Common.stripTransaction(tx)),
}; };
} }
private resetUids(): void {
this.uidMap.clear();
this.nextUid = 1;
}
private setUid(tx: TransactionExtended): number {
const uid = this.nextUid;
this.nextUid++;
this.uidMap.set(uid, tx.txid);
tx.uid = uid;
return uid;
}
private getUid(tx: TransactionExtended): number | void {
if (tx?.uid != null && this.uidMap.has(tx.uid)) {
return tx.uid;
}
}
private removeUids(uids: number[]): void {
for (const uid of uids) {
this.uidMap.delete(uid);
}
}
private convertResultTxids({ blocks, clusters }: { blocks: any[][], clusters: Map<number, number[]>})
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] }} {
for (const block of blocks) {
for (const tx of block) {
tx.txid = this.uidMap.get(tx.uid);
if (tx.cpfpRoot) {
tx.cpfpRoot = this.uidMap.get(tx.cpfpRoot);
}
}
}
const convertedClusters = {};
for (const rootUid of clusters.keys()) {
const rootTxid = this.uidMap.get(rootUid);
if (rootTxid) {
const members = clusters.get(rootUid)?.map(uid => {
return this.uidMap.get(uid);
});
convertedClusters[rootTxid] = members;
}
}
return { blocks, clusters: convertedClusters } as { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] }};
}
} }
export default new MempoolBlocks(); export default new MempoolBlocks();

View File

@ -1,10 +1,10 @@
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
import { ThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces'; import { CompactThreadTransaction, MempoolBlockWithTransactions, AuditTransaction } from '../mempool.interfaces';
import { PairingHeap } from '../utils/pairing-heap'; import { PairingHeap } from '../utils/pairing-heap';
import { parentPort } from 'worker_threads'; import { parentPort } from 'worker_threads';
let mempool: { [txid: string]: ThreadTransaction } = {}; let mempool: Map<number, CompactThreadTransaction> = new Map();
if (parentPort) { if (parentPort) {
parentPort.on('message', (params) => { parentPort.on('message', (params) => {
@ -12,10 +12,10 @@ if (parentPort) {
mempool = params.mempool; mempool = params.mempool;
} else if (params.type === 'update') { } else if (params.type === 'update') {
params.added.forEach(tx => { params.added.forEach(tx => {
mempool[tx.txid] = tx; mempool.set(tx.uid, tx);
}); });
params.removed.forEach(txid => { params.removed.forEach(uid => {
delete mempool[txid]; mempool.delete(uid);
}); });
} }
@ -23,7 +23,7 @@ if (parentPort) {
// return the result to main thread. // return the result to main thread.
if (parentPort) { if (parentPort) {
parentPort.postMessage({ blocks, clusters }); parentPort.postMessage({ blocks, clusters });
} }
}); });
} }
@ -32,26 +32,25 @@ if (parentPort) {
* Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core * Build projected mempool blocks using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp) * (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/ */
function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction }) function makeBlockTemplates(mempool: Map<number, CompactThreadTransaction>)
: { blocks: ThreadTransaction[][], clusters: { [root: string]: string[] } } { : { blocks: CompactThreadTransaction[][], clusters: Map<number, number[]> } {
const start = Date.now(); const start = Date.now();
const auditPool: { [txid: string]: AuditTransaction } = {}; const auditPool: Map<number, AuditTransaction> = new Map();
const mempoolArray: AuditTransaction[] = []; const mempoolArray: AuditTransaction[] = [];
const restOfArray: ThreadTransaction[] = []; const restOfArray: CompactThreadTransaction[] = [];
const cpfpClusters: { [root: string]: string[] } = {}; const cpfpClusters: Map<number, number[]> = new Map();
// grab the top feerate txs up to maxWeight mempool.forEach(tx => {
Object.values(mempool).sort((a, b) => b.feePerVsize - a.feePerVsize).forEach(tx => {
// initializing everything up front helps V8 optimize property access later // initializing everything up front helps V8 optimize property access later
auditPool[tx.txid] = { auditPool.set(tx.uid, {
txid: tx.txid, uid: tx.uid,
fee: tx.fee, fee: tx.fee,
weight: tx.weight, weight: tx.weight,
feePerVsize: tx.feePerVsize, feePerVsize: tx.feePerVsize,
effectiveFeePerVsize: tx.feePerVsize, effectiveFeePerVsize: tx.feePerVsize,
vin: tx.vin, inputs: tx.inputs || [],
relativesSet: false, relativesSet: false,
ancestorMap: new Map<string, AuditTransaction>(), ancestorMap: new Map<number, AuditTransaction>(),
children: new Set<AuditTransaction>(), children: new Set<AuditTransaction>(),
ancestorFee: 0, ancestorFee: 0,
ancestorWeight: 0, ancestorWeight: 0,
@ -59,8 +58,8 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
used: false, used: false,
modified: false, modified: false,
modifiedNode: null, modifiedNode: null,
}; });
mempoolArray.push(auditPool[tx.txid]); mempoolArray.push(auditPool.get(tx.uid) as AuditTransaction);
}); });
// Build relatives graph & calculate ancestor scores // Build relatives graph & calculate ancestor scores
@ -73,8 +72,8 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
// Sort by descending ancestor score // Sort by descending ancestor score
mempoolArray.sort((a, b) => { mempoolArray.sort((a, b) => {
if (b.score === a.score) { if (b.score === a.score) {
// tie-break by lexicographic txid order for stability // tie-break by uid for stability
return a.txid < b.txid ? -1 : 1; return a.uid < b.uid ? -1 : 1;
} else { } else {
return (b.score || 0) - (a.score || 0); return (b.score || 0) - (a.score || 0);
} }
@ -82,14 +81,14 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
// Build blocks by greedily choosing the highest feerate package // Build blocks by greedily choosing the highest feerate package
// (i.e. the package rooted in the transaction with the best ancestor score) // (i.e. the package rooted in the transaction with the best ancestor score)
const blocks: ThreadTransaction[][] = []; const blocks: CompactThreadTransaction[][] = [];
let blockWeight = 4000; let blockWeight = 4000;
let blockSize = 0; let blockSize = 0;
let transactions: AuditTransaction[] = []; let transactions: AuditTransaction[] = [];
const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => { const modified: PairingHeap<AuditTransaction> = new PairingHeap((a, b): boolean => {
if (a.score === b.score) { if (a.score === b.score) {
// tie-break by lexicographic txid order for stability // tie-break by uid for stability
return a.txid > b.txid; return a.uid > b.uid;
} else { } else {
return (a.score || 0) > (b.score || 0); return (a.score || 0) > (b.score || 0);
} }
@ -126,20 +125,23 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx]; const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
let isCluster = false; let isCluster = false;
if (sortedTxSet.length > 1) { if (sortedTxSet.length > 1) {
cpfpClusters[nextTx.txid] = sortedTxSet.map(tx => tx.txid); cpfpClusters.set(nextTx.uid, sortedTxSet.map(tx => tx.uid));
isCluster = true; isCluster = true;
} }
const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4); const effectiveFeeRate = nextTx.ancestorFee / (nextTx.ancestorWeight / 4);
const used: AuditTransaction[] = []; const used: AuditTransaction[] = [];
while (sortedTxSet.length) { while (sortedTxSet.length) {
const ancestor = sortedTxSet.pop(); const ancestor = sortedTxSet.pop();
const mempoolTx = mempool[ancestor.txid]; const mempoolTx = mempool.get(ancestor.uid);
if (!mempoolTx) {
continue;
}
ancestor.used = true; ancestor.used = true;
ancestor.usedBy = nextTx.txid; ancestor.usedBy = nextTx.uid;
// update original copy of this tx with effective fee rate & relatives data // update original copy of this tx with effective fee rate & relatives data
mempoolTx.effectiveFeePerVsize = effectiveFeeRate; mempoolTx.effectiveFeePerVsize = effectiveFeeRate;
if (isCluster) { if (isCluster) {
mempoolTx.cpfpRoot = nextTx.txid; mempoolTx.cpfpRoot = nextTx.uid;
} }
mempoolTx.cpfpChecked = true; mempoolTx.cpfpChecked = true;
transactions.push(ancestor); transactions.push(ancestor);
@ -169,7 +171,7 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
if ((exceededPackageTries || queueEmpty) && blocks.length < 7) { if ((exceededPackageTries || queueEmpty) && blocks.length < 7) {
// construct this block // construct this block
if (transactions.length) { if (transactions.length) {
blocks.push(transactions.map(t => mempool[t.txid])); blocks.push(transactions.map(t => mempool.get(t.uid) as CompactThreadTransaction));
} }
// reset for the next block // reset for the next block
transactions = []; transactions = [];
@ -194,7 +196,7 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
} }
// add the final unbounded block if it contains any transactions // add the final unbounded block if it contains any transactions
if (transactions.length > 0) { if (transactions.length > 0) {
blocks.push(transactions.map(t => mempool[t.txid])); blocks.push(transactions.map(t => mempool.get(t.uid) as CompactThreadTransaction));
} }
const end = Date.now(); const end = Date.now();
@ -208,10 +210,10 @@ function makeBlockTemplates(mempool: { [txid: string]: ThreadTransaction })
// recursion unavoidable, but should be limited to depth < 25 by mempool policy // recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives( function setRelatives(
tx: AuditTransaction, tx: AuditTransaction,
mempool: { [txid: string]: AuditTransaction }, mempool: Map<number, AuditTransaction>,
): void { ): void {
for (const parent of tx.vin) { for (const parent of tx.inputs) {
const parentTx = mempool[parent]; const parentTx = mempool.get(parent);
if (parentTx && !tx.ancestorMap?.has(parent)) { if (parentTx && !tx.ancestorMap?.has(parent)) {
tx.ancestorMap.set(parent, parentTx); tx.ancestorMap.set(parent, parentTx);
parentTx.children.add(tx); parentTx.children.add(tx);
@ -220,7 +222,7 @@ function setRelatives(
setRelatives(parentTx, mempool); setRelatives(parentTx, mempool);
} }
parentTx.ancestorMap.forEach((ancestor) => { parentTx.ancestorMap.forEach((ancestor) => {
tx.ancestorMap.set(ancestor.txid, ancestor); tx.ancestorMap.set(ancestor.uid, ancestor);
}); });
} }
}; };
@ -238,7 +240,7 @@ function setRelatives(
// avoids recursion to limit call stack depth // avoids recursion to limit call stack depth
function updateDescendants( function updateDescendants(
rootTx: AuditTransaction, rootTx: AuditTransaction,
mempool: { [txid: string]: AuditTransaction }, mempool: Map<number, AuditTransaction>,
modified: PairingHeap<AuditTransaction>, modified: PairingHeap<AuditTransaction>,
): void { ): void {
const descendantSet: Set<AuditTransaction> = new Set(); const descendantSet: Set<AuditTransaction> = new Set();
@ -254,9 +256,9 @@ function updateDescendants(
}); });
while (descendants.length) { while (descendants.length) {
descendantTx = descendants.pop(); descendantTx = descendants.pop();
if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) { if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.uid)) {
// remove tx as ancestor // remove tx as ancestor
descendantTx.ancestorMap.delete(rootTx.txid); descendantTx.ancestorMap.delete(rootTx.uid);
descendantTx.ancestorFee -= rootTx.fee; descendantTx.ancestorFee -= rootTx.fee;
descendantTx.ancestorWeight -= rootTx.weight; descendantTx.ancestorWeight -= rootTx.weight;
tmpScore = descendantTx.score; tmpScore = descendantTx.score;

View File

@ -282,7 +282,7 @@ class WebsocketHandler {
this.printLogs(); this.printLogs();
if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) { if (config.MEMPOOL.ADVANCED_GBT_MEMPOOL) {
await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions.map(tx => tx.txid), true); await mempoolBlocks.$updateBlockTemplates(newMempool, newTransactions, deletedTransactions, true);
} else { } else {
mempoolBlocks.updateMempoolBlocks(newMempool, true); mempoolBlocks.updateMempoolBlocks(newMempool, true);
} }

View File

@ -84,17 +84,18 @@ export interface TransactionExtended extends IEsploraApi.Transaction {
block: number, block: number,
vsize: number, vsize: number,
}; };
uid?: number;
} }
export interface AuditTransaction { export interface AuditTransaction {
txid: string; uid: number;
fee: number; fee: number;
weight: number; weight: number;
feePerVsize: number; feePerVsize: number;
effectiveFeePerVsize: number; effectiveFeePerVsize: number;
vin: string[]; inputs: number[];
relativesSet: boolean; relativesSet: boolean;
ancestorMap: Map<string, AuditTransaction>; ancestorMap: Map<number, AuditTransaction>;
children: Set<AuditTransaction>; children: Set<AuditTransaction>;
ancestorFee: number; ancestorFee: number;
ancestorWeight: number; ancestorWeight: number;
@ -104,13 +105,24 @@ export interface AuditTransaction {
modifiedNode: HeapNode<AuditTransaction>; modifiedNode: HeapNode<AuditTransaction>;
} }
export interface CompactThreadTransaction {
uid: number;
fee: number;
weight: number;
feePerVsize: number;
effectiveFeePerVsize?: number;
inputs: number[];
cpfpRoot?: string;
cpfpChecked?: boolean;
}
export interface ThreadTransaction { export interface ThreadTransaction {
txid: string; txid: string;
fee: number; fee: number;
weight: number; weight: number;
feePerVsize: number; feePerVsize: number;
effectiveFeePerVsize?: number; effectiveFeePerVsize?: number;
vin: string[]; inputs: number[];
cpfpRoot?: string; cpfpRoot?: string;
cpfpChecked?: boolean; cpfpChecked?: boolean;
} }