mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Add cluster mempool backend unit tests
This commit is contained in:
parent
4af8f5abc3
commit
4718a04182
4 changed files with 1627 additions and 0 deletions
473
backend/src/__tests__/cluster-mempool/cluster-mempool.test.ts
Normal file
473
backend/src/__tests__/cluster-mempool/cluster-mempool.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
494
backend/src/__tests__/cluster-mempool/depgraph.test.ts
Normal file
494
backend/src/__tests__/cluster-mempool/depgraph.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
485
backend/src/__tests__/cluster-mempool/linearize.test.ts
Normal file
485
backend/src/__tests__/cluster-mempool/linearize.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
175
backend/src/__tests__/cluster-mempool/test-utils.ts
Normal file
175
backend/src/__tests__/cluster-mempool/test-utils.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue