Sync and history
How a node gets the chain from other nodes without trusting them, how it joins from a state snapshot, what it drops as history expires, and how history moves between nodes as era files. This is a reference for node operators and auditors, down to what each step verifies and what each failure message means. Log lines and sizes here come from chains run with the 0.2.3 binaries, some with the hidden test flag --era-length 64 so that eras complete quickly.
Trust model
A node starts from its genesis file and trusts nothing else it reads over HTTP. Sources are the nodes named with --follow URL, and any number of them may lie.
What a follower relies on:
- The genesis file it was initialized with.
- At most f of the n = 3f + 1 validators of each epoch being faulty. A fresh node accepts any chain of epochs that verifies from the genesis validators.
- One reachable source that answers. Sources are not compared or voted on: a pass uses one source, and the next pass uses another only if this one failed.
| A source can | What happens |
|---|---|
| Serve a wrong block or state | It fails a hash link, a proof, a range proof or re-execution, and nothing is stored |
| Serve another chain | {source} follows a different chain, and the node moves to the next source |
| Withhold data or stop answering | The pass fails, and the node moves to the next source |
| Report a height it can't back | /v1/status is not authenticated, so the node may try for nothing; see the caveat under keeping up |
A source cannot make a node store a block that has no valid proof, or state that doesn't hash to a certified root.
The follow loop
Follower holds the node, the source list, a next_source counter and an HTTP agent with a 60 s timeout per request. Replies are capped at MAX_REPLY_BYTES, 64 MiB. One pass is Follower::step, which adds 1 to next_source if the pass fails. The steps of a pass:
GET /v1/statusfromsources[next_source % len]. Thegenesis_hashmust match this node's.- If the source's height is not above the head, stop: nothing to do.
- If the head is 0, and the source is more than
--snapshot-if-behindblocks ahead or itsblocks_fromis above 1, try a snapshot. If one was taken, the pass ends there. If the source offers none, go on. - If the source's
blocks_fromis abovehead + 1, fail: it no longer has the next block. - Set
endto the lower of the source's height and the next multiple of 64 above the head. GET /v1/blocks/{h}/rawfor each height fromhead + 1toend, with?prove=trueon the last./v1/blocks/{h}/rawincludes a proof if one is kept for that height, andprove=truebuilds one when it isn't.- Run
apply_provenon the run of blocks.
A pass covers at most 64 blocks, and while the source is far ahead it ends on a height that has a kept proof.
Who drives it:
| Caller | Runs | Cadence |
|---|---|---|
API node, Follower::spawn | thread follower | passes back to back while blocks arrive, otherwise every 500 ms |
| Validator at startup | catch_up(2, 3600 s) | until within 2 blocks of the source; 30 s without an answer ends it |
| Running validator | Validator::follow | see keeping up |
| Standby key | thread standby | one pass every 100 ms |
catch_up asks for the height, passes if behind, and sleeps 200 ms when a pass applied nothing. It returns an error at its timeout or after UNREACHABLE_AFTER, 30 s, of silence. At startup that error logs could not catch up; joining consensus anyway, and the validator goes on.
Verifying blocks
apply_proven(node, parent, parent_height, blocks, proofs) takes blocks in height order and applies only those it can prove. It reads the set and chain of the head's epoch (current_view) and calls committed_prefix, which for each block in turn does these checks:
- The height is
parent_height + 1 + i, or the run fails:expected height {h}, got {h'}. block.parentis the id of the block before, or the run fails:block {h} does not extend the previous block.- If a proof came with the block,
proof.tipmust be the block's id andproof.verify(block.round, validators, chain)must pass. A proof that passes moves the proven prefix up to this block. A proof that fails is cleared and is no error. - If the block changes the validators, it must have its own good proof, because the next block is signed by others. Without one, the walk stops here. With one, the view switches to the new set, with
chainset to this block's id.
A block counts as proven if it, or anything linked above it in the run, has a good proof. Blocks past the last good proof are dropped, and a later pass fetches them again. See finality proofs for why one proof covers its ancestors.
Then, for each block of the proven prefix, in order:
check_tx_root, or the pass fails.execute_block_cachedruns every transaction with the real rules. Blocks contain only valid transactions, so any failure fails the pass:block {h}: ....write_committedprepares the tree, comparesstate_rootandreceipt_root, and commits in one synced batch. See writing state.
Every block is re-executed, so a certificate alone never changes a follower's state: a block with valid signatures from a quorum and an invalid transaction stops at step 6.
The ConsensusTip written with each block gets the certificate of the next proof at or above it in the run. /v1/blocks/{h}/raw returns { height, block, finality }, where block is the hex of borsh ChainBlock, and finality the hex of a borsh FinalityProof or null.
Snapshots
A fresh node far behind takes the state at a recent certified height instead of replaying every block. Below that height it holds no blocks and no state.
What a server offers
Node::snapshot_height:
regular = head - head % 64, the newest multiple ofPROOF_EVERY.epoch_startis the first height of the current epoch, andcandidateis the higher of the two.- There is no snapshot if
candidateis 0, is belowpruned_before, or has no kept proof.
GET /v1/state/snapshot answers with that height, or 503 and no snapshot to serve:
pub struct SnapshotResponse {
pub height: u64,
pub state_root: Hash,
pub block: String, // hex of borsh ChainBlock
pub finality: String, // hex of borsh FinalityProof
}
A node pruning fewer than 64 versions could fall below the candidate and offer nothing, but --prune-keep refuses values below 100, so a pruned node on the command line always has one.
Walking the epochs
The pivot block is signed by the validators of its epoch, and the only set a fresh node knows is genesis. So it walks every validator change from there.
GET /v1/state/snapshot. Treat 404 and 503 as "no snapshot".GET /v1/epochs, which lists every epoch as{ epoch, start, start_height }.- For each epoch above 0,
GET /v1/blocks/{start_height}/raw. A reply without a proof fails:an epoch's first block comes without a proof. - The pivot's height and root must match the summary, or
the snapshot block does not match its summary. - Run
checked_pivot.
epoch_views builds the chain of sets. It starts with the genesis validators and chain set to the genesis hash. For each epoch i + 1, in order:
- The summary must match the block:
epochisi + 1,startis the block's id,start_heightis its height, and that height is above the previous epoch's. - The block must contain a
SetValidators, have a goodtx_root, andproof.tipmust be its id. proof.verify(block.round, validators, chain)must pass with the set of epochi. This is the link: each set vouches for the next.- The last
SetValidatorsin the block gives the next set, withchainset to the block's id.
checked_pivot then checks the pivot itself:
proof.tipis the pivot's id, and itstx_rootis good. Otherwisethe pivot block and its proof do not match.- Find the last epoch whose
start_heightis below the pivot's height: that set signed the pivot. A pivot at genesis fails. proof.verifywith that set, orthe pivot's proof fails.- If the pivot changes the validators, an epoch must start exactly there, or
the pivot changes the validators but no epoch starts there. - Return the epoch the pivot belongs to, and the list of epochs up to it.
Leaving epochs out of /v1/epochs doesn't help a source: a pivot signed by a later set then fails step 3.
Downloading the state
GET /v1/state/ranges?height=H&from=KEY&bytes=N returns application/octet-stream, the borsh of:
pub struct StateChunk {
pub leaves: Vec<([u8; 32], Vec<u8>)>, // key hash, value; in key-hash order
pub proof: Option<StateRangeProof>,
pub done: bool,
}
| Constant | Value | Meaning |
|---|---|---|
DEFAULT_CHUNK_BYTES | 2 MiB | bytes when omitted, and what the follower always asks for |
MAX_CHUNK_BYTES | 4 MiB | server cap on bytes, a soft limit |
MAX_REPLY_BYTES | 64 MiB | the most a follower reads from one reply |
from is 32 bytes of hex, default all zero. A leaf counts 32 bytes plus its value toward the limit. The limit is soft: the leaf that crosses it is still included, so asking for 1,500 bytes returned one leaf of 2,008 bytes and a proof, 2,314 bytes in all.
The follower walks it like this:
Store::begin_restore(height, state_root)refuses unless the head is 0 (a snapshot can only be restored into a fresh chain) and the height is above 0.- Start with
fromat 32 zero bytes. - Fetch a chunk, take
chunk.next()as the nextfrom, which is the last key hash plus 1, or nothing oncedoneis set. Restore::add_chunk(chunk).- Log
restoring the stateevery 50 chunks.
add_chunk hands the leaves and the proof to JellyfishMerkleRestore:
- Key hashes must rise strictly from the last one seen.
- Each leaf goes into the partial tree, with its value hash computed locally.
- The proof holds only
right_siblings: what lies to the right of the path to the chunk's last leaf. The left siblings come from the leaves received so far, so a proof at chunkicovers chunks 0 toi. The root computed from the last leaf and both sets of siblings must be the certified root. - Finished nodes and values go to
jmt_nodesandjmt_valuesthroughSink, unsynced.
A lying source fails at its first bad chunk. It can't insert, change or leave out a leaf, because the root would differ. No trailing part of the tree can be omitted either, because the right siblings of the last leaf commit to it.
Finishing
Store::finish_restore(restore, block):
- The block's height and root must match the restore.
inner.finish()writes what is left.get_root_hash(version)is compared with the root once more.- The head must still be 0.
- One synced batch sets
latest_height,pruned_beforeandblocks_fromto the pivot, and writes the block, itstx_indexentries and its metadata.
Before step 5 the node is still at height 0, and nothing it restored counts. The metadata is the tip, the tip proof, finality/<pivot>, consensus_epoch and consensus_epochs.
After a snapshot the node holds:
| Data | Held |
|---|---|
| State | the pivot and everything after it, state_from = pivot |
| Blocks | the pivot and everything after it, blocks_from = pivot, plus the first block of each earlier epoch with its proof, so the next node can join from this one |
| Receipt of the pivot's own transactions | no, finish_restore writes none |
| Blocks that ended earlier epochs | no, they were checked and not stored |
| The list of epochs | yes |
Later passes replay from the pivot. qubits_snapshot_syncs_total goes up by one.
A join in the logs, with 44 leaves, on loopback:
INFO qubits_node::sync: taking the state from a snapshot height=320
INFO qubits_node::sync: state restored from a snapshot height=320 leaves=44 chunks=1
INFO qubits_node::app: committed block height=321 round=321 txs=40 kb=135 id=6a192319e94d958f...
Failure modes
A failed attempt is retried by the next pass, with the next source, from the start. The two caveats were reproduced against Store.
- The pivot is pruned or the source goes away while downloading.
/v1/state/rangesanswers 404,state at version {v} was pruned (kept from {p}), and the pass fails. On a node with--prune-keep 100a pivot can be as little as 37 blocks from being pruned. - Another source has another pivot. The next attempt starts over with that pivot.
- A source that holds no epoch blocks. A node that joined from a snapshot doesn't hold the blocks that ended earlier epochs, so on a chain that has changed validators, a joiner pointed only at it fails at
GET /v1/blocks/{start_height}/raw. Put eras back on that node first, or follow a node with full history.
A failed attempt leaves its nodes and values in
jmt_nodesandjmt_values. The markers don't move, so the node stays at height 0 and those versions are never read, until a retry finishes at a later pivot. Then a key that held a value at the failed pivot and was deleted before the new one still answers fromjmt_values, while the tree proves it absent. Only contract storage keys are deleted, so this needs a contract. Iftaking the state from a snapshotis logged without a matchingstate restored from a snapshot, stop the node, delete the data directory and runinitagain.
If a chunk reaches its limit exactly on the last leaf live at the pivot, and
jmt_valuesholds further key hashes that aren't live there, such as keys created after the pivot,doneis false and the next chunk has no leaves and no proof.Restore::add_chunkrefuses it withchunk without a proof. That attempt fails, and so does every retry at that pivot. The next pivot gets a different walk.
Keeping up while validating
A validator follows the chain through consensus, and its peers answer block requests from memory. The engine keeps blocks down to BLOCK_RETENTION, 256 blocks below the committed height, and answers with up to SYNC_BATCH, 64, ancestors. A validator further behind than that can't catch up through consensus. With --follow, it syncs over HTTP before that happens.
| Constant | Value | Meaning |
|---|---|---|
MAX_CONSENSUS_LAG | 128 | blocks behind before syncing over HTTP |
BLOCK_RETENTION | 256 | blocks peers keep to serve |
KEEP_UP_EVERY_MS | 2,000 | how often the height is polled and compared |
STUCK_CHECKS | 5 | checks in a row, behind and not moving, that count as stuck |
Validator::follow starts a KeepingUp with its own thread, keep-up, which polls remote_height every 2 s, so watching the sources never holds up consensus. On the validator thread, every 2 s, keep_up:
- Computes
behind = remote - head. - Counts a stuck check if
behindis above 0 and the head has not moved. On an idle chain no traffic arrives to catch up from, so this is the only way to notice. - Sets
farifbehindis above 128. - Does nothing if
behindis 0, or if this key is a member and neither far nor stuck. - Logs
syncing from the follow sourcesatWARNwithbehind. - If far, runs
catch_up(2, 300 s). Otherwise runs one pass. - On success, adds 1 to
qubits_syncs_total. If the head moved, drops the current engine and opens the epoch again from the new tip.
A validator removed from the set has no engine, so it counts as a non-member and keeps syncing like this.
catch_upin step 6 runs on the validator thread, and the height it acts on is unverified. A source that reports a height it can't back makes a member with--followstop running consensus untilcatch_uptimes out, 300 s at a time. Point--followat nodes you run or trust to be up.
A key that is not in the set runs spawn_standby and needs --follow. It does a pass every 100 ms, drops anything arriving from peers, and checks after each pass whether the set now holds its key. Once it does, it starts a Validator, logs joined the validator set, and hands its Follower to Validator::follow. See Validators and epochs.
History expiry
--history-keep bounds how long a node keeps old blocks. The default is 7d, and all keeps everything. State is not affected: that is --prune-keep, see state pruning.
The value is all, or a count and a unit: 7d, 12h, 30m, 45s, for days, hours, minutes and seconds. The count must be above 0.
| Constant | Value | Meaning |
|---|---|---|
CHECKPOINT_EVERY | 8,192 | blocks per era; a block at a multiple of this is kept forever |
HISTORY_MARGIN | 128 (2 * PROOF_EVERY) | blocks this close to the head stay whatever their age, because snapshots start from them |
PRUNE_EVERY | 10 s | pause between runs |
PRUNE_BATCH | 1,024 | blocks looked at in one run |
Unless the value is all, thread history runs on every node, validators included. After a run that deleted a full batch it goes again at once. Otherwise it waits PRUNE_EVERY.
prune_once computes cutoff = now - keep, and Store::prune_history walks from blocks_from upward, stopping at head - 128 or after 1,024 blocks. For each block held:
- If its timestamp is at or above
cutoff, stop. Everything from here on stays. - If its height is a multiple of the era length, or it changes the validators, keep it.
- Otherwise delete it, with its
tx_indexentries, itsreceiptsentries andfinality/<height>.
blocks_from moves past everything looked at, kept or not, in one synced batch with the deletions. So it means "the oldest block from which on everything is held", and kept blocks sit below it.
Data below blocks_from | Kept |
|---|---|
| Blocks at multiples of the era length | forever, with proof, index entries and receipts |
| Blocks that change the validators | forever, with proof, index entries and receipts |
| Every other block | dropped, with the same three |
| State | not touched |
Epoch ends hand the next validator set to joining nodes. Era ends anchor era files, and any range of blocks can be proven final from the next one. A checkpoint proof with 50 validators is about 113 KB, per 8,192 blocks.
/v1/status reports blocks_from. Fetching a dropped block answers 404, history before block {from} is not kept on this node, and a dropped transaction 404, unknown transaction. qubits_history_pruned_total counts blocks deleted.
Measured with --era-length 64 --history-keep 1s, at head 357:
INFO qubits_node::history: dropped old blocks deleted=64 from=229
blocks_from was 229, which is 357 - 128. Block 64, an era end, still answered, and block 65 answered 404.
Era files
An era file holds a run of blocks that can be checked against the genesis file alone, so history can live anywhere. An archive node hands them out, and a node that dropped its history takes them back.
Era index holds heights index * length + 1 to (index + 1) * length, with length 8,192. Era 0 is heights 1 to 8,192, and every era ends on a block whose proof is kept forever. The length is a chain-wide agreement. --era-length exists for tests, and every node of a chain must use the same value.
Layout
All borsh, integers little-endian. Blocks are streamed, never all held in memory.
Header
magic [u8; 4] "QERA"
version u8 1
genesis_hash [u8; 32]
first_height u64
count u32 blocks in the file
Vec<EraEpoch> u32 length, then the items
epoch u64
block ChainBlock
proof FinalityProof
count times
block ChainBlock
proof Option<FinalityProof> 1 tag byte, then the proof if the tag is 1
The header is 49 bytes. Vec<EraEpoch> holds every epoch above 0 that starts below first_height: the blocks and proofs a reader needs to get from the genesis set to the set in force at the start of the file. Each block carries the proof kept for its height, if there is one. The last block always has one.
The first bytes of a real era 0, from a chain with genesis hash 4203f664...:
00000000: 5145 5241 0142 03f6 6474 3dd9 48e3 0b61 QERA.B..dt=.H..a
00000010: 7a14 c320 a806 db4c d194 66fe efae a8f5 z.. ...L..f.....
00000020: ee5a 9f26 3101 0000 0000 0000 0040 0000 .Z.&1........@..
00000030: 0000 0000 0001 0000 0000 0000 0001 0000 ................
| Offset | Bytes | Field | Value |
|---|---|---|---|
| 0 | 4 | magic | QERA |
| 4 | 1 | version | 1 |
| 5 | 32 | genesis_hash | 4203f664... |
| 37 | 8 | first_height | 1 |
| 45 | 4 | count | 64 |
| 49 | 4 | length of Vec<EraEpoch> | 0 |
| 53 | 8 | first block, round | 1 |
| 61 | 8 | first block, height | 1 |
Two 64-block files from that chain, holding 1,200 and 1,160 transfers, were 4,256,574 and 4,039,298 bytes.
File names are {chain_id}-{first 8 hex of the genesis hash}-{index:05}.era, and for a part of an era {chain_id}-{hash}-{index:05}-to-{height}.era.
Serving
| Endpoint | Answer |
|---|---|
GET /v1/eras | { "length": 8192, "eras": [...] }, the complete eras this node can hand out |
GET /v1/eras/{index} | the file, streamed as it is read, as application/octet-stream with a content-disposition file name |
GET /v1/eras/{index}?to=H | the same era, ending at H |
An era is listed if its first block is at or above blocks_from and the proof at its end is kept. A node that has lost part of an era doesn't offer it. ?to=H needs H inside the era and at or below the head, the era's first block held, and a proof kept at H, because a file must end on a proof. It answers 404 otherwise.
Commands
qubits-node era fetch --from URL --out DIR [ERA...]
qubits-node era fetch --from URL --out DIR --to HEIGHT
qubits-node era verify --genesis FILE FILES...
qubits-node era import --data-dir DIR FILES...
era fetch with no era numbers takes every era the source offers, and skips files already in DIR. Each file is written as {name}.part, synced, then renamed. There is no timeout. Flags are listed on the node page.
--to HEIGHT is for a node that holds blocks from HEIGHT on: it fetches the part of that era up to HEIGHT, then each complete era below, newest first. That is what a node that joined from a snapshot needs to get its history back, and HEIGHT is its pivot, which has a proof.
$ qubits-node era fetch --from http://127.0.0.1:28545 --out eras
qubits-docs-4203f664-00000.era: 4 MB
qubits-docs-4203f664-00001.era: 4 MB
$ qubits-node era fetch --from http://127.0.0.1:28545 --out eras
qubits-docs-4203f664-00000.era: already here
qubits-docs-4203f664-00001.era: already here
$ qubits-node era verify --genesis genesis.json eras/*.era
eras/qubits-docs-4203f664-00000.era: ok, blocks 1..=64, 1200 transactions, 1 proofs
eras/qubits-docs-4203f664-00001.era: ok, blocks 65..=128, 1160 transactions, 1 proofs
era::verify
era verify needs nothing but the genesis file. It exits with code 1 if any file fails, and prints INVALID: {reason} for that file.
Reader::openchecks the magic and the version, and refusescount0 orfirst_height0.- The genesis hash in the header must match:
another chain's era. epoch_viewswalksVec<EraEpoch>as in walking the epochs, from the genesis validators.- The set in force is the last epoch starting below
first_height. A later epoch in the list fails:epochs after the era's start included. - For each block: the height must be the next one,
parentmust be the previous id, andcheck_tx_rootmust pass. - If the block has a proof, it must verify with the set in force.
- A block that changes the validators must have a proof, and then the set in force changes.
- The last block must have a proof:
the last block has no proof.
Blocks are linked by hash, and block ids commit to their payloads, so the proof at the end covers the whole run. Verification does not execute anything or check transaction signatures. That takes state, and era import does it when appending.
Flipping one bit in a file gave:
bad.era: INVALID: block 1: decoding failed: tx_root does not match transactions
Error: some files failed
era::import
era import opens the database itself, so stop the node first. A running node makes it fail with storage: IO error: While lock file: {dir}/LOCK: Resource temporarily unavailable. Node::open needs the database initialized with the same genesis. It prints what it did per file, then height {h}, blocks held from {from}.
With first and last the file's heights, head the node's head and from its blocks_from:
| Case | Condition | Action |
|---|---|---|
| Append | first == head + 1 | executes the blocks on top of the head |
| Put back | first < from and last + 1 >= from | stores the blocks below the oldest held |
| Already here | last <= head and first >= from | error: blocks {first}..={last} are already here |
| Neither | anything else | error: era {first}..={last} fits neither after the head ({head}) nor under the oldest block held ({from}): import eras in order |
Append goes through apply_proven, the path of verifying blocks, including full re-execution. It flushes at each block with a proof, and at 64 blocks at the latest. Blocks left unproven at the end fail with the era's last blocks are not provably final. It rebuilds a node from files alone: init, then import from era 0 up, oldest first. Blocks are logged as usual.
a-eras/qubits-docs-4203f664-00000.era: executed blocks 1..=64
a-eras/qubits-docs-4203f664-00001.era: executed blocks 65..=128
height 128, blocks held from 1
Put back gets history back under a node that started from a snapshot or dropped it. It does not execute: state below state_from is gone, and the blocks are only served.
- Set up the same
Checkerasera::verify, from the file's epochs, and run it on every block. - Stream the blocks. Those below
fromgo toblocks, with theirtx_indexentries and proofs, 64 at a time throughStore::put_blocks. They are not visible yet:blocks_fromhasn't moved. - Where the file overlaps blocks held, the first such block's id must equal the one held, or
block {h} is not the one this node holds. That links the whole file to a block this node already trusts. - If the file ends right under
from, the block held atfrommust have the file's last block as its parent, orthe era does not link to block {from}. set_blocks_from(first).
Any failure undoes the writes with Store::delete_blocks, leaving index entries that lead nowhere. Import newest first, because each file must reach up to the blocks already held.
$ qubits-node era import --data-dir api eras/qubits-docs-4203f664-00001.era eras/qubits-docs-4203f664-00000.era
eras/qubits-docs-4203f664-00001.era: put back blocks 65..=128
eras/qubits-docs-4203f664-00000.era: put back blocks 1..=64
height 131, blocks held from 1
After that, the node in this run reported blocks_from: 1 and offered eras 0 and 1 itself. state_from stayed 128, so a proof at height 5 answered state at that height was pruned.
Era files hold no receipts. After a put back, /v1/transactions/{id} finds a contract transaction but its receipt is null. Append re-executes, so it gets receipts back.
What failures look like
| Message | Meaning | What to do |
|---|---|---|
{source} keeps blocks only from {from} and this node is at {head}: re-initialize it to join from a snapshot | The source dropped the blocks this node needs next. Logged at WARN as sync pass failed on every pass | Delete the data directory, run init, and let it join from a snapshot, or fetch and import the missing eras. A validator loses its vote record this way, see the caveat |
the same message with this node is at 0 | The node is fresh, and the source dropped history and offered no snapshot | Use another source: this node has nothing to re-initialize |
{source} follows a different chain | The source's genesis hash differs | Fix --follow |
source: {url}: ... | The source is down or answered an HTTP error | Nothing, while another source answers |
invalid data from source: ... | A check failed: a block, a proof, a chunk | The source is faulty or on another chain; the node has moved on to the next one |
could not catch up; joining consensus anyway | catch_up timed out at startup | Check the sources; the validator runs anyway, and consensus may catch it up |
syncing from the follow sources | The validator was far behind, stuck or out of the set | Expected once in a while |
sync failed | The sync started by keep_up failed | Check the sources; it tries again in 2 s |
A follower blocked on keeps blocks only from does not recover by itself, and it logs that line about twice a second until the operator acts.
Source
chain/crates/qubits-node/src/sync.rschain/crates/qubits-node/src/history.rschain/crates/qubits-node/src/era.rschain/crates/qubits-node/src/validator.rschain/crates/qubits-node/src/lib.rschain/crates/qubits-node/src/rpc.rschain/crates/qubits-node/src/main.rschain/crates/qubits-node/tests/sync.rschain/crates/qubits-state/src/store.rschain/crates/qubits-consensus/src/engine.rschain/crates/qubits-consensus/src/types.rschain/crates/qubits-types/src/rpc.rschain/crates/qubits-e2e/solo.py