Skip to content

Receipt/log index mutation is not failure-atomic and can corrupt state on allocation errors #53

Description

@roninjin10

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

  1. Make both mutation paths transactional:
    • stage data first,
    • commit both indexes only after all allocations succeed,
    • rollback partial writes on failure.
  2. In ReceiptIndex, prevent by_tx insertion until by_block ownership is guaranteed (or add explicit undo list).
  3. In LogIndex, rollback appended logs when block_range.put fails.
  4. Add fail-allocator tests for both modules to assert no partial state and no leaks/double-frees on simulated OOM.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions