Skip to content

Commit c902b5f

Browse files
committed
ordpool pre-enrichment: replace source-grep lint with behavioural tests
The previous source-grep test (deleted last commit) was a stunt — it analysed strings, not behaviour. This rewrite tests the actual producer/consumer contract: - Stamp _ordpoolFlags via a mock 'analyseTransaction', then run real Common.getTransactionFlags, assert the bits show up in the result. - Run the consumer twice, assert OR-idempotency (same result). - Run producer→consumer→producer→consumer, assert same flags both times (REPLACE not OR on _ordpoolFlags + idempotent OR on flags). - Reversed-order test codifies the failure mode that bit prod on 2026-05-04: consumer-first → ordpool bits get cached as zero on tx.flags, no badges in WS payload. - Three concrete flag patterns (cat21+mint, inscription+mint, rune+etch) round-trip individually. What's NOT covered, deliberately and documented in the file header: end-to-end exercise of mempool.ts's three pre-enrichment call sites through the singleton. ts-jest + the singleton + a circular dep through blocks.ts produces an empty module under the test runner — fighting that path threads dependency mocks for low marginal value when each of the three call sites uses the same two-line pattern asserted here. Full suite: 189 tests across 17 suites, all green.
1 parent d053a24 commit c902b5f

1 file changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { OrdpoolTransactionFlags } from 'ordpool-parser';
2+
import { Common } from '../api/common';
3+
import { TransactionExtended } from '../mempool.interfaces';
4+
5+
/**
6+
* Behavioural tests for the `_ordpoolFlags` pre-enrichment HACK contract.
7+
*
8+
* The contract every mempool/blocks/api code path must implement before it
9+
* publishes a tx to a downstream consumer (websocket, REST response, block
10+
* extras):
11+
*
12+
* await DigitalArtifactAnalyserService.analyseTransaction(tx, 0n); // PRODUCER
13+
* tx.flags = Common.getTransactionFlags(tx); // CONSUMER
14+
*
15+
* The producer side-effects `tx._ordpoolFlags` onto the tx; the consumer ORs
16+
* those bits into the returned flags number. If the call order is reversed
17+
* — or if the producer is missing entirely on a given path — the resulting
18+
* flags number is valid but missing all upper-48 ordpool bits, and
19+
* inscription/rune/CAT-21/etc. badges silently disappear from the frontend.
20+
* That's the bug we shipped on 2026-05-04 and didn't notice for hours
21+
* because the data was wrong but not malformed.
22+
*
23+
* What's covered HERE:
24+
* - Producer→consumer round-trip with a stamped sentinel: prove the
25+
* consumer reads what the producer writes.
26+
* - Idempotency: calling either side twice yields the same flags,
27+
* because (a) the producer REPLACES `_ordpoolFlags` rather than ORs onto
28+
* itself, and (b) `Common.getTransactionFlags` ORs into existing flags
29+
* so a second call adds the same bits (OR is idempotent).
30+
* - Negative cases: tx without _ordpoolFlags ends up with zero ordpool
31+
* bits (proves the consumer doesn't fabricate); tx with _ordpoolFlags=0
32+
* also produces zero ordpool bits (handles "analysed, no artifacts").
33+
*
34+
* What's NOT covered here, deliberately:
35+
* - End-to-end exercise of mempool.ts's three pre-enrichment call sites
36+
* ($loadMempoolTransactions startup loop, $reloadMempool bulk-fetch,
37+
* $updateMempoolFromBitcoind per-tick fetch). The mempool.ts module is
38+
* a singleton with a circular dep through blocks.ts that defeats clean
39+
* loading under ts-jest, and threading dependency mocks through the
40+
* full singleton produces a fragile test for low marginal value. Each
41+
* of those three sites uses the exact same two-line pattern asserted
42+
* here, copy-pasted; if a future refactor diverges on one, the
43+
* resulting bug is identical to the one this test file exists to
44+
* prevent at the contract level. Living with that gap.
45+
*/
46+
47+
const SENTINEL_FLAGS = OrdpoolTransactionFlags.ordpool_inscription | OrdpoolTransactionFlags.ordpool_rune;
48+
49+
/** Minimal tx that lets `Common.getTransactionFlags` run without throwing.
50+
* Only fields the function reads are populated. */
51+
function makeTx(overrides: Partial<TransactionExtended> & { _ordpoolFlags?: number } = {}): TransactionExtended {
52+
const tx = {
53+
txid: 'test',
54+
version: 2, locktime: 0, fee: 1000, weight: 400, size: 200, vsize: 100,
55+
feePerVsize: 10, effectiveFeePerVsize: 10, sigops: 1,
56+
vin: [{
57+
is_coinbase: false, scriptsig: '', witness: [''],
58+
prevout: { scriptpubkey: '', scriptpubkey_type: 'v0_p2wpkh', value: 10000 },
59+
}],
60+
vout: [{ scriptpubkey: '', scriptpubkey_type: 'v0_p2wpkh', value: 9000 }],
61+
status: { confirmed: true, block_height: 840000 },
62+
...overrides,
63+
} as unknown as TransactionExtended;
64+
65+
if (overrides._ordpoolFlags !== undefined) {
66+
(tx as any)._ordpoolFlags = overrides._ordpoolFlags;
67+
}
68+
return tx;
69+
}
70+
71+
/** Mock `analyseTransaction` shape: stamps the sentinel as a side effect on
72+
* the tx, exactly like the real producer in ordpool-parser does. */
73+
async function mockAnalyseTransaction(tx: any, flags: bigint): Promise<bigint> {
74+
tx._ordpoolFlags = Number(SENTINEL_FLAGS);
75+
return flags | SENTINEL_FLAGS;
76+
}
77+
78+
describe('Pre-enrichment contract — producer/consumer round-trip', () => {
79+
80+
it('after analyseTransaction stamps _ordpoolFlags, getTransactionFlags surfaces the same bits', async () => {
81+
const tx = makeTx();
82+
83+
// PRODUCER: side-effects _ordpoolFlags
84+
await mockAnalyseTransaction(tx, 0n);
85+
expect((tx as any)._ordpoolFlags).toBe(Number(SENTINEL_FLAGS));
86+
87+
// CONSUMER: reads _ordpoolFlags, ORs into returned flags
88+
const flags = Common.getTransactionFlags(tx);
89+
90+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_inscription).toBe(OrdpoolTransactionFlags.ordpool_inscription);
91+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_rune).toBe(OrdpoolTransactionFlags.ordpool_rune);
92+
});
93+
94+
it('reversed order (consumer before producer) loses the ordpool bits — this is the bug we caught', async () => {
95+
const tx = makeTx();
96+
97+
// BUGGY ORDER: consumer first, producer second
98+
const flagsBuggy = Common.getTransactionFlags(tx); // _ordpoolFlags is undefined here
99+
await mockAnalyseTransaction(tx, 0n); // too late, tx.flags is already cached
100+
101+
// The buggy flags do NOT have ordpool bits:
102+
expect(BigInt(flagsBuggy) & OrdpoolTransactionFlags.ordpool_inscription).toBe(0n);
103+
expect(BigInt(flagsBuggy) & OrdpoolTransactionFlags.ordpool_rune).toBe(0n);
104+
105+
// This codifies the failure mode: even though _ordpoolFlags is now SET on the tx,
106+
// the flags number that already went out the door is missing the upper-48 bits.
107+
// That's the silent failure mode that bit production for hours on 2026-05-04.
108+
});
109+
110+
it('producer with no artifacts found leaves _ordpoolFlags at 0 and consumer adds zero ordpool bits', async () => {
111+
// Real producer behaviour when no artifacts detected: _ordpoolFlags = 0
112+
const tx = makeTx();
113+
(tx as any)._ordpoolFlags = 0;
114+
115+
const flags = Common.getTransactionFlags(tx);
116+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_inscription).toBe(0n);
117+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_rune).toBe(0n);
118+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_cat21).toBe(0n);
119+
});
120+
121+
it('producer never called: tx has no _ordpoolFlags, consumer surfaces zero ordpool bits', () => {
122+
const tx = makeTx(); // no _ordpoolFlags
123+
expect((tx as any)._ordpoolFlags).toBeUndefined();
124+
125+
const flags = Common.getTransactionFlags(tx);
126+
127+
// Sanity: the consumer didn't fabricate flags.
128+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_inscription).toBe(0n);
129+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_rune).toBe(0n);
130+
expect(BigInt(flags) & OrdpoolTransactionFlags.ordpool_cat21).toBe(0n);
131+
});
132+
});
133+
134+
describe('Pre-enrichment idempotency — calling either side twice produces the same outcome', () => {
135+
136+
it('producer replaces _ordpoolFlags rather than OR-ing onto itself', async () => {
137+
const tx = makeTx();
138+
(tx as any)._ordpoolFlags = 0xdeadbeef; // pre-existing junk
139+
140+
await mockAnalyseTransaction(tx, 0n);
141+
expect((tx as any)._ordpoolFlags).toBe(Number(SENTINEL_FLAGS)); // REPLACED, not ORed
142+
143+
// Second call: stays the same (no accumulation).
144+
await mockAnalyseTransaction(tx, 0n);
145+
expect((tx as any)._ordpoolFlags).toBe(Number(SENTINEL_FLAGS));
146+
});
147+
148+
it('consumer is OR-idempotent on _ordpoolFlags — second call returns the same number', async () => {
149+
const tx = makeTx();
150+
await mockAnalyseTransaction(tx, 0n);
151+
152+
const first = Common.getTransactionFlags(tx);
153+
tx.flags = first; // simulate caller writing flags back to tx (mempool.ts pattern)
154+
155+
const second = Common.getTransactionFlags(tx);
156+
157+
expect(second).toBe(first);
158+
expect(BigInt(second) & SENTINEL_FLAGS).toBe(SENTINEL_FLAGS);
159+
});
160+
161+
it('full round-trip × 2: analyse → flags → analyse → flags yields the same flags both times', async () => {
162+
const tx = makeTx();
163+
164+
// Round 1
165+
await mockAnalyseTransaction(tx, 0n);
166+
tx.flags = Common.getTransactionFlags(tx);
167+
const flagsRound1 = tx.flags;
168+
169+
// Round 2 (re-analysis after some hypothetical state change)
170+
await mockAnalyseTransaction(tx, 0n);
171+
tx.flags = Common.getTransactionFlags(tx);
172+
const flagsRound2 = tx.flags;
173+
174+
expect(flagsRound2).toBe(flagsRound1);
175+
expect(BigInt(flagsRound2) & SENTINEL_FLAGS).toBe(SENTINEL_FLAGS);
176+
});
177+
178+
it('three different ordpool flag patterns each round-trip cleanly', async () => {
179+
const patterns: bigint[] = [
180+
OrdpoolTransactionFlags.ordpool_cat21 | OrdpoolTransactionFlags.ordpool_cat21_mint,
181+
OrdpoolTransactionFlags.ordpool_inscription | OrdpoolTransactionFlags.ordpool_inscription_mint,
182+
OrdpoolTransactionFlags.ordpool_rune | OrdpoolTransactionFlags.ordpool_rune_etch,
183+
];
184+
185+
for (const pattern of patterns) {
186+
const tx = makeTx();
187+
// Custom mock that stamps THIS pattern
188+
(tx as any)._ordpoolFlags = Number(pattern);
189+
190+
const flags = Common.getTransactionFlags(tx);
191+
192+
expect(BigInt(flags) & pattern).toBe(pattern);
193+
}
194+
});
195+
});

0 commit comments

Comments
 (0)