Reverse-chronological log of user-visible changes. Entries cover JSON-RPC, REST, WebSocket subscriptions, docker/config, and indexer behavior. Internal refactors and test additions are summarized only when they affect runtime behavior.
- New config key
allow_peer_discovery(bool) controls DNS peer discovery/rotation. Absent:truewhenp2p_nodeis empty,falseotherwise (unchanged behavior). Modes: static (false+p2p_node), dynamic (true, nop2p_node), hybrid (true+p2p_node).falsewithoutp2p_nodeis a config error.
- Fix stuck issue when seamless transition
- Clamping number of requested log events on bob
END_EPOCH events now reach WebSocket subscribers automatically. The end-epoch log batch lives on a virtual tick (lastQuorumTick+1) that never gets quorum tick data or votes; previously it was silently dropped on every live path.
- Live
logs/transferssubscriptions: verified logs on ticks without tick data (the END_EPOCH virtual tick, quorum-empty ticks with logs) are now delivered instead of dropped. Theirtimestampis backfilled from the previous tick (matching the REST/epochLogsconvention) instead of0. - Live
tickStreamsubscriptions: the indexer now pushes the virtual end-epoch tick's log batch (zeroed tick data,hasNoTickData: true, realtimestamp) before exiting at epoch end. The batch carries the ending epoch's number; the same tick number later re-appears as the next epoch's init tick with the new epoch's number — clients deduplicating across the boundary should key on(epoch, tick), nottickalone. tickStreamcatch-up: recovered end-epoch logs (backup keys) are now actually merged into the boundary tick's message (previously collected but never emitted), deduplicated against live keys, withtimestampbackfilled. Catch-up during the 30-minute end-epoch serving window no longer hangs waiting on the exited indexer and now covers the virtual end-epoch tick.logs/transferscatch-up: END_EPOCH events crossing an epoch boundary now resolvetxHashtoSC_END_EPOCH_TX_<tick>via the backed-up end-epoch log ranges instead of misattributing them to an unrelated transaction of the reused tick number.
Improve log accessing.
- Fix bug when accessing END_EPOCH events from previous epoch.
- Fix several memory bugs
- Update fourq verify
- Faster catch-up:
future-offsetdefault raised to 10. The docker image previously shipped3, which throttled sync to ~3 ticks of look-ahead pipelining; it is now consistent across the code default, docker/bob.json, default_config_bob.json, and docs. Raise viaFUTURE_OFFSETto accelerate large initial syncs. - Configurable log-event chunk size:
LOG_EVENT_CHUNK_SIZE/log-event-chunk-sizecontrols how many log IDs are requested perRequestLog(default and hard ceiling 999). - Log-range integrity (misalignment fix):
db_insert_log_rangenow writes the range blob and its summary atomically (SETNX blob + conditional summary), and corrupt/garbage summaries are rejected and refetched. This eliminates the non-deterministic state-digest misalignment crashes / restart loops caused by inconsistent (start, length) reads across restarts. - Connection resilience: an always-on peer watchdog disconnects peers idle for 30s; bootstrap and rescue waits are now bounded so an unresponsive peer can't stall startup indefinitely; the redundant
EXCHANGE_PUBLIC_PEERShandshake was dropped from the bootstrap sequence. - Diagnostics: per-cycle request/response traffic counters and a "Blocked-on" snapshot in the periodic state line; an optional
DIAGNOSTIC_MODEfor deeper per-tick / per-log auditing; per-log source attribution to investigate "wrong tick" reports. - New tool:
tools/bob_probe— a standalone CLI to probe a BM node's handshake / tick-info / log-range / log-event responses for connectivity debugging.
- Database validation: Added validation rules to discard garbage log range data (prevents corrupted data storage)
- Bug fix: Fixed log range validation to correctly handle non-contiguous log slots
- Logging: Added detailed warning logs for database validation failures to aid debugging
Default tx_tick_to_live lowered to 1000 (was 3000) in both
docker/bob.json and the documented example in
README.md. With the post-cutover 4096-tx-per-tick ceiling
the previous 3000-tick window kept a lot of transaction:* blobs in
KeyDB; 1000 keeps roughly 30 minutes of recent tx data resident and
migrates the rest to kvrocks faster, easing memory pressure on
guardian hosts.
Operators that rely on instant lookups of older recent txs from KeyDB
can revert to the previous value via TX_TICK_TO_LIVE=3000.
Config key normalization — bob.json keys are now lowercased and - is converted to _ before parsing, so mixed naming styles (e.g. log-level, Log_Level) all resolve to the same canonical key.
Enhanced computor verification — Added TARGET_TICK_VOTE_SIGNATURE, computorListSignature, and arbitrator signature to status/computor endpoints for fast signature verification.
TickStream catch-up: epoch-boundary jump
Previously, when catch-up spanned the numerical gap between one epoch's
last tick and the next epoch's initTick, bob iterated every tick in
that gap and emitted a placeholder for each — potentially hundreds of
thousands of empty messages over a slow WebSocket.
Catch-up now detects the boundary via the persisted end_epoch_tick:<e>
and init_tick:<e+1> keys, emits one synthetic event:
{
"type": "epochBoundary",
"fromEpoch": 214,
"toEpoch": 215,
"lastEpochEndTick": 52500000,
"newEpochInitTick": 52800000,
"skippedTicks": 299999
}…and jumps the cursor directly to the next epoch's initTick. The
sub.lastTick is updated so a reconnect-during-catch-up doesn't replay
the gap.
If the cutover is contiguous (no numerical gap), no boundary event is emitted — the existing tick events carry the transition naturally (including the SC_END_EPOCH log merge from 1.5.3).
Multiple epoch transitions in a single catch-up range are handled iteratively — each gap fires its own boundary event.
End-epoch logs are still delivered before the jump
The catch-up loop still visits the prior epoch's endTick first and
emits its tick event including the merged SC_END_EPOCH log batch. Only
the empty numerical range after that endTick is jumped.
The 1.5.3 end-epoch-log merge looked up end_epoch_tick:<td.epoch - 1>,
which was based on a stale assumption that the new epoch's initTick
could equal the prior epoch's endTick. In practice an epoch's endTick
is always strictly less than the next epoch's initTick, so at the
endTick td.epoch is always the ending epoch — the previous logic
looked one epoch too far back and missed the merge. The lookup now
checks both td.epoch and td.epoch - 1 against end_epoch_tick:<e>
so the SC_END_EPOCH batch is delivered correctly.
Special-event log delivery fixes for TickStream catch-up
Two bugs caused SC_INITIALIZE_TX, SC_BEGIN_EPOCH_TX, and SC_END_EPOCH_TX
log events to sometimes be missing for clients using TickStream catch-up:
-
SC_END_EPOCHevents were unreachable via catch-up. At each epoch transition the verifier renamestick_log_range:<endTick>andlog_ranges:<endTick>intobackup_end_epoch:*keys so the new epoch can reuse the same tick number. The standarddb_get_logs_by_tick_rangeonly looks at the canonical key, so catch-up returned nothing for the end-tick of any past epoch. Live broadcast was fine; only clients that reconnected and asked for catch-up across an epoch boundary missed these events.Fixed by checking the end-epoch backup keys in
performCatchUpwhen a tick matches the previous epoch'send_epoch_tick:<epoch>value, and merging those log events into the tick's stream with their original slot index (typicallySC_END_EPOCH_TX). -
Race at the start of a new epoch. A client subscribing right after bob restarted for a new epoch could race past
initTickbefore the indexer had populatedtick_log_range:<initTick>, missing theSC_INITIALIZE_TXandSC_BEGIN_EPOCH_TXlog events. Fixed by waiting forgCurrentIndexingTickto advance past each tick before reading its logs. The wait is a no-op when catch-up is far behind the cursor; it only blocks at the leading edge.
Also fixed: db_get_endepoch_log_range_info now accepts both the
canonical 4096-slot and the legacy 1024-slot LogRangesPerTxInTick layout,
so end-epoch metadata archived before the 4096-tx-per-tick cutover remains
queryable.
Configurable external service URLs + peer-discovery failover
All outbound HTTP URLs are now configurable via bob.json keys / docker
env vars. Defaults remain the public *.qubic.global / qubic.li
endpoints, so existing deployments work unchanged.
New env vars (see docs/DOCKER_ENV.md for full reference):
| Env var | Effect |
|---|---|
PEER_DISCOVERY_URLS |
Comma-separated list of base URLs that serve /random-peers. Bob tries each in order until one returns peers (new failover behavior). |
CURRENT_TICK_ENDPOINTS |
Semicolon-separated url|path|shape triples for the network's current-tick lookup. shape is flat or nested. |
STATE_FILES_URLS |
Comma-separated list of URL templates (failover order) for per-epoch state snapshot downloads. Each entry may include {EPOCH} which is substituted at download time (so mirrors with different layouts work: https://dl.qubic.global/ep{EPOCH}.zip vs https://storage.example.com/{EPOCH}/ep{EPOCH}.zip). Entries without {EPOCH} fall back to <base>/ep<epoch>.zip (back-compat). STATE_FILES_URL (singular) still accepted. |
CHECKIN_URL |
Base URL for the /checkin POST. Empty to disable. |
Refactor: all the previously hard-coded https://api.qubic.global,
https://api.qubic.li, https://rpc.qubic.org, https://dl.qubic.global
literals in connection/NodeIntroducer.cpp
are now driven by runtime config.
Peer-discovery failover (new behavior) — previously bob queried only
api.qubic.global for peer discovery and returned an empty list if it
failed. With PEER_DISCOVERY_URLS listing multiple base URLs, bob walks
the list in order and uses the first non-empty response — same failover
pattern that the current-tick lookup has used for some time.
Path-prefix handling fix — drogon's HttpClient::newHttpClient
silently drops the path portion of its base URL, so a configured
endpoint like https://api.qubic.li/public would hit /random-peers
on the wrong host path. A new splitOriginAndPath() helper now extracts
the prefix and prepends it to the request path, so prefix-qualified URLs
work as expected across peer-discovery, current-tick, and check-in calls.
- Fixed a three-way deadlock between the tick fetcher, indexer, and log verifier that could freeze synchronization indefinitely when catching up from far behind.
Required for the epoch 214 cutover (2026-05-20) — core raises max transactions per tick from 1024 → 4096. Bob must run this version (or later) before the epoch boundary.
Also contains a wire-incompatible change to the
balance/ asset-balance fields. See "Migration" below.
Canonical layout bumped to 4096
NUMBER_OF_TRANSACTIONS_PER_TICKis now4096in common/defines.h.LOG_TX_PER_TICKauto-scales (4096 + 6 specials).- Per-tick signature buffers, indexer loops, request bit-flags, and
on-wire
TickDatasize auto-scale.
Backwards compatibility (read-only)
- New
LegacyTickData,LegacyLogRangesPerTxInTick, andLegacyFullTickStructstructs in common/structs.h describe the pre-epoch-214 wire/storage layout. db_get_tick_data,db_get_vtick_from_kvrocks,_db_get_log_ranges_by_key,db_get_cLogRange_from_kvrocksall branch on the stored blob's byte length and upcast the legacy layout in memory. Historical ticks remain queryable via REST/RPC after the upgrade.processTickDataaccepts both canonical and legacy wire packets, verifying each over the byte range matching its original signature.- All writes are in the canonical 4096-slot layout. The legacy structs are read-only.
Behavior intentionally limited
replyTickDatarefuses to serve ticks from epochs< 214. We only hold them upcasted in memory; replying with the canonical layout would fail signature verification on the peer side. Pre-cutover peers must re-sync legacy ticks from each other or core's archive.
Constants cleanup
- Fixed a latent bug:
TickData::contractFeeswas declared ascontractFees[NUMBER_OF_TRANSACTIONS_PER_TICK]. It is now correctly sized byMAX_NUMBER_OF_CONTRACTS. Both used to be 1024 so the wire layout happened to match; bumpingNUMBER_OF_TRANSACTIONS_PER_TICKto 4096 without this fix would have broken signature verification. MAX_NUMBER_OF_CONTRACTSandNUMBER_OF_TRANSACTIONS_PER_TICKare now defined exactly once (indefines.h); the duplicate "placeholder" defines indatabase/db.hare removed.contractFeesJSON output loops inRESTAPI/QubicRpcMapper.cppandRESTAPI/bobAPI.cppnow iterateMAX_NUMBER_OF_CONTRACTSinstead of a literal1024.
Storage impact
tick_data:<tick>in keydb roughly 4× per tick (mainly the digest array). Historical pre-cutover blobs remain at the legacy size; only new ticks pay the cost.vtick:<tick>in kvrocks: nominally 4× but zstd compresses sparse digest arrays well; realistic ~2×.- The bump only raises the ceiling. Actual storage grows with real tx throughput.
Deployment
- Deploy
1.5.0before the first tick of epoch 214. - A bob running an older binary across the cutover will signature-fail
every
TickDatapacket for epoch 214+, silently halting sync.
RPC/REST parity fixes (audit pass)
Bugs:
qubic_getLogsandqubic_getTransfersno longer hardcodeepoch = gCurrentProcessingEpoch. They derive the epoch fromfromTick(or accept an optionalepochfilter). Historical tick ranges now return their real logs instead of empty arrays.qubic_getLogsaccepts both numeric and stringfromTick/toTick. Previously a numeric form ({"fromTick": 123}) threw an internal error.
Parameter / shape parity:
qubic_getAssetTransfersandqubic_getAllAssetTransfersnow accept eitherissuerorassetIssuer(the latter is what the REST endpoints use). Old payloads keep working; REST payloads now work unchanged on RPC.qubic_statusaccepts an optionalchallengeparameter (string or{"challenge": "..."}object form). Mirrors the REST/status?challenge=...feature so callers can verify operator identity.qubic_getTickByNumberresponse now includesvotes[],contractFees,logIdStart,logIdEnd— matching the REST/tick/{n}shape. Existing fields unchanged; this is additive.- REST
/findLognow normalizes topic strings, accepting 0x-prefixed hex, 60-char uppercase identity, or already-lowercase identity. Previously only the third form worked; same input that succeeded onqubic_findLogIdsfailed on REST. bobGetBalanceREST keys: fixed typos"currentBobTick:"→"currentBobTick"and"error:"→"error". Clients keyed on the typo'd names must update.
Wire-incompatible:
-
Numeric fields that were previously emitted as JSON strings on the RPC surface are now emitted as JSON numbers. Brings RPC into line with REST (REST already used numbers via
LogEvent::parseToJson). Affected fields:qubic_getBalance:balance,incomingAmount,outgoingAmountqubic_getAssetBalance:ownershipBalance,possessionBalancequbic_getLogs/qubic_getTransferslog entries:amount(QU_TRANSFER, BURNING),numberOfShares(ASSET_OWNERSHIP_CHANGE, ASSET_POSSESSION_CHANGE)
Strict-typed clients that decoded these as strings must update.
Internal — additive on the wire
- Topic / identity normalization for log-search APIs is now centralized in
ApiHelpers::normalizeTopicIdentity. RPCqubic_getTransfers,qubic_findLogIds, and REST/findLog+/getlogcustomall accept the same input shapes (60-char Qubic identity in either case, 64-char hex, 0x-prefixed hex). Previously REST/findLogrejected 0x-hex and/getlogcustomaccepted only A-Z text. qubic_getLogs(logEventToQubicLog) now delegates body extraction toLogEvent::parseToJson, so new log types automatically surface on RPC without a parallel switch update. Existing fields are unchanged; the refactor adds previously-omitted fields for BURNING (contractIndexBurnedFor) and CONTRACT_* messages (content), and newly emits bodies for log types that weren't covered before (ASSET_ISSUANCE,ORACLE_QUERY_STATUS_CHANGE, etc.).
Migration
- JSON decoders that read
balance,incomingAmount,outgoingAmount,ownershipBalance,possessionBalance,amount, ornumberOfSharesas strings should be updated to read them as integers. - If you parsed REST
/balance/{id}keyscurrentBobTick:orerror:, drop the trailing colons.
Asset RPC
qubic_getAssetBalancenow accepts an optionalmanageSCIndex(4th positional parameter, or named field in an object-formparams). Required for assets managed by a contract other than the issuer — e.g. QDOGE / QX-managed shares usemanageSCIndex=1. Previously the RPC hard-coded0, producingownershipBalance: "-1", possessionBalance: "-1"for these assets.- Response now echoes back the
manageSCIndexthat was queried. - Docs and the RPC playground updated; the REST endpoint
/asset/{identity}/{issuer}/{name}/{manageSCIndex}was already correct.
Oracle
- New
PERSIST_ORACLE_TXenv-var /persist-oracle-txconfig flag for the docker image. Enables persisting the raw oracle reply transactions (destinationPublicKey=0,inputType ∈ {6,7,10}) so they can be inspected via the standard tx endpoints. Disabled by default.
End-of-epoch reliability
- The end-of-epoch shutdown busy-loop in
bob.cppwas pegging a CPU and could hang indefinitely if a single data thread was stuck in a deep DB call (e.g. during heavy kvrocks compaction). It now sleeps 50ms between wake bursts and force-exits withstd::_Exit(0)after a 60-second timeout so the supervisor restarts bob for the new epoch instead of leaving the REST/WS server alive without sync progress. - Catch-up on
TickStreamsubscriptions no longer silently drops ticks whosetick_data:row is missing (typical for the empty first tick of an epoch or for ticks evicted underlastNTickstorage). Such ticks are emitted as placeholders withhasNoTickData: trueandisSkipped: true. verifyLoggingEventbroadcasts the end-of-epoch log batch to subscribed WebSocket clients before the 30-minute grace period.
Docker / kvrocks
KVROCKS_TTLenv-var documented and respected by the entrypoint — controls how many epochs bob retains. Default 14 days (~2 epochs).docker/kvrocks.confcompression switched fromsnappytozstd. Roughly halves on-disk size for the kind of data bob stores. New SSTs use zstd immediately; existing snappy SSTs convert as compactions touch them (or runredis-cli -p 6666 COMPACTfor a one-shot migration).
REST
- New
bobGetEndEpochLogendpoint to fetch the log range belonging to the virtual end-epoch tick. Useful for reconciling dividend distributions and end-of-epoch protocol reports.
⚠️ Breaking: transaction status is now tri-state.Previously:
executed: true|false,status: "success"|"failed". Now: a third state,pending, is emitted when the tx's tick has not yet been log-verified. In that caseexecutedis JSONnullandstatusis"pending". Strict-typed clients must accept nullableexecutedand the new status value.
Affected surfaces
- JSON-RPC
qubic_getTransactionReceipt - JSON-RPC
qubic_getTickByNumber(transactions embedded in tick responses now also carryexecutedandstatus) - REST
bobGetTransaction - WebSocket
TickStreamdeliveries (real-time and catch-up)
Behavior changes that prompted the new state
- Transaction-status endpoints now compute execution state directly from
TickData+ log ranges + logs, not from theitx:index. Consequence: receipts are correct regardless of indexer state or spam-filter settings, but they need a way to express "we don't know yet for this tick" instead of returning a wrong "failed". spam-qu-thresholddefault flipped from100→0. Dust QU transfers are indexed like any other tx; receipts reflect their actual execution state. Estimated extra kvrocks disk for a 150M-tx-per-epoch workload: ~0.5–1 GB.- Subscription/streaming paths share the same execution rule as the receipt
endpoint via
ApiHelpers::isTxExecuted.
Migration guidance
- Treat
statusas"success" | "failed" | "pending". - Treat
executedas nullable (bool | null). - For receipts, retry with backoff while
status == "pending". - For
TickStream,pendingis terminal for that delivery — the tick will re-emit after log verification completes.
Other changes
/_admin/checkTransactionsaudit endpoint unchanged — still probes theitx:index intentionally.
TickStreamsubscription paths now computeexecutedfrom the already-loaded log range / tick logs instead of falling back todb_get_indexed_tx. Removes the last non-test caller of the indexer's per-tx lookup for streaming.- Internal refactor of REST API doc comments.
SC_NOTIFICATION_TXconstant renamed (underscore fix).bobGetLogperformance improved.bob.jsonships with un-mapped tx logging enabled for easier debugging.
- New direct-lookup path for single-tx responses (
qubic_getTransactionReceipt, RESTbobGetTransaction). Returns correctexecuted/statusregardless of whether theitx:index has the row — fixes the long-standing "low-value tx reported as failed" bug. spam-qu-thresholddefault lowered from100→0indefault_config_bob.json,docker/bob.json, and the docs. The threshold was originally a RAM-saving measure when indexer data lived in keydb; now that it lives in kvrocks, the cost is ~3% extra disk per epoch and worth the correctness.- Large internal cleanup: unit test coverage for
db_*, asset, log-core, and processor modules. Refactor ofcommon/,processors/, and asset handling. No external behavior change beyond the items above.
- New
WAIT_AT_EPOCH_ENDenv var (docker) — controls the post-END_EPOCH grace period during which bob keeps serving slower peers before exiting. - RPC tick lookup checks the new
db_is_tick_emptyflag so callers can distinguish empty/skipped ticks from unknown ticks.
Earlier 1.3.x releases were largely incremental: configuration knobs,
documentation, bugfixes for tick storage and recovery. See git log v1.3.0..v1.3.18.
- Major (
x.0.0) — wire-incompatible change to RPC/REST/WS schemas. Clients must adapt. Examples: 1.4.0's tri-state status. - Minor (
1.x.0) — new endpoints/fields, new config knobs, behavior expansions that are additive on the wire. - Patch (
1.4.x) — bugfixes, performance, operational improvements (compression, retention defaults, etc.). No client changes required.