Skip to content

Live transport: crossbook connects to venues, and verifies against them - #1

Merged
jdardash merged 4 commits into
mainfrom
feat/live-transport
Aug 2, 2026
Merged

Live transport: crossbook connects to venues, and verifies against them#1
jdardash merged 4 commits into
mainfrom
feat/live-transport

Conversation

@jdardash

@jdardash jdardash commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Closes the one item the README listed as not built: crossbook now opens the socket itself, and the correctness claim it makes is executable rather than descriptive.

What this adds

Transport — RFC 6455 framing, the opening handshake, URL parsing and the capture format are all pure functions of bytes, unit tested and fuzzed without a network. Only the socket and TLS are platform code, quarantined in an optional crossbook_net target. The library stays header-only with zero dependencies; -DCROSSBOOK_BUILD_TOOLS=OFF drops the transport entirely and consuming crossbook::crossbook never pulls in a TLS stack.

TLS uses Schannel on Windows (ships with the OS, so the tools build on a stock machine with nothing installed) and OpenSSL elsewhere, with explicit hostname verification.

crossbook_verify — connects to Kraken with no API key, rebuilds the book, recomputes the exchange's CRC32 on every update, and reports the match rate with every divergence enumerated. Non-zero exit on any mismatch, decode failure or resync.

crossbook_capture — records raw frames verbatim with arrival timestamps. It deliberately does not decode: recording and interpreting are separate jobs, which is what makes a capture evidence rather than output.

The result

Three minutes live on BTC/USD: 2759 of 2759 checksums matched.

More useful than the number — tests/fixtures/kraken_btcusd_l2.cbcap is 72 KB of verbatim Kraken bytes, committed, replaying to 301 of 301 matches and one fixed state hash. CI runs it offline on Linux, macOS and Windows, so the README figure is a regression test rather than an anecdote.

Live verification found a real bug

The first live run reported 98.66% — 4 of 298 mismatched — with 20 bid levels held for a depth-10 subscription.

Kraken reports cancellations, so a reader handling those looks correct. It never reports that a level fell out of the top ten because a better one arrived; from the venue's side there is nothing to say. Those orphaned levels sit below the checksummed depth until removals near the touch promote one back into view, and then the checksum fails on an update that was itself fine, minutes after the real divergence.

Fixed by BasicL2Book::trim, with a negative-control test asserting the same capture still fails when trimming is disabled — so the passing test cannot start passing for the wrong reason.

No amount of testing the book against itself would have surfaced this.

Two design notes worth flagging in review

  • Split handshake and read timeouts. One value cannot serve both. An opening handshake through a busy edge takes tens of seconds — measured against Kraken, sometimes over twenty, and curl agreed — while a steady-state read wants about a second so the poll loop stays responsive. The single-timeout version failed to connect during exactly the episodes it needed to ride out.
  • Frame reader failures latch. There is no resynchronisation point in a stream of length-prefixed frames: after a bad length, the next byte read as an opcode is whatever happened to be there. Recovering would mean inventing frames.

Overlap with the decode layer

Rebased on 50146de. book.hpp and feed.hpp are touched by the trimming fix — that is the one place this branch reaches into the decode layer, and it is the bug above.

Verification

  • 196 tests pass, /W4 /WX clean on MSVC
  • 52 new test cases across framing, URL, handshake, capture, depth trimming and fixture replay
  • New fuzz_ws_frame target, in CI at 90 s — the frame reader is the only parser here fed bytes nothing we control has framed
  • Live CI job is manual-only: a green build must never depend on an exchange being reachable

jdardash added 4 commits July 31, 2026 19:44
Lands the transport the README listed as in progress: crossbook now connects
to a venue itself rather than only reconstructing books from events it is
handed.

Everything difficult is pure and testable without a network - RFC 6455 framing,
the handshake, URL parsing, the capture format - and only the socket and TLS
are platform code, quarantined in a separate optional target. The header-only
library keeps its zero dependencies; consuming crossbook does not pull any of
this in.

- ws_frame.hpp: RFC 6455 codec. Rejects reserved bits, non-minimal lengths,
  fragmented and oversized control frames, and masked server frames. Failures
  latch: there is no resynchronisation point in a length-prefixed stream.
- handshake.hpp: SHA-1 and base64 from scratch so Sec-WebSocket-Accept is
  verified rather than assumed. Checked against the RFC 6455 and FIPS 180-4
  vectors.
- transport: Schannel on Windows (ships with the OS, so the tools build on a
  stock machine), OpenSSL elsewhere with explicit hostname verification.
- capture.hpp: length-prefixed capture format, so a live measurement can be
  replayed byte-identically by someone else.
- crossbook_capture: connects to Kraken or Binance with no API key.

52 new test cases and a fuzz target for the frame reader, which is the one
parser here exposed to unframed bytes off a socket.
Found by pointing crossbook_verify at Kraken rather than by a test. The first
live run reported 98.66% - 4 of 298 updates mismatched - and the book held 20
bid levels for a subscription that asked for 10.

The depth-limited contract has a gap that is easy to miss. Kraken reports
cancellations, so a reader that handles those looks correct. It never reports
that a level fell out of the top ten because a BETTER level arrived: from the
venue's side there is nothing to say. Those orphaned levels sit below the
checksummed depth doing no harm, until enough removals near the touch promote
one back into view - and then the checksum fails, on an update that was itself
perfectly fine, minutes after the actual divergence.

- BasicL2Book::trim keeps the levels nearest the touch and drops the rest.
  Allocation-free: removals batch through a stack buffer and the loop repeats
  if one pass cannot name them all.
- Feed takes a depth and trims BEFORE verifying, since the stale level the
  checksum is about to catch is exactly the one trimming removes.
- FeedStats::levels_trimmed, so a depth that was never configured is visible
  rather than silent.

Verified on both book implementations, including prices spread wide enough to
push levels into the array book's overflow map.
Makes the README's central claim executable and checkable by someone who is not
me. crossbook_verify connects to Kraken with no API key, rebuilds the book,
recomputes the exchange's CRC32 over local state on every update, and reports
the match rate with every divergence enumerated. Exit status is non-zero on any
mismatch, decode failure, or resync - a verifier that reports problems and then
exits successfully is one nobody will wire into anything.

Measured over three minutes on BTC/USD: 2759 of 2759 checksums matched.

The part that matters more than the number: tests/fixtures/kraken_btcusd_l2.cbcap
is 72 KB of verbatim Kraken bytes, committed, and replays to 301 of 301 matches
and one fixed state hash on every platform. CI runs it offline on Linux, macOS
and Windows, so the number is a regression test rather than an anecdote. This is
only possible because crypto market data can be redistributed; the equities
equivalent cannot, which is why public ITCH projects ship without runnable data.

Also here:

- Instrument scales and subscription depth are read off the venue's own
  spelling in the snapshot, so a capture is self-describing and a replay needs
  no arguments beyond the file. A wrong scale fails every checksum, and a
  hard-coded table rots silently when a venue changes precision.
- Split handshake and read timeouts. One value cannot serve both: an opening
  handshake through a busy edge takes tens of seconds - measured against
  Kraken, sometimes over twenty, and curl agrees - while a steady-state read
  wants about a second so the poll loop stays responsive. The single-timeout
  version failed to connect during exactly the episodes it needed to ride out.
- Connect retry with exponential backoff, because reconnecting in a tight loop
  is what earns a throttle in the first place.
- The live CI job is manual-only. A green build must never depend on an
  exchange being reachable.
Three POSIX-only failures, none of which MSVC can see.

- socklen_t is unsigned on POSIX and a signed int on Winsock, so every
  setsockopt and connect length was a sign conversion under -Werror. Named the
  difference as a `SockLen` alias rather than sprinkling casts that happen to be
  right on one platform.
- GCC 13 rejected `clear()` then `resize()` on the send buffer under
  -Wstringop-overflow, unable to prove the frame header had room. The header now
  goes into a fixed std::array whose bound is in the type. Better code for the
  same reason the warning fired.
- Two includes MSVC supplies transitively and libstdc++ does not.
@jdardash
jdardash merged commit da9e572 into main Aug 2, 2026
14 checks passed
@jdardash
jdardash deleted the feat/live-transport branch August 2, 2026 23:31
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