Skip to content

Merge confirmed_log and preprocessed_blocks into block_hashes#6163

Merged
afck merged 6 commits into
linera-io:mainfrom
afck:rm-confirmed-log
May 7, 2026
Merged

Merge confirmed_log and preprocessed_blocks into block_hashes#6163
afck merged 6 commits into
linera-io:mainfrom
afck:rm-confirmed-log

Conversation

@afck

@afck afck commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Motivation

We are planning to implement checkpointing for chains, which will mean that even the executed part of a chain will not necessarily start at block 0. This makes the artificial distinction between confirmed_log and preprocessed_blocks even more complicated, since the former now has an offset, i.e. index is not equal to block height anymore.

Proposal

Replace confirmed_log: LogView<CryptoHash> and preprocessed_blocks: MapView<BlockHeight, CryptoHash> with a single block_hashes: CustomMapView<BlockHeight, CryptoHash> that holds executed blocks (height < next_block_height) and preprocessed blocks (height >= next_block_height) together.

Add a big-endian CustomSerialize impl for BlockHeight so that lexicographic key order matches numeric order; this also fixes a latent bug in next_height_to_preprocess, which relied on indices().last() returning the numerically highest height. (Fixed by #6162 on testnet_conway.)

Collapse three near-duplicate WorkerError variants (ConfirmedLogEntryNotFound, PreprocessedBlocksEntryNotFound, ConfirmedBlockHashNotFound) into a single BlockHashNotFound.

Test Plan

CI should catch any regressions.

Release Plan

  • Nothing to do / These changes follow the usual release cycle.

Links

Replaces `confirmed_log: LogView<CryptoHash>` and `preprocessed_blocks:
MapView<BlockHeight, CryptoHash>` with a single `block_hashes:
CustomMapView<BlockHeight, CryptoHash>` that holds executed blocks
(height < next_block_height) and preprocessed blocks (height >=
next_block_height) together. This removes the dense/sparse partition
spread across multiple call sites and prepares for checkpointing, where
the executed range will no longer start at zero.

Adds a big-endian `CustomSerialize` impl for `BlockHeight` so that
lexicographic key order matches numeric order; this also fixes a
latent bug in `next_height_to_preprocess`, which relied on
`indices().last()` returning the numerically highest height.

Collapses three near-duplicate `WorkerError` variants
(`ConfirmedLogEntryNotFound`, `PreprocessedBlocksEntryNotFound`,
`ConfirmedBlockHashNotFound`) into a single `BlockHashNotFound`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Apr 29, 2026

Copy link
Copy Markdown

Instruction Count Benchmark Results

Baseline: d10fe83f81

Deterministic metrics — reproducible across runs (34 benchmarks)
Benchmark Instructions Total R+W
Cold Load
load_1000 693,857 (No change) 1,010,338 (No change)
CollectionView
indices_100 192,360 (-0.01%) 267,627 (-0.01%)
load_all_100_from_storage 638,209 (-0.00%) 901,177 (-0.00%)
load_all_100_in_memory 340,864 (-0.01%) 477,136 (-0.01%)
pre_save_100 265,880 (No change) 367,671 (No change)
try_load_10_from_100 100,570 (No change) 142,401 (No change)
MapView
contains_key_10_from_100 52,533 (No change) 74,551 (No change)
contains_key_10_from_1000 355,093 (No change) 501,852 (No change)
get_10_from_100 55,010 (No change) 78,171 (No change)
get_10_from_1000 357,626 (No change) 505,553 (No change)
get_100_missing_from_1000 610,063 (-0.00%) 851,207 (-0.00%)
indices_100 100,452 (No change) 138,331 (No change)
indices_1000 948,222 (No change) 1,322,202 (No change)
insert_100 257,098 (-0.04%) 355,490 (-0.04%)
insert_1000 2,963,505 (No change) 4,013,290 (No change)
post_save_1000 1,027,028 (No change) 1,481,044 (No change)
pre_save_100 332,585 (No change) 462,487 (No change)
pre_save_1000 3,381,713 (No change) 4,758,733 (No change)
remove_500_from_1000 1,189,593 (-0.00%) 1,660,493 (-0.00%)
QueueView / BucketQueueView
delete_500_from_1000 22,386 (+0.44%) 34,344 (+0.45%)
front_100_from_1000 5,701 (+0.37%) 8,420 (+0.31%)
pre_save_1000 43,101 (No change) 60,470 (No change)
push_1000 24,367 (+0.09%) 33,322 (+0.08%)
delete_500_from_1000 10,243 (+0.21%) 12,351 (+0.21%)
front_100_from_1000 9,137 (+0.23%) 13,881 (+0.19%)
pre_save_1000 1,042,902 (No change) 1,498,608 (No change)
push_1000 24,294 (+0.09%) 33,225 (+0.08%)
ReentrantCollectionView
contains_key_10_from_100 141,897 (+0.01%) 201,820 (+0.01%)
indices_100 237,095 (No change) 332,315 (No change)
load_all_100_from_storage 803,429 (+0.00%) 1,132,514 (+0.01%)
load_all_100_in_memory 411,235 (No change) 566,271 (No change)
pre_save_100 350,812 (No change) 488,465 (No change)
RegisterView
get_set_100 81,292 (+0.03%) 120,126 (+0.02%)
pre_save 5,464 (-0.38%) 8,063 (-0.32%)

Regression threshold: 1%${\color{red}\textbf{red}}$ = regression, ${\color{green}\textbf{green}}$ = improvement.

Cache-dependent metrics — expect fluctuations between runs (34 benchmarks)
Benchmark L1 Hits LLC Hits RAM Hits Est. Cycles
Cold Load
load_1000 1,001,807 (+0.00%) 8,355 (-0.02%) 176 (No change) 1,049,742 (-0.00%)
CollectionView
indices_100 266,378 (-0.01%) 854 (+0.23%) 395 (-0.25%) 284,473 (-0.02%)
load_all_100_from_storage 896,656 (-0.00%) 3,858 (-0.54%) 663 (-0.15%) 939,151 (-0.02%)
load_all_100_in_memory 475,001 (-0.01%) 1,394 (+0.14%) 741 (-0.13%) 507,906 (-0.01%)
pre_save_100 365,729 (-0.00%) 1,346 (+0.30%) 596 (No change) 393,319 (+0.00%)
try_load_10_from_100 141,543 (+0.00%) 630 (-0.32%) 228 (No change) 152,673 (-0.01%)
MapView
contains_key_10_from_100 74,253 (No change) 90 (No change) 208 (No change) 81,983 (No change)
contains_key_10_from_1000 498,667 (-0.00%) 2,977 (+0.07%) 208 (No change) 520,832 (+0.00%)
get_10_from_100 77,875 (+0.00%) 84 (${\color{green}\textbf{-2.33\%%}}$) 212 (No change) 85,715 (-0.01%)
get_10_from_1000 502,359 (-0.00%) 2,981 (+0.03%) 213 (No change) 524,719 (+0.00%)
get_100_missing_from_1000 847,998 (-0.00%) 2,981 (-0.03%) 228 (-0.44%) 870,883 (-0.01%)
indices_100 137,711 (No change) 224 (No change) 396 (No change) 152,691 (No change)
indices_1000 1,314,542 (-0.00%) 6,480 (+0.02%) 1,180 (No change) 1,388,242 (+0.00%)
insert_100 354,745 (-0.05%) 95 (${\color{red}\textbf{+11.76\%%}}$) 650 (No change) 377,970 (-0.03%)
insert_1000 4,006,265 (+0.00%) 3,043 (-0.07%) 3,982 (No change) 4,160,850 (-0.00%)
post_save_1000 1,469,656 (No change) 11,205 (No change) 183 (No change) 1,532,086 (No change)
pre_save_100 461,110 (+0.00%) 762 (-0.13%) 615 (No change) 486,445 (-0.00%)
pre_save_1000 4,744,802 (+0.00%) 10,115 (-0.13%) 3,816 (No change) 4,928,937 (-0.00%)
remove_500_from_1000 1,656,117 (-0.00%) 4,199 (-0.07%) 177 (-0.56%) 1,683,307 (-0.00%)
QueueView / BucketQueueView
delete_500_from_1000 34,145 (+0.44%) 37 (${\color{red}\textbf{+12.12\%%}}$) 162 (+0.62%) 40,000 (+0.51%)
front_100_from_1000 8,245 (+0.27%) 38 (${\color{red}\textbf{+8.57\%%}}$) 137 (+0.74%) 13,230 (+0.55%)
pre_save_1000 60,131 (+0.01%) 60 (${\color{green}\textbf{-9.09\%%}}$) 279 (No change) 70,196 (-0.03%)
push_1000 33,108 (+0.08%) 52 (${\color{green}\textbf{-1.89\%%}}$) 162 (+0.62%) 39,038 (+0.14%)
delete_500_from_1000 12,175 (+0.17%) 38 (${\color{red}\textbf{+11.76\%%}}$) 138 (+0.73%) 17,195 (+0.44%)
front_100_from_1000 13,680 (+0.15%) 38 (${\color{red}\textbf{+11.76\%%}}$) 163 (+0.62%) 19,575 (+0.39%)
pre_save_1000 1,493,910 (+0.00%) 2,736 (-0.07%) 1,962 (No change) 1,576,260 (-0.00%)
push_1000 33,014 (+0.08%) 51 (${\color{green}\textbf{-1.92\%%}}$) 160 (+0.63%) 38,869 (+0.14%)
ReentrantCollectionView
contains_key_10_from_100 200,602 (+0.01%) 1,019 (-0.39%) 199 (+0.51%) 212,662 (+0.02%)
indices_100 330,746 (+0.00%) 1,198 (-0.17%) 371 (No change) 349,721 (-0.00%)
load_all_100_from_storage 1,125,961 (+0.01%) 6,147 (-0.02%) 406 (-0.25%) 1,170,906 (+0.00%)
load_all_100_in_memory 563,884 (-0.00%) 1,840 (+0.05%) 547 (No change) 592,229 (+0.00%)
pre_save_100 485,544 (No change) 2,233 (No change) 688 (No change) 520,789 (No change)
RegisterView
get_set_100 119,903 (+0.02%) 41 (${\color{green}\textbf{-2.38\%%}}$) 182 (+0.55%) 126,478 (+0.04%)
pre_save 7,857 (-0.34%) 45 (${\color{red}\textbf{+4.65\%%}}$) 161 (-0.62%) 13,717 (-0.38%)

Cache metrics fluctuate because anything that changes the virtual memory layout
shifts which data lands on which cache lines, changing the L1/LLC/RAM distribution.
Probable causes: ASLR (even across identical binaries), executable binary size changes,
shared library size changes, and even filename length differences.

Cachegrind simulates a two-level cache (L1 + LLC) auto-detected from the host CPU.
Est. Cycles = L1 hits + 5 × LLC hits + 35 × RAM hits.

Runner cache sizes: L1d cache: 64 KiB (2 instances);L1i cache: 64 KiB (2 instances) L2 cache: 1 MiB (2 instances);L3 cache: 32 MiB (1 instance)

Update the GraphQL query to fetch (height, hash) pairs from
`blockHashes`, and rewrite the "Confirmed Log" section in Chain.vue to
iterate the entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@afck afck requested review from bart-linera, deuszx and ma2bd April 29, 2026 12:27
@deuszx

deuszx commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Add a big-endian CustomSerialize impl for BlockHeight so that lexicographic key order matches numeric order; this also fixes a latent bug in next_height_to_preprocess, which relied on indices().last() returning the numerically highest height. (Fixed by #6162 on testnet_conway.)

Can you add a test for this, similar to the on in #6162 ?

Ports the regression test from linera-io#6162 to the merged-`block_hashes` design:
heights 1 and 256 span a byte boundary, so a naive `indices().last()`
under little-endian encoding would return the wrong height. With the
big-endian `CustomSerialize` impl, `next_height_to_preprocess` returns
the numeric maximum.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@afck

afck commented Apr 29, 2026

Copy link
Copy Markdown
Contributor Author

@deuszx: Done in 7fcb725.

Comment thread linera-chain/src/chain.rs Outdated
Comment on lines +484 to +488
let next_block_height = self.tip_state.get().next_block_height;
if let Some(height) = self.block_hashes.indices().await?.last() {
if *height >= next_block_height {
return Ok(height.saturating_add(BlockHeight(1)));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For chains with many executed blocks and no preprocessed ones, this is an extra cost on every call. Probably negligible since indices().last() on a sorted map should be O(log n), but worth confirming the storage backend doesn't first load the full list.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.indices() most certainly loads the full list

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! We'd need a last_key method, I guess.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a field to memoize the max height.

Later we could do something like #6171, and remove that field.

afck and others added 2 commits April 29, 2026 18:44
Replaces the O(N) `block_hashes.indices().last()` scan with a direct read
from a new `next_height_to_preprocess: RegisterView<C, BlockHeight>`
field. The register is updated on every `block_hashes.insert(..)` in
`confirm_block` and `preprocess_block`, so it always reflects one past
the highest height known to this chain (executed or preprocessed).

Callers read the register directly (`*chain.next_height_to_preprocess.get()`),
which lets us drop the asynchronous wrapper method.

This is a temporary shortcut: once `linera-views` exposes an efficient
`last_index` lookup on `CustomMapView`, the field can be removed and the
value computed from `block_hashes` directly. The field's docstring
records that future cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both call sites (`confirm_block`, `preprocess_block`) used to call
`self.block_hashes.insert(...)` and then separately update
`next_height_to_preprocess`. Wrapping the pair in a single
`insert_block_hash(height, hash)` helper makes the invariant local —
every write to `block_hashes` keeps the register in sync — and a future
third write site can't accidentally skip the update.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread linera-chain/src/chain.rs
Comment on lines +216 to +217
/// `height < next_block_height` is executed; a block at `height >= next_block_height`
/// is preprocessed (verified but not yet executed) and may not be contiguous.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(maybe related) Is this still the case that we eagerly execute sender blocks if they happen to be contiguous?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

@afck afck added this pull request to the merge queue May 7, 2026
Merged via the queue into linera-io:main with commit 2b843ac May 7, 2026
41 checks passed
@afck afck deleted the rm-confirmed-log branch May 7, 2026 13:46
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.

Merge confirmed_log and preprocessed_blocks.

4 participants