Skip to content

Validate child instrument IDs in OrderBookDeltas - #4710

Open
folknor wants to merge 1 commit into
nautechsystems:developfrom
folknor:fix-orderbook-deltas-child-instrument
Open

Validate child instrument IDs in OrderBookDeltas#4710
folknor wants to merge 1 commit into
nautechsystems:developfrom
folknor:fix-orderbook-deltas-child-instrument

Conversation

@folknor

@folknor folknor commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

OrderBookDeltas pairs a wrapper instrument_id with children that each carry their own, and nothing requires the two to agree.

Children are never checked against the wrapper

OrderBookDeltas::new_checked validates only that the vector is non-empty, then takes flags, sequence, ts_event and ts_init from the last child. OrderBook::apply_deltas compares the wrapper against the book and delegates to apply_deltas_unchecked, which applies each child through apply_delta_unchecked, so no child's instrument ID is compared to anything. The singular apply_delta does check its delta's ID before delegating; the batch path has no equivalent.

new_checked now rejects a batch whose children disagree with the wrapper. The comparison is pointer-first with a string-value fallback, because InstrumentId equality is Ustr pointer equality and that is the reason apply_delta_unchecked exists at all: the fallback compares symbol and venue as strings, so identifiers interned in different pools compare equal when their values match. Ordinary in-process data takes the pointer path and never evaluates the fallback.

No in-tree caller is affected, since every built-in assembler resolves one instrument and uses it for the wrapper and each child.

The check is not repeated in apply_deltas. A batch is constructed once and may be applied by several consumers, and application is the hot path. Construction is also not a complete boundary: the struct's fields are public, the Cap'n Proto and SBE decoders build it literally, and derived Deserialize bypasses it. This closes the ordinary construction route rather than making the type invariant-preserving.

Testing

test_order_book_deltas_new_checked_rejects_mismatched_instrument covers a mismatched first child and a mismatched later child, so the whole vector is examined rather than its head; a homogeneous multi-child case is the control. Both rejection cases fail against the unfixed constructor.

test_apply_deltas_does_not_validate_children_constructed_literally builds the struct literally to bypass the constructor and pins what remains: apply_deltas returns Ok with the foreign child's level resting in the book, and apply_deltas_unchecked still applies one. It records the boundary rather than asserting a rollback that does not occur.

Two existing tests met the new contract in opposite ways. test_order_book_deltas_single_delta paired a BTCUSD.CRYPTO wrapper with a EURUSD.SIM fixture child while asserting only metadata propagation, and now takes its wrapper from the child. The SBE round-trip fixture test_order_book_deltas_preserve_delta_instrument_ids is heterogeneous on purpose, since it exists to prove each child's ID is encoded independently, so it constructs its value literally and preserves that coverage; the encoded input is unchanged.

`OrderBookDeltas` carries a wrapper `instrument_id` while each child
`OrderBookDelta` carries its own, and nothing required the two to agree.
`new_checked` validated only that the vector was non-empty, and
`apply_deltas` compared the wrapper against the book before delegating
to `apply_deltas_unchecked`, which applies every child through
`apply_delta_unchecked`, so no child ID was compared to anything. The
singular `apply_delta` does check its delta's ID; the batch path had no
equivalent.

Reject a mismatched batch in `new_checked`. The comparison is
pointer-first with a string-value fallback: `InstrumentId` equality is
`Ustr` pointer equality, which is the reason `apply_delta_unchecked`
exists, so the fallback compares `symbol` and `venue` as strings and
accepts identifiers interned in different pools. Ordinary in-process
data takes the pointer path.

The check is not repeated in `apply_deltas`, where a batch built once
may be applied by several consumers. Public fields and the binary
decoders still bypass construction; this closes the ordinary route.

Coded by an LLM.
@folknor
folknor requested a review from cjdsellers August 10, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant