Skip to content

Commit a532ab0

Browse files
committed
fix(indexer): a pending log would have been written as a trade in block zero
blockNumber and logIndex are nullable in viem's type because a log from a pending block has neither. Every insert site coerced that to zero with `?? 0n`, which would write a real trade attached to block zero at a log index colliding with that block's genuine first log — and the foreign key would have accepted it, because block zero exists. getLogs over a fixed range never returns a pending log, so this should be unreachable. That is the argument for refusing rather than defaulting: an unreachable state that silently produces a plausible row is worse than one that fails the tick and retries the range. Pending logs are now rejected before anything is sorted, since sorting them as zero placed them ahead of every real log in the range. Found by sweeping for the placeholder shape that produced the last three bugs: a value that stands in for an impossible state, typechecks forever, and is indistinguishable from a real one once written.
1 parent 4917532 commit a532ab0

1 file changed

Lines changed: 48 additions & 7 deletions

File tree

services/indexer/src/ingest.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,23 @@ export class Indexer {
324324
logs = [...logs, ...extra];
325325
}
326326

327+
/*
328+
* Pending logs are refused before anything is sorted or written.
329+
*
330+
* `getLogs` over a fixed range should never return one, so this is a
331+
* consistency check on the node rather than an expected case. Sorting them
332+
* as zero would have placed a pending log ahead of every real one in the
333+
* range, and the insert sites would then have written it as a trade in
334+
* block zero.
335+
*/
336+
for (const log of logs) {
337+
if (log.blockNumber === null || log.logIndex === null) {
338+
throw new Error(
339+
`[indexer] node returned a pending log for range ${from}..${to} from ${log.address}`,
340+
);
341+
}
342+
}
343+
327344
const ordered = [...logs].sort((a, b) => {
328345
const blockDelta = (a.blockNumber ?? 0n) - (b.blockNumber ?? 0n);
329346
if (blockDelta !== 0n) return blockDelta < 0n ? -1 : 1;
@@ -613,7 +630,7 @@ export class Indexer {
613630
pg: p0 * 25n,
614631
qG: (TOTAL_SUPPLY * 50n) / 76n,
615632
launchedAt: timestamp,
616-
launchedAtBlock: log.blockNumber ?? 0n,
633+
launchedAtBlock: this.positionOf(log).blockNumber,
617634
});
618635

619636
// Staged, NOT applied. This runs inside the advance transaction, and a
@@ -630,8 +647,7 @@ export class Indexer {
630647

631648
const market = log.address.toLowerCase() as `0x${string}`;
632649
const a = decoded.args as Record<string, unknown>;
633-
const blockNumber = log.blockNumber ?? 0n;
634-
const logIndex = log.logIndex ?? 0;
650+
const { blockNumber, logIndex } = this.positionOf(log);
635651

636652
if (decoded.eventName === "Bought" || decoded.eventName === "Sold") {
637653
const isBuy = decoded.eventName === "Bought";
@@ -726,8 +742,7 @@ export class Indexer {
726742
const from = String(a.from).toLowerCase();
727743
const to = String(a.to).toLowerCase();
728744
const value = a.value as bigint;
729-
const blockNumber = log.blockNumber ?? 0n;
730-
const logIndex = log.logIndex ?? 0;
745+
const { blockNumber, logIndex } = this.positionOf(log);
731746

732747
/*
733748
* Both sides are recorded. A transfer moves exposure rather than creating it,
@@ -775,9 +790,11 @@ export class Indexer {
775790

776791
const a = decoded.args as Record<string, unknown>;
777792

793+
const { blockNumber, logIndex } = this.positionOf(log);
794+
778795
await recordStockbackFunding(tx, {
779-
blockNumber: log.blockNumber ?? 0n,
780-
logIndex: log.logIndex ?? 0,
796+
blockNumber,
797+
logIndex,
781798
market: String(a.market).toLowerCase(),
782799
amount: a.amount as bigint,
783800
totalFunded: a.totalFunded as bigint,
@@ -821,6 +838,30 @@ export class Indexer {
821838
* The curve is rebuilt from the market's own `p0`, which is recorded at launch
822839
* and never re-anchored (§402).
823840
*/
841+
/**
842+
* A log's position on the chain, or a refusal.
843+
*
844+
* `blockNumber` and `logIndex` are nullable in viem's type because a log from
845+
* a pending block has neither. Coercing that to zero — which is what `?? 0n`
846+
* did at every insert site — writes a real trade attached to block zero, at a
847+
* log index that collides with the genuine first log of that block.
848+
*
849+
* `getLogs` over a fixed range never returns a pending log, so this should be
850+
* unreachable. That is exactly why it throws rather than defaulting: the tick
851+
* fails, the cursor holds, and the range is retried — instead of a plausible
852+
* row landing in the projection with nothing to indicate it is wrong.
853+
*/
854+
private positionOf(log: Log): { blockNumber: bigint; logIndex: number } {
855+
if (log.blockNumber === null || log.logIndex === null) {
856+
throw new Error(
857+
`[indexer] refusing a pending log from ${log.address}: ` +
858+
`block ${String(log.blockNumber)}, index ${String(log.logIndex)}`,
859+
);
860+
}
861+
862+
return { blockNumber: log.blockNumber, logIndex: log.logIndex };
863+
}
864+
824865
private priceAfter(market: string, distributed: bigint): bigint {
825866
const known =
826867
this.knownMarkets.get(market) ?? this.pendingMarkets.find((m) => m.market === market);

0 commit comments

Comments
 (0)