Add cluster mempool backend unit tests

This commit is contained in:
Mononaut 2026-03-07 10:49:17 +00:00 committed by mononaut
parent 4af8f5abc3
commit 4718a04182
No known key found for this signature in database
GPG key ID: BFD16BE592A9CD8D
4 changed files with 1627 additions and 0 deletions

View file

@ -0,0 +1,473 @@
import { ClusterMempool } from '../../cluster-mempool/cluster-mempool';
import { makeTx, txid } from './test-utils';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
function buildMempool(txs: MempoolTransactionExtended[]): { [txid: string]: MempoolTransactionExtended } {
const mempool: { [txid: string]: MempoolTransactionExtended } = {};
for (const tx of txs) {
mempool[tx.txid] = tx;
}
return mempool;
}
describe('ClusterMempool', () => {
describe('constructor', () => {
it('should build clusters from mempool', () => {
const parentId = txid('a1');
const childId = txid('a2');
const mempool = buildMempool([
makeTx(parentId, 100, 100),
makeTx(childId, 5000, 100, [parentId]),
]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(2);
});
it('should create separate clusters for unrelated txs', () => {
const mempool = buildMempool([
makeTx(txid('a1'), 100, 100),
makeTx(txid('b1'), 200, 100),
]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(2);
});
});
describe('getClusterInfo', () => {
it('should return cluster info for a known tx', () => {
const parentId = txid('a1');
const childId = txid('a2');
const mempool = buildMempool([
makeTx(parentId, 100, 100),
makeTx(childId, 5000, 100, [parentId]),
]);
const cm = new ClusterMempool(mempool);
const info = cm.getClusterInfo(parentId);
expect(info).not.toBeNull();
expect(info?.chunkFeerate).toBeGreaterThan(0);
});
it('should return null for unknown tx', () => {
const mempool = buildMempool([makeTx(txid('a1'), 100, 100)]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterInfo(txid('zz'))).toBeNull();
});
});
describe('getCluster', () => {
it('should return cluster data with correct topology', () => {
const parentId = txid('a1');
const childId = txid('a2');
const mempool = buildMempool([
makeTx(parentId, 100, 100),
makeTx(childId, 5000, 100, [parentId]),
]);
const cm = new ClusterMempool(mempool);
const info = cm.getClusterInfo(parentId);
expect(info).not.toBeNull();
const data = cm.getCluster(info!.clusterId);
expect(data).not.toBeNull();
expect(data!.txs.length).toBe(2);
expect(data!.chunks.length).toBeGreaterThan(0);
});
it('should return null for unknown cluster id', () => {
const mempool = buildMempool([makeTx(txid('a1'), 100, 100)]);
const cm = new ClusterMempool(mempool);
expect(cm.getCluster(9999)).toBeNull();
});
});
describe('applyMempoolChange', () => {
it('should handle adding a new singleton tx', () => {
const mempool = buildMempool([makeTx(txid('a1'), 100, 100)]);
const cm = new ClusterMempool(mempool);
const initialCount = cm.getClusterCount();
cm.applyMempoolChange({
added: [makeTx(txid('b1'), 200, 100)],
removed: [],
accelerations: {},
});
expect(cm.getClusterCount()).toBe(initialCount + 1);
expect(cm.getTxCount()).toBe(2);
});
it('should handle removing a tx', () => {
const parentId = txid('a1');
const childId = txid('a2');
const mempool = buildMempool([
makeTx(parentId, 100, 100),
makeTx(childId, 5000, 100, [parentId]),
]);
const cm = new ClusterMempool(mempool);
cm.applyMempoolChange({
added: [],
removed: [childId],
accelerations: {},
});
expect(cm.getTxCount()).toBe(1);
expect(cm.getClusterInfo(childId)).toBeNull();
expect(cm.getClusterInfo(parentId)).not.toBeNull();
});
it('should split cluster when middle tx is removed', () => {
const a = txid('a1');
const b = txid('b1');
const c = txid('c1');
const mempool = buildMempool([
makeTx(a, 100, 100),
makeTx(b, 200, 100, [a]),
makeTx(c, 300, 100, [b]),
]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(1);
cm.applyMempoolChange({
added: [],
removed: [b],
accelerations: {},
});
expect(cm.getTxCount()).toBe(2);
const infoA = cm.getClusterInfo(a);
const infoC = cm.getClusterInfo(c);
expect(infoA).not.toBeNull();
expect(infoC).not.toBeNull();
expect(infoA!.clusterId).not.toBe(infoC!.clusterId);
});
it('should handle fee changes via acceleration', () => {
const parentId = txid('a1');
const childId = txid('a2');
const mempool = buildMempool([
makeTx(parentId, 100, 100),
makeTx(childId, 100, 100, [parentId]),
]);
const cm = new ClusterMempool(mempool);
const infoBefore = cm.getClusterInfo(childId);
cm.applyMempoolChange({
added: [],
removed: [],
accelerations: { [childId]: { feeDelta: 49900 } },
});
const infoAfter = cm.getClusterInfo(childId);
expect(infoAfter).not.toBeNull();
expect(infoAfter!.clusterId).not.toBe(infoBefore!.clusterId);
});
it('should merge clusters when new tx connects them', () => {
const a = txid('a1');
const b = txid('b1');
const mempool = buildMempool([
makeTx(a, 100, 100),
makeTx(b, 200, 100),
]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(2);
const bridgeTx = makeTx(txid('c1'), 300, 100, [a, b]);
cm.applyMempoolChange({
added: [bridgeTx],
removed: [],
accelerations: {},
});
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(3);
});
});
describe('cluster merging', () => {
it('should merge 3 separate clusters when new tx bridges them', () => {
const a = txid('a1');
const b = txid('b1');
const c = txid('c1');
const mempool = buildMempool([
makeTx(a, 100, 100),
makeTx(b, 200, 100),
makeTx(c, 300, 100),
]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(3);
const bridge = makeTx(txid('d1'), 400, 100, [a, b, c]);
cm.applyMempoolChange({ added: [bridge], removed: [], accelerations: {} });
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(4);
});
it('should grow cluster by 1 when new tx has parents in same cluster', () => {
const a = txid('a1');
const b = txid('b1');
const mempool = buildMempool([
makeTx(a, 100, 100),
makeTx(b, 200, 100, [a]),
]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(1);
const c = makeTx(txid('c1'), 300, 100, [a]);
cm.applyMempoolChange({ added: [c], removed: [], accelerations: {} });
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(3);
});
it('should grow chain incrementally', () => {
const a = txid('a1');
const mempool = buildMempool([makeTx(a, 100, 100)]);
const cm = new ClusterMempool(mempool);
const b = makeTx(txid('b1'), 200, 100, [a]);
cm.applyMempoolChange({ added: [b], removed: [], accelerations: {} });
const c = makeTx(txid('c1'), 300, 100, [txid('b1')]);
cm.applyMempoolChange({ added: [c], removed: [], accelerations: {} });
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(3);
});
});
describe('cluster splitting', () => {
it('should split star into singletons when center is removed', () => {
const center = txid('center');
const leaves = Array.from({ length: 5 }, (_, i) => txid(`leaf${i}`));
const centerTx = makeTx(center, 100, 100);
for (let i = 1; i < 5; i++) {
centerTx.vout.push({
scriptpubkey: '', scriptpubkey_asm: '', scriptpubkey_type: 'v0_p2wpkh', value: 50000,
});
}
const leafTxs = leaves.map((l, i) => {
const tx = makeTx(l, 200, 100, [center]);
tx.vin[0].vout = i;
return tx;
});
const mempool = buildMempool([centerTx, ...leafTxs]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(1);
cm.applyMempoolChange({ added: [], removed: [center], accelerations: {} });
expect(cm.getTxCount()).toBe(5);
expect(cm.getClusterCount()).toBe(5);
});
it('should shrink cluster without splitting when leaf is removed', () => {
const a = txid('a1');
const b = txid('b1');
const c = txid('c1');
const aTx = makeTx(a, 100, 100);
aTx.vout.push({ scriptpubkey: '', scriptpubkey_asm: '', scriptpubkey_type: 'v0_p2wpkh', value: 50000 });
const bTx = makeTx(b, 200, 100, [a]);
bTx.vin[0].vout = 0;
const cTx = makeTx(c, 300, 100, [a]);
cTx.vin[0].vout = 1;
const mempool = buildMempool([aTx, bTx, cTx]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(1);
cm.applyMempoolChange({ added: [], removed: [c], accelerations: {} });
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(2);
});
it('should produce singleton when tx is removed from 2-tx cluster', () => {
const a = txid('a1');
const b = txid('b1');
const mempool = buildMempool([
makeTx(a, 100, 100),
makeTx(b, 200, 100, [a]),
]);
const cm = new ClusterMempool(mempool);
cm.applyMempoolChange({ added: [], removed: [b], accelerations: {} });
expect(cm.getClusterCount()).toBe(1);
expect(cm.getTxCount()).toBe(1);
expect(cm.getClusterInfo(a)).not.toBeNull();
});
it('should create 3+ components when removing a tx that bridges multiple subgraphs', () => {
const hub = txid('hub');
const a = txid('a1');
const b = txid('b1');
const c = txid('c1');
const hubTx = makeTx(hub, 100, 100);
hubTx.vout.push(
{ scriptpubkey: '', scriptpubkey_asm: '', scriptpubkey_type: 'v0_p2wpkh', value: 50000 },
{ scriptpubkey: '', scriptpubkey_asm: '', scriptpubkey_type: 'v0_p2wpkh', value: 50000 },
);
const txA = makeTx(a, 200, 100, [hub]);
txA.vin[0].vout = 0;
const txB = makeTx(b, 300, 100, [hub]);
txB.vin[0].vout = 1;
const txC = makeTx(c, 400, 100, [hub]);
txC.vin[0].vout = 2;
const mempool = buildMempool([hubTx, txA, txB, txC]);
const cm = new ClusterMempool(mempool);
expect(cm.getClusterCount()).toBe(1);
cm.applyMempoolChange({ added: [], removed: [hub], accelerations: {} });
expect(cm.getClusterCount()).toBe(3);
expect(cm.getTxCount()).toBe(3);
});
});
describe('fee changes via accelerations', () => {
it('should increase chunk feerate when acceleration added', () => {
const a = txid('a1');
const mempool = buildMempool([makeTx(a, 100, 100)]);
const cm = new ClusterMempool(mempool);
const infoBefore = cm.getClusterInfo(a);
expect(infoBefore).not.toBeNull();
cm.applyMempoolChange({
added: [],
removed: [],
accelerations: { [a]: { feeDelta: 9900 } },
});
const infoAfter = cm.getClusterInfo(a);
expect(infoAfter).not.toBeNull();
expect(infoAfter!.chunkFeerate).toBeGreaterThan(infoBefore!.chunkFeerate);
});
it('should decrease chunk feerate when acceleration removed', () => {
const a = txid('a1');
const mempool = buildMempool([makeTx(a, 100, 100)]);
const cm = new ClusterMempool(mempool, { [a]: { feeDelta: 9900 } });
const infoBefore = cm.getClusterInfo(a);
expect(infoBefore).not.toBeNull();
cm.applyMempoolChange({
added: [],
removed: [],
accelerations: {},
});
const infoAfter = cm.getClusterInfo(a);
expect(infoAfter).not.toBeNull();
expect(infoAfter!.chunkFeerate).toBeLessThan(infoBefore!.chunkFeerate);
});
it('should reorder chunks when acceleration shifts priorities', () => {
const a = txid('a1');
const b = txid('b1');
const mempool = buildMempool([
makeTx(a, 100, 100),
makeTx(b, 200, 100, [a]),
]);
const cm = new ClusterMempool(mempool);
const infoBBefore = cm.getClusterInfo(b);
expect(infoBBefore).not.toBeNull();
cm.applyMempoolChange({
added: [],
removed: [],
accelerations: { [b]: { feeDelta: 49800 } },
});
const infoBAfter = cm.getClusterInfo(b);
expect(infoBAfter).not.toBeNull();
expect(infoBAfter!.chunkFeerate).toBeGreaterThan(infoBBefore!.chunkFeerate);
});
});
describe('getBlocks', () => {
it('should return projected blocks', () => {
const txs: MempoolTransactionExtended[] = [];
for (let i = 0; i < 10; i++) {
txs.push(makeTx(txid(`t${i}`), 1000 * (i + 1), 100));
}
const mempool = buildMempool(txs);
const cm = new ClusterMempool(mempool);
const blocks = cm.getBlocks(3);
expect(blocks.length).toBeGreaterThan(0);
expect(blocks[0].txids.length).toBeGreaterThan(0);
});
it('should return empty array for empty mempool', () => {
const cm = new ClusterMempool({});
const blocks = cm.getBlocks(3);
expect(blocks.length).toBe(0);
});
it('should respect chunk ordering for single-cluster mempool', () => {
const a = txid('a1');
const b = txid('b1');
const c = txid('c1');
const mempool = buildMempool([
makeTx(a, 3000, 100),
makeTx(b, 200, 100, [a]),
makeTx(c, 100, 100, [b]),
]);
const cm = new ClusterMempool(mempool);
const blocks = cm.getBlocks(1);
expect(blocks.length).toBe(1);
const txids = blocks[0].txids;
expect(txids.indexOf(a)).toBeLessThan(txids.indexOf(b));
expect(txids.indexOf(b)).toBeLessThan(txids.indexOf(c));
});
it('should maintain topological validity within blocks', () => {
const a = txid('a1');
const b = txid('b1');
const c = txid('c1');
const d = txid('d1');
const aTx = makeTx(a, 1000, 100);
aTx.vout.push({ scriptpubkey: '', scriptpubkey_asm: '', scriptpubkey_type: 'v0_p2wpkh', value: 50000 });
const bTx = makeTx(b, 500, 100, [a]);
bTx.vin[0].vout = 0;
const cTx = makeTx(c, 500, 100, [a]);
cTx.vin[0].vout = 1;
const mempool = buildMempool([aTx, bTx, cTx, makeTx(d, 200, 100, [b, c])]);
const cm = new ClusterMempool(mempool);
const blocks = cm.getBlocks(1);
const txids = blocks[0].txids;
expect(txids.indexOf(a)).toBeLessThan(txids.indexOf(b));
expect(txids.indexOf(a)).toBeLessThan(txids.indexOf(c));
expect(txids.indexOf(b)).toBeLessThan(txids.indexOf(d));
expect(txids.indexOf(c)).toBeLessThan(txids.indexOf(d));
});
});
describe('empty and degenerate cases', () => {
it('should handle empty diff with no changes', () => {
const mempool = buildMempool([makeTx(txid('a1'), 100, 100)]);
const cm = new ClusterMempool(mempool);
const countBefore = cm.getClusterCount();
const txCountBefore = cm.getTxCount();
cm.applyMempoolChange({ added: [], removed: [], accelerations: {} });
expect(cm.getClusterCount()).toBe(countBefore);
expect(cm.getTxCount()).toBe(txCountBefore);
});
it('should not crash when removing nonexistent tx', () => {
const mempool = buildMempool([makeTx(txid('a1'), 100, 100)]);
const cm = new ClusterMempool(mempool);
cm.applyMempoolChange({
added: [],
removed: [txid('nonexistent')],
accelerations: {},
});
expect(cm.getTxCount()).toBe(1);
});
});
});

View file

@ -0,0 +1,494 @@
import { DepGraph, sortTopological, subgraph } from '../../cluster-mempool/depgraph';
import { buildChain, buildFanOut, buildDiamond, buildStar } from './test-utils';
describe('DepGraph', () => {
describe('addTransaction', () => {
it('should add a transaction and return a ClusterTx', () => {
const dg = new DepGraph();
const tx = dg.addTransaction('tx0', 1000, 100);
expect(dg.size).toBe(1);
expect(tx.effectiveFee).toBe(1000);
expect(tx.weight).toBe(100);
expect(tx.txid).toBe('tx0');
});
it('should assign distinct ClusterTx objects', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
expect(a).not.toBe(b);
expect(b).not.toBe(c);
expect(dg.size).toBe(3);
});
it('should include self in ancestors and descendants', () => {
const dg = new DepGraph();
const tx = dg.addTransaction('tx0', 100, 10);
expect(tx.ancestors.has(tx)).toBe(true);
expect(tx.descendants.has(tx)).toBe(true);
});
it('should handle large clusters', () => {
const dg = new DepGraph();
for (let i = 0; i < 100; i++) {
dg.addTransaction(`tx${i}`, 100, 10);
}
expect(dg.size).toBe(100);
});
});
describe('addDependency', () => {
it('should establish parent-child relationship', () => {
const dg = new DepGraph();
const parent = dg.addTransaction('parent', 100, 10);
const child = dg.addTransaction('child', 200, 20);
dg.addDependency(parent, child);
expect(child.ancestors.has(parent)).toBe(true);
expect(parent.descendants.has(child)).toBe(true);
expect(child.parents.has(parent)).toBe(true);
expect(parent.children.has(child)).toBe(true);
});
it('should propagate ancestors transitively', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
dg.addDependency(b, c);
expect(c.ancestors.has(a)).toBe(true);
expect(c.ancestors.has(b)).toBe(true);
expect(c.ancestors.has(c)).toBe(true);
expect(a.descendants.has(a)).toBe(true);
expect(a.descendants.has(b)).toBe(true);
expect(a.descendants.has(c)).toBe(true);
});
it('should handle diamond dependencies', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
const d = dg.addTransaction('d', 400, 40);
dg.addDependency(a, b);
dg.addDependency(a, c);
dg.addDependency(b, d);
dg.addDependency(c, d);
expect(d.ancestors.size).toBe(4);
expect(a.descendants.size).toBe(4);
});
});
describe('removeTransactions', () => {
it('should remove transactions and update sets', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
dg.addDependency(b, c);
dg.removeTransactions(new Set([b]));
expect(dg.size).toBe(2);
expect(dg.hasTx(b)).toBe(false);
expect(c.ancestors.has(b)).toBe(false);
expect(a.descendants.has(b)).toBe(false);
});
it('should handle slot reuse after removal', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
dg.addTransaction('b', 200, 20);
dg.removeTransactions(new Set([a]));
const c = dg.addTransaction('c', 300, 30);
expect(dg.size).toBe(2);
expect(c.txid).toBe('c');
});
});
describe('dependsOn (via ancestors)', () => {
it('should correctly identify dependencies', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
expect(b.ancestors.has(a)).toBe(true);
expect(a.ancestors.has(b)).toBe(false);
expect(c.ancestors.has(a)).toBe(false);
});
});
describe('findConnectedComponents', () => {
it('should find a single component', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
dg.addDependency(a, b);
const components = dg.findConnectedComponents();
expect(components.length).toBe(1);
expect(components[0].size).toBe(2);
});
it('should find multiple disconnected components', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
const d = dg.addTransaction('d', 400, 40);
dg.addDependency(a, b);
dg.addDependency(c, d);
const components = dg.findConnectedComponents();
expect(components.length).toBe(2);
});
it('should handle isolated transactions', () => {
const dg = new DepGraph();
dg.addTransaction('a', 100, 10);
dg.addTransaction('b', 200, 20);
dg.addTransaction('c', 300, 30);
const components = dg.findConnectedComponents();
expect(components.length).toBe(3);
});
});
describe('parents / children (direct)', () => {
it('should return only direct parents, not transitive', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
dg.addDependency(b, c);
expect(c.parents.has(b)).toBe(true);
expect(c.parents.has(a)).toBe(false);
});
it('should return only direct children', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
dg.addDependency(b, c);
expect(a.children.has(b)).toBe(true);
expect(a.children.has(c)).toBe(false);
});
});
describe('appendTopo', () => {
it('should output in topological order', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
dg.addDependency(b, c);
const output = sortTopological(new Set([c, a, b]));
expect(output.indexOf(a)).toBeLessThan(output.indexOf(b));
expect(output.indexOf(b)).toBeLessThan(output.indexOf(c));
});
});
describe('restrict', () => {
it('should create a subgraph with correct deps', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
const { depgraph: sub, txMap } = subgraph(new Set([a, b]));
expect(sub.size).toBe(2);
const newA = txMap.get(a)!;
const newB = txMap.get(b)!;
expect(newB.ancestors.has(newA)).toBe(true);
});
});
describe('graceful error handling', () => {
it('should no-op addDependency with non-member txs', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const dg2 = new DepGraph();
const foreign = dg2.addTransaction('foreign', 200, 20);
dg.addDependency(a, foreign);
dg.addDependency(foreign, a);
expect(a.ancestors.size).toBe(1);
});
});
describe('deep chain topology', () => {
it('should track ancestors and descendants at each depth', () => {
const { depgraph, txs } = buildChain(20, 100, 10);
expect(depgraph.size).toBe(20);
expect(txs[19].ancestors.size).toBe(20);
expect(txs[0].descendants.size).toBe(20);
expect(txs[10].ancestors.size).toBe(11);
expect(txs[10].descendants.size).toBe(10);
expect(txs[10].parents.size).toBe(1);
expect(txs[10].parents.has(txs[9])).toBe(true);
expect(txs[10].children.size).toBe(1);
expect(txs[10].children.has(txs[11])).toBe(true);
});
});
describe('wide fan-out topology', () => {
it('should track parent-children relationships for 1 parent with 10 children', () => {
const { depgraph, parent, children } = buildFanOut(10, 100, 10, 50, 10);
expect(depgraph.size).toBe(11);
expect(parent.children.size).toBe(10);
expect(parent.descendants.size).toBe(11);
for (const child of children) {
expect(child.ancestors.size).toBe(2);
expect(child.ancestors.has(parent)).toBe(true);
expect(child.parents.size).toBe(1);
expect(child.parents.has(parent)).toBe(true);
}
});
});
describe('wide fan-in topology', () => {
it('should track many parents converging to one child', () => {
const dg = new DepGraph();
const parents: any[] = [];
for (let i = 0; i < 10; i++) {
parents.push(dg.addTransaction(`p${i}`, 100, 10));
}
const child = dg.addTransaction('child', 500, 50);
for (const p of parents) {
dg.addDependency(p, child);
}
expect(child.parents.size).toBe(10);
expect(child.ancestors.size).toBe(11);
for (const p of parents) {
expect(p.descendants.has(child)).toBe(true);
}
});
});
describe('multiple diamonds in sequence', () => {
it('should handle A→B,C→D→E,F→G topology', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 100, 10);
const c = dg.addTransaction('c', 100, 10);
const d = dg.addTransaction('d', 100, 10);
const e = dg.addTransaction('e', 100, 10);
const f = dg.addTransaction('f', 100, 10);
const g = dg.addTransaction('g', 100, 10);
dg.addDependency(a, b);
dg.addDependency(a, c);
dg.addDependency(b, d);
dg.addDependency(c, d);
dg.addDependency(d, e);
dg.addDependency(d, f);
dg.addDependency(e, g);
dg.addDependency(f, g);
expect(g.ancestors.size).toBe(7);
expect(a.descendants.size).toBe(7);
expect(d.parents.size).toBe(2);
expect(d.children.size).toBe(2);
});
});
describe('disconnected subgraphs', () => {
it('should coexist in one DepGraph', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
dg.addDependency(a, b);
const c = dg.addTransaction('c', 300, 30);
const d = dg.addTransaction('d', 400, 40);
dg.addDependency(c, d);
const e = dg.addTransaction('e', 500, 50);
expect(dg.size).toBe(5);
expect(b.ancestors.has(c)).toBe(false);
expect(a.descendants.has(d)).toBe(false);
expect(e.ancestors.size).toBe(1);
expect(dg.findConnectedComponents().length).toBe(3);
});
});
describe('removeTransactions edge cases', () => {
it('should remove a leaf without affecting siblings', () => {
const { depgraph, parent, children } = buildFanOut(3, 100, 10, 50, 10);
depgraph.removeTransactions(new Set([children[2]]));
expect(depgraph.size).toBe(3);
expect(parent.children.size).toBe(2);
expect(depgraph.hasTx(children[0])).toBe(true);
expect(depgraph.hasTx(children[1])).toBe(true);
});
it('should remove a root without affecting unrelated txs', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
dg.addDependency(a, b);
dg.removeTransactions(new Set([a]));
expect(dg.size).toBe(1);
expect(dg.hasTx(a)).toBe(false);
expect(b.ancestors.size).toBe(1);
expect(b.ancestors.has(b)).toBe(true);
});
it('should break transitive edges when middle of chain is removed', () => {
const { depgraph, txs } = buildChain(5, 100, 10);
depgraph.removeTransactions(new Set([txs[2]]));
expect(depgraph.size).toBe(4);
expect(txs[0].descendants.has(txs[3])).toBe(false);
expect(txs[0].descendants.has(txs[1])).toBe(true);
expect(txs[3].ancestors.has(txs[0])).toBe(false);
expect(txs[3].descendants.has(txs[4])).toBe(true);
});
it('should handle batch removal of multiple txs', () => {
const { depgraph, txs } = buildChain(5, 100, 10);
depgraph.removeTransactions(new Set([txs[1], txs[3]]));
expect(depgraph.size).toBe(3);
expect(depgraph.hasTx(txs[0])).toBe(true);
expect(depgraph.hasTx(txs[2])).toBe(true);
expect(depgraph.hasTx(txs[4])).toBe(true);
});
it('should result in empty graph when all txs removed', () => {
const { depgraph, txs } = buildChain(3, 100, 10);
depgraph.removeTransactions(new Set(txs));
expect(depgraph.size).toBe(0);
expect(depgraph.getTxs().size).toBe(0);
});
it('should produce clean state when new tx is added after removal', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
dg.addDependency(a, b);
dg.removeTransactions(new Set([a]));
const c = dg.addTransaction('c', 300, 30);
expect(c.ancestors.size).toBe(1);
expect(c.descendants.size).toBe(1);
expect(c.ancestors.has(c)).toBe(true);
expect(b.ancestors.has(c)).toBe(false);
});
});
describe('findConnectedComponents after removal', () => {
it('should split into 2 components when bridge tx is removed', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
const d = dg.addTransaction('d', 400, 40);
dg.addDependency(a, b);
dg.addDependency(b, c);
dg.addDependency(d, c);
dg.removeTransactions(new Set([b]));
const components = dg.findConnectedComponents();
expect(components.length).toBe(2);
const sizes = components.map(comp => comp.size).sort((x, y) => x - y);
expect(sizes).toEqual([1, 2]);
});
it('should produce N singletons when center of star is removed', () => {
const { depgraph, center, leaves } = buildStar(5, 100, 10, 50, 10);
depgraph.removeTransactions(new Set([center]));
const components = depgraph.findConnectedComponents();
expect(components.length).toBe(5);
for (const comp of components) {
expect(comp.size).toBe(1);
}
});
it('should remain 1 component when non-bridge tx is removed', () => {
const { depgraph, txs } = buildDiamond([100, 200, 300, 400], [10, 20, 30, 40]);
depgraph.removeTransactions(new Set([txs[1]]));
const components = depgraph.findConnectedComponents();
expect(components.length).toBe(1);
});
});
describe('restrict edge cases', () => {
it('should restrict to a single tx', () => {
const { depgraph, txs } = buildChain(3, 100, 10);
const { depgraph: sub } = subgraph(new Set([txs[1]]));
expect(sub.size).toBe(1);
});
it('should preserve edges within partial chain subset', () => {
const { depgraph, txs } = buildChain(5, 100, 10);
const subset = new Set([txs[0], txs[1], txs[2]]);
const { depgraph: sub, txMap } = subgraph(subset);
expect(sub.size).toBe(3);
const newA = txMap.get(txs[0]);
const newB = txMap.get(txs[1]);
const newC = txMap.get(txs[2]);
if (!newA || !newB || !newC) {
throw new Error('txMap missing entries');
}
expect(newB.ancestors.has(newA)).toBe(true);
expect(newC.ancestors.has(newB)).toBe(true);
});
});
describe('sortTopological edge cases', () => {
it('should handle subset with no internal dependencies', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
const output = sortTopological(new Set([a, b, c]));
expect(output.length).toBe(3);
expect(new Set(output).size).toBe(3);
});
it('should handle single-tx subset', () => {
const { txs } = buildChain(3, 100, 10);
const output = sortTopological(new Set([txs[1]]));
expect(output).toEqual([txs[1]]);
});
});
describe('addDependency edge cases', () => {
it('should be idempotent when adding the same dependency twice', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
dg.addDependency(a, b);
dg.addDependency(a, b);
expect(b.ancestors.size).toBe(2);
expect(a.descendants.size).toBe(2);
});
it('should handle redundant edge when parent is already transitive ancestor', () => {
const { depgraph, txs } = buildChain(3, 100, 10);
depgraph.addDependency(txs[0], txs[2]);
expect(txs[2].ancestors.size).toBe(3);
expect(txs[2].parents.size).toBe(2);
});
});
});

View file

@ -0,0 +1,485 @@
import { DepGraph } from '../../cluster-mempool/depgraph';
import { chunkify, postLinearize, spanningForestLinearize, linearizeCluster } from '../../cluster-mempool/linearize';
import { buildChain, buildFanOut, buildStar, verifyLinearization, verifyTopologicalOrder } from './test-utils';
describe('chunkify', () => {
it('should create one chunk per tx when feerates are decreasing', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 300, 10);
const b = dg.addTransaction('b', 200, 10);
const c = dg.addTransaction('c', 100, 10);
const chunks = chunkify([a, b, c]);
expect(chunks.length).toBe(3);
expect(chunks[0].txs).toEqual([a]);
expect(chunks[1].txs).toEqual([b]);
expect(chunks[2].txs).toEqual([c]);
});
it('should merge all into one chunk when feerates are increasing', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 10);
const c = dg.addTransaction('c', 300, 10);
const chunks = chunkify([a, b, c]);
expect(chunks.length).toBe(1);
expect(chunks[0].txs).toEqual([a, b, c]);
expect(chunks[0].fee).toBe(600);
expect(chunks[0].weight).toBe(30);
});
it('should NOT merge equal feerates (matching Core behavior)', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 100, 10);
const chunks = chunkify([a, b]);
expect(chunks.length).toBe(2);
});
it('should handle single transaction', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 500, 50);
const chunks = chunkify([a]);
expect(chunks.length).toBe(1);
expect(chunks[0].fee).toBe(500);
expect(chunks[0].weight).toBe(50);
});
it('should handle empty linearization', () => {
const chunks = chunkify([]);
expect(chunks.length).toBe(0);
});
it('should produce decreasing chunk feerates', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 50, 10);
const c = dg.addTransaction('c', 200, 10);
const d = dg.addTransaction('d', 30, 10);
const chunks = chunkify([a, c, b, d]);
for (let i = 1; i < chunks.length; i++) {
const prevRate = chunks[i - 1].fee / chunks[i - 1].weight;
const curRate = chunks[i].fee / chunks[i].weight;
expect(prevRate).toBeGreaterThanOrEqual(curRate);
}
});
});
describe('postLinearize', () => {
it('should improve a bad linearization', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 500, 100);
const result = postLinearize([a, b]);
expect(result[0]).toBe(b);
expect(result[1]).toBe(a);
});
it('should not violate dependencies', () => {
const dg = new DepGraph();
const parent = dg.addTransaction('parent', 100, 100);
const child = dg.addTransaction('child', 500, 100);
dg.addDependency(parent, child);
const result = postLinearize([parent, child]);
expect(result.indexOf(parent)).toBeLessThan(result.indexOf(child));
});
it('should handle already-optimal ordering', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 500, 100);
const b = dg.addTransaction('b', 100, 100);
const result = postLinearize([a, b]);
expect(result).toEqual([a, b]);
});
});
describe('spanningForestLinearize', () => {
it('should sort independent transactions by feerate', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 500, 100);
const c = dg.addTransaction('c', 300, 100);
const result = spanningForestLinearize(dg.getTxs());
expect(result[0]).toBe(b);
expect(result[1]).toBe(c);
expect(result[2]).toBe(a);
});
it('should respect dependencies', () => {
const dg = new DepGraph();
const parent = dg.addTransaction('parent', 100, 100);
const child = dg.addTransaction('child', 500, 100);
dg.addDependency(parent, child);
const result = spanningForestLinearize(dg.getTxs());
expect(result.indexOf(parent)).toBeLessThan(result.indexOf(child));
});
it('should handle CPFP pattern', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 10000, 100);
dg.addDependency(a, b);
const { chunks } = linearizeCluster(dg.getTxs());
expect(chunks.length).toBe(1);
expect(chunks[0].txs.length).toBe(2);
});
it('should separate high and low feerate independent txs', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 1000, 100);
const b = dg.addTransaction('b', 100, 100);
const { chunks } = linearizeCluster(dg.getTxs());
expect(chunks.length).toBe(2);
expect(chunks[0].txs).toContain(a);
expect(chunks[1].txs).toContain(b);
});
it('should handle single transaction', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 1000, 100);
const result = spanningForestLinearize(dg.getTxs());
expect(result).toEqual([a]);
});
it('should handle empty graph', () => {
const dg = new DepGraph();
const result = spanningForestLinearize(dg.getTxs());
expect(result).toEqual([]);
});
});
describe('minimize', () => {
it('should keep equal-feerate chain as individual chunks', () => {
const dg = new DepGraph();
const txs: any[] = [];
for (let i = 0; i < 5; i++) {
txs.push(dg.addTransaction(`tx${i}`, 19, 140));
}
for (let i = 1; i < 5; i++) {
dg.addDependency(txs[i - 1], txs[i]);
}
const { chunks } = linearizeCluster(dg.getTxs());
expect(chunks.length).toBe(5);
for (const chunk of chunks) {
expect(chunk.txs.length).toBe(1);
}
});
it('should merge chain where child has strictly higher feerate', () => {
const dg = new DepGraph();
const parent = dg.addTransaction('parent', 100, 200);
const child = dg.addTransaction('child', 900, 100);
dg.addDependency(parent, child);
const { chunks } = linearizeCluster(dg.getTxs());
expect(chunks.length).toBe(1);
expect(chunks[0].txs.length).toBe(2);
});
it('should split parent-child with equal feerate', () => {
const dg = new DepGraph();
const parent = dg.addTransaction('parent', 1720, 344);
const child = dg.addTransaction('child', 1240, 248);
dg.addDependency(parent, child);
const { chunks } = linearizeCluster(dg.getTxs());
expect(chunks.length).toBe(2);
});
it('should split disconnected equal-feerate components', () => {
const dg = new DepGraph();
dg.addTransaction('a', 100, 100);
dg.addTransaction('b', 100, 100);
const { chunks } = linearizeCluster(dg.getTxs());
expect(chunks.length).toBe(2);
expect(chunks[0].txs.length).toBe(1);
expect(chunks[1].txs.length).toBe(1);
});
});
describe('chunkify edge cases', () => {
it('should produce N separate chunks when all feerates are equal', () => {
const dg = new DepGraph();
const txs: any[] = [];
for (let i = 0; i < 5; i++) {
txs.push(dg.addTransaction(`tx${i}`, 100, 10));
}
const chunks = chunkify(txs);
expect(chunks.length).toBe(5);
});
it('should handle alternating high/low feerates', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 1000, 10);
const b = dg.addTransaction('b', 100, 10);
const c = dg.addTransaction('c', 1000, 10);
const d = dg.addTransaction('d', 100, 10);
const chunks = chunkify([a, b, c, d]);
for (let i = 1; i < chunks.length; i++) {
const prevRate = chunks[i - 1].fee * chunks[i].weight;
const curRate = chunks[i].fee * chunks[i - 1].weight;
expect(prevRate).toBeGreaterThanOrEqual(curRate);
}
});
it('should merge all when single very high feerate tx is at the end', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 10, 10);
const b = dg.addTransaction('b', 10, 10);
const c = dg.addTransaction('c', 10, 10);
const d = dg.addTransaction('d', 10000, 10);
const chunks = chunkify([a, b, c, d]);
expect(chunks.length).toBe(1);
expect(chunks[0].txs.length).toBe(4);
});
it('should maintain non-increasing feerates for 50+ tx linearization', () => {
const dg = new DepGraph();
const txs: any[] = [];
for (let i = 0; i < 50; i++) {
txs.push(dg.addTransaction(`tx${i}`, 5000 - i * 100, 100));
}
const chunks = chunkify(txs);
for (let i = 1; i < chunks.length; i++) {
const prevRate = chunks[i - 1].fee * chunks[i].weight;
const curRate = chunks[i].fee * chunks[i - 1].weight;
expect(prevRate).toBeGreaterThanOrEqual(curRate);
}
});
it('should handle zero-fee transaction', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 1000, 100);
const b = dg.addTransaction('b', 0, 100);
const chunks = chunkify([a, b]);
expect(chunks.length).toBe(2);
expect(chunks[1].fee).toBe(0);
});
});
describe('postLinearize edge cases', () => {
it('should sort three independent txs by feerate', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 500, 100);
const c = dg.addTransaction('c', 300, 100);
const result = postLinearize([a, c, b]);
expect(result[0]).toBe(b);
expect(result[2]).toBe(a);
});
it('should respect parent-child dependency even when child has higher feerate', () => {
const dg = new DepGraph();
const parent = dg.addTransaction('parent', 100, 100);
const child = dg.addTransaction('child', 1000, 100);
dg.addDependency(parent, child);
const result = postLinearize([parent, child]);
expect(result[0]).toBe(parent);
expect(result[1]).toBe(child);
});
it('should handle chain A→B→C with CPFP-like feerates', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 200, 100);
const c = dg.addTransaction('c', 10000, 100);
dg.addDependency(a, b);
dg.addDependency(b, c);
const result = postLinearize([a, b, c]);
verifyTopologicalOrder(result);
});
});
describe('SFL adversarial topologies', () => {
it('should handle comb pattern: one root with many children at different feerates', () => {
const dg = new DepGraph();
const root = dg.addTransaction('root', 100, 100);
for (let i = 0; i < 8; i++) {
const child = dg.addTransaction(`child${i}`, (i + 1) * 500, 100);
dg.addDependency(root, child);
}
const { linearization, chunks } = linearizeCluster(dg.getTxs());
verifyLinearization(dg.getTxs(), linearization, chunks);
});
it('should handle inverted tree: many leaves → intermediates → root', () => {
const dg = new DepGraph();
const root = dg.addTransaction('root', 100, 100);
const mid1 = dg.addTransaction('mid1', 200, 100);
const mid2 = dg.addTransaction('mid2', 300, 100);
dg.addDependency(root, mid1);
dg.addDependency(root, mid2);
for (let i = 0; i < 4; i++) {
const leaf = dg.addTransaction(`leaf${i}`, 5000, 100);
dg.addDependency(i < 2 ? mid1 : mid2, leaf);
}
const { linearization, chunks } = linearizeCluster(dg.getTxs());
verifyLinearization(dg.getTxs(), linearization, chunks);
});
it('should handle two parallel chains with shared root', () => {
const dg = new DepGraph();
const root = dg.addTransaction('root', 100, 100);
let prev1: any = root;
for (let i = 0; i < 5; i++) {
const tx = dg.addTransaction(`chain1_${i}`, 200, 100);
dg.addDependency(prev1, tx);
prev1 = tx;
}
let prev2: any = root;
for (let i = 0; i < 3; i++) {
const tx = dg.addTransaction(`chain2_${i}`, 300, 100);
dg.addDependency(prev2, tx);
prev2 = tx;
}
const { linearization, chunks } = linearizeCluster(dg.getTxs());
verifyLinearization(dg.getTxs(), linearization, chunks);
});
it('should handle deep CPFP: low-fee chain with high-fee tip', () => {
const { depgraph, txs } = buildChain(6, 10, 100);
txs[5].effectiveFee = 50000;
const { linearization, chunks } = linearizeCluster(depgraph.getTxs());
verifyLinearization(depgraph.getTxs(), linearization, chunks);
expect(chunks[0].txs.length).toBeGreaterThan(1);
});
it('should find better result than ancestor-feerate for overlapping high-feerate subsets', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 100, 100);
const c = dg.addTransaction('c', 10000, 100);
dg.addDependency(a, c);
dg.addDependency(b, c);
const { linearization, chunks } = linearizeCluster(dg.getTxs());
verifyLinearization(dg.getTxs(), linearization, chunks);
const firstChunkFee = chunks[0].fee;
const firstChunkSize = chunks[0].weight;
expect(firstChunkFee / firstChunkSize).toBeGreaterThan(100 / 100);
});
});
describe('linearizeCluster', () => {
it('should produce valid topological linearization', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 10);
const b = dg.addTransaction('b', 200, 20);
const c = dg.addTransaction('c', 300, 30);
dg.addDependency(a, b);
dg.addDependency(b, c);
const { linearization } = linearizeCluster(dg.getTxs());
expect(linearization.indexOf(a)).toBeLessThan(linearization.indexOf(b));
expect(linearization.indexOf(b)).toBeLessThan(linearization.indexOf(c));
});
it('should produce monotonically decreasing chunk feerates', () => {
const dg = new DepGraph();
for (let i = 0; i < 10; i++) {
dg.addTransaction(`tx${i}`, Math.floor(Math.random() * 10000) + 100, Math.floor(Math.random() * 500) + 50);
}
const { chunks } = linearizeCluster(dg.getTxs());
for (let i = 1; i < chunks.length; i++) {
const prevRate = chunks[i - 1].fee * chunks[i].weight;
const curRate = chunks[i].fee * chunks[i - 1].weight;
expect(prevRate).toBeGreaterThanOrEqual(curRate);
}
});
it('should handle complex diamond dependency graph', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 1000, 100);
const b = dg.addTransaction('b', 500, 100);
const c = dg.addTransaction('c', 100, 100);
const d = dg.addTransaction('d', 300, 100);
dg.addDependency(a, b);
dg.addDependency(a, c);
dg.addDependency(b, d);
dg.addDependency(c, d);
const { linearization, chunks } = linearizeCluster(dg.getTxs());
verifyLinearization(dg.getTxs(), linearization, chunks);
});
it('should produce at-least-as-good result when given suboptimal hint', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 500, 100);
const b = dg.addTransaction('b', 100, 100);
const c = dg.addTransaction('c', 1000, 100);
const suboptimal = [b, a, c];
const { chunks: hintChunks } = linearizeCluster(dg.getTxs(), suboptimal);
const { chunks: freshChunks } = linearizeCluster(dg.getTxs());
const hintFirstFeerate = hintChunks[0].fee * freshChunks[0].weight;
const freshFirstFeerate = freshChunks[0].fee * hintChunks[0].weight;
expect(hintFirstFeerate).toBeGreaterThanOrEqual(freshFirstFeerate - 1);
});
it('should preserve an already-optimal linearization', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 1000, 100);
const b = dg.addTransaction('b', 500, 100);
const c = dg.addTransaction('c', 100, 100);
const optimal = [a, b, c];
const { linearization } = linearizeCluster(dg.getTxs(), optimal);
expect(linearization).toEqual(optimal);
});
it('should produce valid linearizations on repeated calls', () => {
const dg = new DepGraph();
const a = dg.addTransaction('a', 100, 100);
const b = dg.addTransaction('b', 100, 100);
const c = dg.addTransaction('c', 100, 100);
dg.addDependency(a, c);
dg.addDependency(b, c);
const result1 = linearizeCluster(dg.getTxs());
const result2 = linearizeCluster(dg.getTxs());
verifyLinearization(dg.getTxs(), result1.linearization, result1.chunks);
verifyLinearization(dg.getTxs(), result2.linearization, result2.chunks);
});
it('should pass invariant checks for fan-out topology', () => {
const { depgraph } = buildFanOut(6, 100, 100, 500, 100);
const { linearization, chunks } = linearizeCluster(depgraph.getTxs());
verifyLinearization(depgraph.getTxs(), linearization, chunks);
});
it('should pass invariant checks for star topology', () => {
const { depgraph } = buildStar(5, 100, 100, 300, 100);
const { linearization, chunks } = linearizeCluster(depgraph.getTxs());
verifyLinearization(depgraph.getTxs(), linearization, chunks);
});
});

View file

@ -0,0 +1,175 @@
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { ClusterTx, DepGraph } from '../../cluster-mempool/depgraph';
import { LinearizationChunk } from '../../cluster-mempool/linearize';
export function makeTx(
txid: string,
fee: number,
vsize: number,
parentTxids: string[] = [],
): MempoolTransactionExtended {
const vin = parentTxids.length > 0
? parentTxids.map(ptxid => ({
txid: ptxid,
vout: 0,
is_coinbase: false,
scriptsig: '',
scriptsig_asm: '',
inner_redeemscript_asm: '',
inner_witnessscript_asm: '',
sequence: 0,
witness: [] as string[],
prevout: null,
}))
: [{
txid: '0000000000000000000000000000000000000000000000000000000000000000',
vout: 0,
is_coinbase: false,
scriptsig: '',
scriptsig_asm: '',
inner_redeemscript_asm: '',
inner_witnessscript_asm: '',
sequence: 0,
witness: [] as string[],
prevout: null,
}];
return {
txid,
version: 2,
locktime: 0,
size: vsize,
weight: vsize * 4,
fee,
vin,
vout: [{
scriptpubkey: '',
scriptpubkey_asm: '',
scriptpubkey_type: 'v0_p2wpkh',
value: 50000,
}],
status: { confirmed: false },
vsize,
feePerVsize: fee / vsize,
effectiveFeePerVsize: fee / vsize,
order: 0,
sigops: 0,
adjustedVsize: vsize,
adjustedFeePerVsize: fee / vsize,
} as MempoolTransactionExtended;
}
export function txid(short: string): string {
return short.padStart(64, '0');
}
export function buildChain(
n: number,
baseFee: number,
baseSize: number,
): { depgraph: DepGraph; txs: ClusterTx[] } {
const depgraph = new DepGraph();
const txs: ClusterTx[] = [];
for (let i = 0; i < n; i++) {
txs.push(depgraph.addTransaction(`chain_${i}`, baseFee, baseSize));
}
for (let i = 1; i < n; i++) {
depgraph.addDependency(txs[i - 1], txs[i]);
}
return { depgraph, txs };
}
export function buildFanOut(
nChildren: number,
parentFee: number,
parentSize: number,
childFee: number,
childSize: number,
): { depgraph: DepGraph; parent: ClusterTx; children: ClusterTx[] } {
const depgraph = new DepGraph();
const parent = depgraph.addTransaction('fanout_parent', parentFee, parentSize);
const children: ClusterTx[] = [];
for (let i = 0; i < nChildren; i++) {
const child = depgraph.addTransaction(`fanout_child_${i}`, childFee, childSize);
depgraph.addDependency(parent, child);
children.push(child);
}
return { depgraph, parent, children };
}
export function buildDiamond(
fees: [number, number, number, number],
sizes: [number, number, number, number],
): { depgraph: DepGraph; txs: [ClusterTx, ClusterTx, ClusterTx, ClusterTx] } {
const depgraph = new DepGraph();
const a = depgraph.addTransaction('diamond_a', fees[0], sizes[0]);
const b = depgraph.addTransaction('diamond_b', fees[1], sizes[1]);
const c = depgraph.addTransaction('diamond_c', fees[2], sizes[2]);
const d = depgraph.addTransaction('diamond_d', fees[3], sizes[3]);
depgraph.addDependency(a, b);
depgraph.addDependency(a, c);
depgraph.addDependency(b, d);
depgraph.addDependency(c, d);
return { depgraph, txs: [a, b, c, d] };
}
export function buildStar(
nLeaves: number,
centerFee: number,
centerSize: number,
leafFee: number,
leafSize: number,
): { depgraph: DepGraph; center: ClusterTx; leaves: ClusterTx[] } {
const depgraph = new DepGraph();
const center = depgraph.addTransaction('star_center', centerFee, centerSize);
const leaves: ClusterTx[] = [];
for (let i = 0; i < nLeaves; i++) {
const leaf = depgraph.addTransaction(`star_leaf_${i}`, leafFee, leafSize);
depgraph.addDependency(center, leaf);
leaves.push(leaf);
}
return { depgraph, center, leaves };
}
export function verifyTopologicalOrder(ordering: ClusterTx[]): void {
const positionMap = new Map<ClusterTx, number>();
for (let i = 0; i < ordering.length; i++) {
positionMap.set(ordering[i], i);
}
for (const tx of ordering) {
for (const parent of tx.parents) {
const parentPos = positionMap.get(parent);
const childPos = positionMap.get(tx);
if (parentPos !== undefined && childPos !== undefined) {
expect(parentPos).toBeLessThan(childPos);
}
}
}
}
export function verifyLinearization(
txs: Set<ClusterTx>,
linearization: ClusterTx[],
chunks: LinearizationChunk[],
): void {
expect(linearization.length).toBe(txs.size);
const linSet = new Set(linearization);
expect(linSet.size).toBe(linearization.length);
for (const tx of txs) {
expect(linSet.has(tx)).toBe(true);
}
verifyTopologicalOrder(linearization);
for (let i = 1; i < chunks.length; i++) {
const prevRate = chunks[i - 1].fee * chunks[i].weight;
const curRate = chunks[i].fee * chunks[i - 1].weight;
expect(prevRate).toBeGreaterThanOrEqual(curRate);
}
const chunkTxs = chunks.flatMap(c => c.txs);
expect(chunkTxs.length).toBe(linearization.length);
for (let i = 0; i < chunkTxs.length; i++) {
expect(chunkTxs[i]).toBe(linearization[i]);
}
}