Documentation Index
Fetch the complete documentation index at: https://docs.generalmarket.io/llms.txt
Use this file to discover all available pages before exploring further.
Bitmap Encoding
Bits. The smallest possible opinion. One bit per market — UP or DOWN, 1 or 0, conviction or its absence. There is no room for nuance in a single bit, which is perhaps why it works.
This page is the definitive reference for the bitmap wire format, encoding algorithm, hashing, and edge cases.
Bit Layout
Each bit represents a prediction for one market:
- 1 = UP (price will increase)
- 0 = DOWN (price will decrease or stay flat)
Bits are ordered big-endian within each byte: bit 0 is the most significant bit (MSB) of byte 0.
Byte 0 Byte 1 Byte 2
┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐
│0│1│2│3│4│5│6│7│ │8│9│A│B│C│D│E│F│ │G│H│I│J│ │ │ │ │
└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘
MSB LSB MSB LSB MSB ▲ LSB
│
Unused bits = 0
Numbers inside cells are market indices (0-indexed). Unused trailing bits in the last byte are always zero.
Bitmap Size
byte_count = ceil(market_count / 8)
| Markets | Bytes | Example |
|---|
| 1 | 1 | 1 market uses only the MSB of byte 0 |
| 7 | 1 | 7 markets use bits 0-6 of byte 0, bit 7 unused |
| 8 | 1 | 8 markets fill exactly 1 byte |
| 9 | 2 | 9 markets use byte 0 fully + MSB of byte 1 |
| 16 | 2 | 16 markets fill exactly 2 bytes |
| 100 | 13 | 100 markets use 12.5 bytes, rounded up to 13 |
Encoding Algorithm
For each market index i (0-based), if the prediction is UP, set the corresponding bit:
byte_index = i / 8 (integer division)
bit_index = 7 - (i % 8) (big-endian: MSB first)
bitmap[byte_index] |= (1 << bit_index)
def encode_bitmap(bets: list[str], market_count: int) -> bytes:
"""Encode UP/DOWN bets into a packed bitmap.
Args:
bets: List of "UP" or "DOWN" strings, one per market.
market_count: Total markets in the batch (determines byte count).
Returns:
Packed bitmap bytes (big-endian bit order within each byte).
"""
byte_count = (market_count + 7) // 8
bitmap = bytearray(byte_count)
for i in range(market_count):
if i < len(bets) and bets[i] == "UP":
byte_idx = i // 8
bit_idx = 7 - (i % 8) # big-endian: bit 0 = MSB
bitmap[byte_idx] |= 1 << bit_idx
return bytes(bitmap)
Decoding Algorithm
To read back a prediction from a packed bitmap:
byte_index = i / 8
bit_index = 7 - (i % 8)
is_up = (bitmap[byte_index] >> bit_index) & 1 == 1
def decode_bitmap(bitmap: bytes, market_count: int) -> list[str]:
"""Decode a packed bitmap into UP/DOWN bets."""
result = []
for i in range(market_count):
byte_idx = i // 8
bit_idx = 7 - (i % 8)
if byte_idx < len(bitmap) and (bitmap[byte_idx] >> bit_idx) & 1:
result.append("UP")
else:
result.append("DOWN")
return result
Hashing
The on-chain commitment is keccak256(bitmap_bytes). You publish the hash before revealing the opinion. A sealed confession — the blockchain knows you have a view but not what it is. This hash is what you pass to joinBatch() as bitmapHash, and what oracles verify when you later reveal the actual bitmap.
from web3 import Web3
bitmap = encode_bitmap(["UP", "DOWN", "UP"], market_count=3)
bitmap_hash = Web3.keccak(bitmap) # bytes32
# For hex strings:
hex_hash = "0x" + bitmap_hash.hex()
The hash function is keccak256 (Ethereum’s variant), not SHA-256. The wrong hash function produces a valid commitment to nothing — joinBatch succeeds but bitmap verification fails. A commitment that cannot be honored is worse than no commitment at all.
Verification
The seal-then-reveal scheme — first commit, then confess:
1. Bot computes: bitmapHash = keccak256(bitmap_bytes)
2. Bot calls: joinBatch(batchId, deposit, stake, bitmapHash)
→ bitmapHash stored on-chain in PlayerPosition.bitmapHash
3. Bot submits: POST /vision/bitmap { bitmap_hex, expected_hash }
4. Oracle checks: keccak256(decode_hex(bitmap_hex)) == expected_hash
5. Oracle checks: expected_hash == on-chain PlayerPosition.bitmapHash
6. Both match → bitmap accepted for tick resolution
If any verification step fails, the bitmap is rejected. The player’s stake still ticks — they lose each tick by default, punished not for being wrong but for failing to be anything at all.
Edge Cases
1 Market = 1 Byte
A single market uses only the MSB of a single byte:
Market 0 = UP → 0b10000000 → 0x80
Market 0 = DOWN → 0b00000000 → 0x00
8 Markets = 1 Byte (Exact Fit)
All 8 markets map directly to bits 0-7 of a single byte with no waste:
Markets: [UP, DOWN, UP, UP, DOWN, DOWN, UP, DOWN]
Bits: 1 0 1 1 0 0 1 0
Binary: 0b10110010
Hex: 0xB2
9 Markets = 2 Bytes
Nine markets spill into a second byte. The second byte uses only its MSB; bits 1-7 are zero:
Markets: [UP, DOWN, UP, UP, DOWN, DOWN, UP, DOWN, UP]
Byte 0: 0b10110010 = 0xB2 (markets 0-7)
Byte 1: 0b10000000 = 0x80 (market 8 in MSB, rest zero)
Full: 0xB280
Missing Bets Default to DOWN
If len(bets) < market_count, the remaining bits are left as 0 (DOWN). Silence is interpreted as pessimism. You can submit predictions for only the markets you have a view on — the rest default to doubt.
All UP / All DOWN
# All UP for 10 markets:
all_up = encode_bitmap(["UP"] * 10, 10)
# → 0xFF 0xC0 (11111111 11000000)
# All DOWN for 10 markets:
all_down = encode_bitmap(["DOWN"] * 10, 10)
# → 0x00 0x00
Worked Example
A batch with 5 markets. Predictions: [UP, DOWN, DOWN, UP, UP].
Market 0 → UP → bit 0 (MSB byte 0) → 1
Market 1 → DOWN → bit 1 → 0
Market 2 → DOWN → bit 2 → 0
Market 3 → UP → bit 3 → 1
Market 4 → UP → bit 4 → 1
Byte 0: 1 0 0 1 1 0 0 0 = 0x98
(bits 5-7 are unused, set to 0)
Bitmap: 0x98 (1 byte)
keccak256: 0x... (32 bytes)
Verify with code:
from web3 import Web3
bets = ["UP", "DOWN", "DOWN", "UP", "UP"]
bitmap = encode_bitmap(bets, 5)
assert bitmap == bytes([0x98])
assert decode_bitmap(bitmap, 5) == bets
hash = Web3.keccak(bitmap)
print(f"bitmap_hex: 0x{bitmap.hex()}")
print(f"bitmap_hash: 0x{hash.hex()}")