Moonbase Docs

Orderbook Checksum

Verify local orderbook state with CRC32 checksums

Problem

When maintaining a local orderbook via WebSocket, state drift can occur due to:

  • Missed messages (network issues)
  • Out-of-order message processing
  • Bugs in diff application logic

Without detection, the client trades on stale/incorrect data.

Solution

The server includes a CRC32 checksum in every book message. Clients verify their local orderbook state matches the server's expected state.

Message Format

{
  "channel": "book",
  "product": "BTC-VND",
  "type": "update",
  "data": {
    "bids": [["2275689000", "2.249857"]],
    "asks": [],
    "timestamp": "1764643130040025000",
    "gsn": 2309207234
  },
  "checksum": 2734336218,
  "timestamp": "1764643130040025000",
  "gsn": 2309207234
}
FieldDescription
type"snapshot" = full book (replace local state), "update" = diff (apply changes)
data.bids/asksPrice levels. For updates: size=0 means remove level
checksumCRC32 of full book state (not the diff)
timestampNanosecond unix timestamp
gsnGlobal sequence number

Checksum Algorithm

CRC32-IEEE on price:size pairs, alternating bids/asks:

Format: "bid_price:bid_size:ask_price:ask_size:bid_price:bid_size:..."

Rules

  1. Bids sorted high→low, Asks sorted low→high
  2. Interleave: bid[0], ask[0], bid[1], ask[1], ...
  3. Use raw decimal strings (e.g., "2226787000", not scientific notation)
  4. No trailing colon

Example

Bids: [["9", "2"]]
Asks: [["10", "1"]]

Preimage: "9:2:10:1"
Checksum: CRC32("9:2:10:1") = 1226559413

Client Implementation (Go)

import (
    "hash/crc32"
    "strings"
)

type Book struct {
    Bids [][2]string // [price, size], sorted high→low
    Asks [][2]string // [price, size], sorted low→high
}

func (b Book) Checksum() uint32 {
    var sb strings.Builder
    maxLen := max(len(b.Bids), len(b.Asks))
    for i := 0; i < maxLen; i++ {
        if i < len(b.Bids) {
            sb.WriteString(b.Bids[i][0] + ":" + b.Bids[i][1] + ":")
        }
        if i < len(b.Asks) {
            sb.WriteString(b.Asks[i][0] + ":" + b.Asks[i][1] + ":")
        }
    }
    s := sb.String()
    if len(s) > 0 {
        s = s[:len(s)-1] // trim trailing colon
    }
    return crc32.ChecksumIEEE([]byte(s))
}

Client Implementation (TypeScript)

import { crc32 } from 'crc';

interface Book {
  bids: [string, string][]; // [price, size], sorted high→low
  asks: [string, string][]; // [price, size], sorted low→high
}

function checksum(book: Book): number {
  const parts: string[] = [];
  const maxLen = Math.max(book.bids.length, book.asks.length);

  for (let i = 0; i < maxLen; i++) {
    if (i < book.bids.length) {
      parts.push(`${book.bids[i][0]}:${book.bids[i][1]}`);
    }
    if (i < book.asks.length) {
      parts.push(`${book.asks[i][0]}:${book.asks[i][1]}`);
    }
  }

  return crc32(parts.join(':')) >>> 0; // unsigned 32-bit
}

Handling Mismatch

On checksum mismatch:

  1. Resubscribe - Unsubscribe and resubscribe to get fresh snapshot
  2. Log/Alert - Track mismatch frequency for debugging
  3. Don't trade - Pause trading until state is verified
// Example: resubscribe on mismatch
manager.SetOnChecksumMismatch(func() {
    log.Error("checksum mismatch - resubscribing")
    ws.Unsubscribe("book", productID)
    ws.Subscribe("book", productID)
})

Never trade on unverified orderbook data. Always validate checksums before using local state for order decisions.