Summary
ReceiptIndex.putBlockReceipts and LogIndex.appendBlockLogs are not failure-atomic. On allocator failures, they can leave internal indexes partially mutated or holding dangling ownership, which can later produce inconsistent query results and double-free risk during teardown.
Why this matters
These indexes back canonical receipt/log RPC surfaces (eth_getTransactionReceipt, eth_getBlockReceipts, eth_getLogs). Under memory pressure, a failed write should not corrupt index state.
Evidence
1) ReceiptIndex.putBlockReceipts can leave by_tx out-of-sync with by_block and dangling ownership on error
const cloned = try allocator.alloc(primitives.Receipt.Receipt, receipts.len);
errdefer allocator.free(cloned);
for (receipts, 0..) |receipt, i| {
cloned[i] = try receipt.clone(allocator);
errdefer cloned[i].deinit(allocator);
try self.by_tx.put(cloned[i].transaction_hash, cloned[i]);
}
try self.by_block.put(block_hash, cloned);
If any put fails after earlier inserts, function returns with partial writes already present in by_tx. If by_block.put fails after loop success, all by_tx entries were inserted but by_block is missing the block, so indexes diverge. Because errdefer deinitializes cloned receipts on failure, by_tx can retain values whose owned internals were already freed.
2) LogIndex.appendBlockLogs can commit logs without committing block_range
for (receipts) |receipt| {
for (receipt.logs) |log| {
const cloned = try primitives.EventLog.clone(allocator, log);
try self.logs.append(allocator, .{
.log = cloned,
.block_hash = block_hash,
});
}
}
const end = self.logs.items.len;
try self.block_range.put(allocator, block_number, .{ .start = start, .end = end });
If block_range.put fails, the append already happened and no rollback occurs. Range queries (from_block/to_block) then miss those committed logs, while block-hash scans can still observe them.
Suggested scope
- Make both mutation paths transactional:
- stage data first,
- commit both indexes only after all allocations succeed,
- rollback partial writes on failure.
- In
ReceiptIndex, prevent by_tx insertion until by_block ownership is guaranteed (or add explicit undo list).
- In
LogIndex, rollback appended logs when block_range.put fails.
- Add fail-allocator tests for both modules to assert no partial state and no leaks/double-frees on simulated OOM.
Summary
ReceiptIndex.putBlockReceiptsandLogIndex.appendBlockLogsare not failure-atomic. On allocator failures, they can leave internal indexes partially mutated or holding dangling ownership, which can later produce inconsistent query results and double-free risk during teardown.Why this matters
These indexes back canonical receipt/log RPC surfaces (
eth_getTransactionReceipt,eth_getBlockReceipts,eth_getLogs). Under memory pressure, a failed write should not corrupt index state.Evidence
1)
ReceiptIndex.putBlockReceiptscan leaveby_txout-of-sync withby_blockand dangling ownership on errorsrc/receipt_index.zig:36src/receipt_index.zig:42src/receipt_index.zig:45If any
putfails after earlier inserts, function returns with partial writes already present inby_tx. Ifby_block.putfails after loop success, allby_txentries were inserted butby_blockis missing the block, so indexes diverge. Becauseerrdeferdeinitializes cloned receipts on failure,by_txcan retain values whose owned internals were already freed.2)
LogIndex.appendBlockLogscan commit logs without committingblock_rangesrc/log_index.zig:49src/log_index.zig:52src/log_index.zig:60If
block_range.putfails, the append already happened and no rollback occurs. Range queries (from_block/to_block) then miss those committed logs, while block-hash scans can still observe them.Suggested scope
ReceiptIndex, preventby_txinsertion untilby_blockownership is guaranteed (or add explicit undo list).LogIndex, rollback appended logs whenblock_range.putfails.