Skip to content

deps: move to UTXO-Z 0.9.1, and to the contracts it changed underneath - #645

Merged
fpelliccioni merged 1 commit into
masterfrom
deps/utxoz-0.9.0-reference-mode
Aug 10, 2026
Merged

deps: move to UTXO-Z 0.9.1, and to the contracts it changed underneath#645
fpelliccioni merged 1 commit into
masterfrom
deps/utxoz-0.9.0-reference-mode

Conversation

@fpelliccioni

@fpelliccioni fpelliccioni commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Moves Knuth from utxoz/0.8.0 to utxoz/0.9.0, with conan.lock regenerated
from scratch rather than patched.

The visible half of this bump is a rename. The half that matters is what four
functions now return.

The rename: compactreference, for the MODE only

UTXO-Z 0.8.0 UTXO-Z 0.9.0
compact_db reference_db
compact_find_result reference_find_result
storage_mode::compact storage_mode::reference
compact_file_size, compact_test_file_size reference_*

Knuth follows: option utxoz_compactutxoz_reference, macro
KTH_UTXOZ_COMPACT_MODEKTH_UTXOZ_REFERENCE_MODE,
database::utxoz_compact_mode()utxoz_reference_mode(), C-API
kth_node_utxoz_compact_modekth_node_utxoz_reference_mode, and the
internal compact_utxo_ref / pack_compact_ref that name the reference-mode
payload. 70 identifiers in all.

What deliberately keeps the word, because it is the compaction operation
or something else entirely: utxoz_database::compact() and UTXO-Z's
compact_all(); block_chain::utxo_compact(); BIP152 compact_block and
send_compact; history_compact and stealth_compact; chain::compact (the
difficulty-bits encoding); CompactSize in the consensus serialisers;
secp256k1's compact signature format; and utxo_compact_block, the UTXO
builder's small parsed-block view, which is used in both modes and is not
the storage mode at all.

Each rename rule names an exact identifier rather than the substring compact,
so none of the above could be caught by accident.

The contracts that changed underneath

Function 0.8.0 0.9.0 Breaks the build?
compact_all() void result<> No
process_pending_lookups() pair<…> result<pair<…>> Yes (×3)
process_pending_deletions() pair<…> result<pair<…>> Yes
erase() size_t result<size_t> Yes

compact_all() is why this is not a version-number change: it used to return
void, so a caller that kept invoking it as a bare statement compiled exactly
as before while discarding every failure.

erase() is worth reading twice. It returned a count, and the natural migration
writes *erased > 0 ? success : key_not_found — which turns the erase failed
into the key was not there, about an entry that may still exist and still be
spendable. Both drains have the same shape: an unreadable catalogue is not an
empty queue, and the caller upstream reads absence as spent. All three now
report failure as failure.

What is new rather than changed

  • sync(), and close() does not do it for you. Wrapped as
    utxoz_database::sync().
  • platform_durability() / durability_level, which say what a successful
    sync is worth: under contents_only the file contents reach the disk and
    the directory entries naming them do not. Exposed as
    utxoz_platform_durability() rather than assumed.
  • The exclusive claim. open() now distinguishes database_in_use (another
    instance holds it) from database_lock_unavailable (the claim could not be
    attempted at all — no permission, a filesystem without locking, a lock file
    that is not a regular file). UTXO-Z's own header notes these send an operator
    looking in different places, so they are logged as different things, alongside
    recovery_required / recovery_failed and a storage-mode mismatch.
  • The error enum grew from 5 values to 21. Failures are now logged by name:
    the numbers moved, so an old note that said error 3 names a different fault
    today. The naming switch is exhaustive with no default, so code 22 will stop
    the build rather than print "unknown".

Negative controls

src/blockchain/test/utxoz_contract.cpp asserts the shape of these contracts,
mostly at compile time — a runtime test can only fail once the wrong call has
already been written and executed.

Proven non-vacuous by breaking it on purpose: reverting compact() to its
0.8.0 form fails the build with

utxoz_contract.cpp:72:24: error: static assertion failed:
  utxoz_database::compact() must report whether compaction happened

and the restored tree builds clean again. The exclusive-claim test has both
halves — a second open() is refused with database_in_use, and the path
opens again once released, so a library that refused every open would not pass.

Verification

Check Result
conan lock create from scratch utxoz/0.9.0#1641e774…, RC=0
conan install (tests, rpc, C++23) RC=0
Full cold build, full mode, RPC on 100%, RC=0
Full build, reference mode 100%, RC=0
kth_infrastructure_test 130992 assertions / 456 cases
kth_domain_test 1008358 / 3823
kth_blockchain_test (full mode) 3802 / 221
kth_blockchain_test (reference mode) 3791 / 220
kth_blockchain_vmlimits_test 4732 / 21
kth_network_test 1440 / 85
kth_node_test 3318 / 169
kth_capi_test 2790 / 735

Full mode remains the default — in the recipe and in both CMake lists.
Reference mode is built and tested here as a check: it is the half of the
#ifdef that nothing in CI compiles, so a rename that broke it would go
unnoticed until someone turned the flag on. Doing that found a real defect this
PR fixes: the executor still printed UTXO-Z mode: compact for reference mode,
because the identifier was renamed and the string beside it was not.

#602 is deliberately not included here.

Summary by CodeRabbit

  • New Features

    • Added UTXO-Z reference-mode support across storage, blockchain processing, diagnostics, and version reporting.
    • Added database synchronization and durability-status reporting.
    • Compaction now reports whether it completed successfully.
    • Added exclusive database access handling and clearer reopening behavior.
  • API Updates

    • Renamed compact-mode diagnostics and configuration options to reference-mode terminology.
  • Bug Fixes

    • Improved handling of storage failures, invalid references, pending queues, lookup errors, and lock conflicts.
    • Prevented validation and synchronization from continuing after critical UTXO processing failures.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@fpelliccioni, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 49 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: aa192d46-5ca4-4b54-8c60-fb040501db6c

📥 Commits

Reviewing files that changed from the base of the PR and between ea60e79 and a168ea2.

📒 Files selected for processing (2)
  • src/blockchain/test/utxoz_contract.cpp
  • src/node/src/sync/block_tasks.cpp
📝 Walkthrough

Walkthrough

The PR renames UTXO-Z compact mode to reference mode across configuration, APIs, storage adapters, blockchain processing, and diagnostics. It adds explicit error, durability, locking, reference-resolution, and contract-test handling.

Changes

UTXO-Z reference-mode migration

Layer / File(s) Summary
Reference-mode build and API contracts
src/blockchain/CMakeLists.txt, src/database/CMakeLists.txt, src/blockchain/include/..., src/database/include/..., src/c-api/...
Build options, compile guards, storage types, public methods, and diagnostics now use reference-mode terminology. Pending operations return std::expected, and compaction returns bool.
Database results and reference resolution
src/database/src/databases/utxoz_database.cpp
The adapter selects reference storage, propagates database failures, reports durability and locking results, and resolves stored references through the block store and header index.
Blockchain UTXO processing and failure propagation
src/blockchain/src/..., src/node/src/sync/block_tasks.cpp, src/blockchain/src/validate/..., src/node/test/...
UTXO construction, deferred lookup and deletion processing, validation, prevout population, and final compaction now check and propagate failures.
Regression and contract coverage
src/blockchain/test/...
Tests cover reference-mode selection, expected-returning APIs, compaction, durability, exclusive opening, lookup failures, undo behavior, and round-trip processing.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant block_tasks
  participant block_chain
  participant utxoz_database
  participant reference_db
  participant block_store
  block_tasks->>block_chain: build block UTXO delta
  block_chain->>utxoz_database: process reference UTXOs
  utxoz_database->>reference_db: resolve deferred lookups
  reference_db-->>utxoz_database: entries or result_code
  utxoz_database->>block_store: read referenced transaction
  block_store-->>utxoz_database: transaction data
  utxoz_database-->>block_chain: expected UTXO delta
  block_chain-->>block_tasks: apply delta or stop on error
Loading

Possibly related PRs

  • k-nuth/kth#547: Modifies the same UTXO-Z round-trip and UTXO construction areas.
  • k-nuth/kth#550: Modifies the shared UTXO-Z deferred-lookup APIs.
  • k-nuth/kth#574: Modifies the related reorganization undo test and UTXO-Z mode configuration.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ⚠️ Warning The title describes the dependency and contract migration, but it names UTXO-Z 0.9.1 while the changes update from 0.8.0 to 0.9.0. Change 0.9.1 to 0.9.0 so the title matches the dependency version updated by this pull request.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch deps/utxoz-0.9.0-reference-mode

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/blockchain/include/kth/blockchain/utxo_builder.hpp`:
- Around line 60-65: Update the Doxygen title immediately preceding
reference_utxo_ref to use “reference” terminology instead of the stale “Compact
UTXO reference” wording, leaving the struct and its documentation unchanged
otherwise.

In `@src/blockchain/src/utxo_builder.cpp`:
- Around line 248-254: Replace the debug-only KTH_ASSERT in the
KTH_UTXOZ_REFERENCE_MODE serialization path with release-safe handling for
file_number < 0: return the surrounding operation’s error, or use KTH_CONTRACT
if this invariant must terminate execution. Ensure reference_utxo_ref is never
constructed with a cast of -1 to UINT32_MAX.

In `@src/blockchain/test/utxoz_contract.cpp`:
- Around line 43-51: Replace the self-referential static_assert in the test
setup with a runtime REQUIRE that compares utxoz_reference_mode() against the
expected reference/full mode selected for the test. Keep the expected mode
derived from the local build configuration, but use the database target’s
utxoz_reference_mode() result to detect divergent definitions.
- Line 24: Update the process-ID usage in the test to be platform-safe: include
<process.h> and call _getpid() under _WIN32, while retaining <unistd.h> and
getpid() for non-Windows builds. Keep the existing PID-dependent test behavior
unchanged.

In `@src/database/src/databases/utxoz_database.cpp`:
- Around line 178-183: Update src/database/src/databases/utxoz_database.cpp at
lines 178-183, 260-269, 315-321, 339-361, and 367-377: preserve UTXO-Z operation
failures instead of converting them to missing-key or empty results. In the
direct lookup path, map only utxoz::error_code::not_found to
result_code::key_not_found and map other errors to result_code::other; return
deferred raw-lookup and deletion drain errors, and keep queue-drain errors
distinct from reference-resolution or genuinely empty results. Change the queue
APIs, wrappers, and callers to return and propagate result_code, stopping each
affected operation on failure.
- Around line 89-93: Update the UTXO-Z open calls in the
KTH_UTXOZ_REFERENCE_MODE branches to preserve the original filesystem path
instead of converting it with path.string(). Use the available UTF-8-preserving
or wide-path overload for both utxoz::reference_db::open and
utxoz::full_db::open, keeping remove_existing unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 9910b04b-3dfe-4247-ad60-102f83b520b2

📥 Commits

Reviewing files that changed from the base of the PR and between 1ed1b8a and c111aa0.

⛔ Files ignored due to path filters (3)
  • conan.lock is excluded by !**/*.lock, !conan.lock and included by none
  • conanfile.py is excluded by !**/conanfile.py and included by none
  • docs/utxo-store-api-requirements.md is excluded by none and included by none
📒 Files selected for processing (17)
  • src/blockchain/CMakeLists.txt
  • src/blockchain/include/kth/blockchain/interface/block_chain.hpp
  • src/blockchain/include/kth/blockchain/utxo_builder.hpp
  • src/blockchain/src/interface/block_chain.cpp
  • src/blockchain/src/utxo_builder.cpp
  • src/blockchain/test/block_undo.cpp
  • src/blockchain/test/reorg_undo_roundtrip.cpp
  • src/blockchain/test/utxoz_contract.cpp
  • src/blockchain/test/utxoz_roundtrip.cpp
  • src/c-api/include/kth/capi/node_info.h
  • src/c-api/src/node_info.cpp
  • src/database/CMakeLists.txt
  • src/database/include/kth/database/block_undo.hpp
  • src/database/include/kth/database/databases/utxoz_database.hpp
  • src/database/include/kth/database/define.hpp
  • src/database/src/databases/utxoz_database.cpp
  • src/node/src/executor/executor.cpp

Comment thread src/blockchain/include/kth/blockchain/utxo_builder.hpp
Comment thread src/blockchain/src/utxo_builder.cpp
Comment thread src/blockchain/test/utxoz_contract.cpp
Comment thread src/blockchain/test/utxoz_contract.cpp
Comment on lines +89 to 93
#ifdef KTH_UTXOZ_REFERENCE_MODE
auto result = utxoz::reference_db::open(path.string(), remove_existing);
#else
auto result = utxoz::full_db::open(path.string(), remove_existing);
#endif

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

In UTXO-Z version 0.9.0, what argument types does utxoz::reference_db::open accept, and what string encoding does it require for Windows paths?

💡 Result:

In the UTXO-Z C++ library (repository utxo-z/utxo-z), the open method for its database classes (such as utxoz::db::open and utxoz::compact_db::open) accepts two arguments: a path (typically a string or string literal) and a remove_existing boolean flag [1]. For Windows paths, while the library's examples demonstrate the use of standard narrow string literals (e.g., "./utxo_data") [1], Windows file APIs natively utilize UTF-16 encoding [2][3]. To ensure compatibility with non-ASCII characters in paths on Windows, programs interacting with the filesystem typically require the use of wide-character strings (UTF-16) and corresponding Win32 APIs (often denoted by a 'W' suffix) [4][3][5]. The library's open method, as shown in the source, takes a standard C++ string-like argument [1]; if the underlying implementation uses standard narrow C library calls, it may be subject to the system's current code page, which can fail for non-ASCII paths unless specifically configured [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find KTH_UTXOZ_REFERENCE_MODE and database open paths"
rg -n "KTH_UTXOZ_REFERENCE_MODE|reference_db::open|full_db::open|std::filesystem::path|string\\(\\)" -S .

echo
echo "Inspect target file around open call"
sed -n '60,110p' src/database/src/databases/utxoz_database.cpp

echo
echo "Locate utxo-z header/reference_db definitions in vendored/source if present"
git ls-files | rg -i "utxo.?z|utxo-z|reference_db|full_db" || true

Repository: k-nuth/kth

Length of output: 50366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect native_file helper and nearby path utility"
sed -n '1,80p' src/database/include/kth/database/native_file.hpp
rg -n "wstring\\(\\)|open_native|_W|SetConsole|ActiveCodePage|UTF-8|char_code|cp65001|u8string\\(\\)" src/database src/infrastructure -S --max-count 200 || true

echo
echo "Inspect utxoz_database.cpp header implementation"
sed -n '1,170p' src/database/include/kth/database/databases/utxoz_database.hpp
sed -n '1,160p' src/database/src/databases/utxoz_database.cpp

echo
echo "Check dependency/source references for utxo-z string handling"
git ls-files | rg -i "utxo.?z" | sed -n '1,120p'
rg -n "reference_db::open|class reference_db|namespace reference_db|open\\(.*string|u8string|wstring|utf8|utf-16|UTF-8|UTF-16" lib deps external third_party -S 2>/dev/null || true

Repository: k-nuth/kth

Length of output: 24595


🌐 Web query:

UTXO-Z C++ header reference_db open overload string literal wide string u8string Windows

💡 Result:

The UTXO-Z library provides a C++ database interface, typically accessed via the utxoz::db (aliased to utxoz::full_db) or utxoz::compact_db namespaces [1]. These databases are opened using static open methods, such as utxoz::db::open(path, remove_existing) [1]. Regarding the specific technical constraints mentioned: 1. Overloads and String Literals: The open method accepts a path argument [1]. In standard C++ practice, passing a string literal (which has type const char[N]) to a function accepting std::string or a similar string type usually works through implicit conversion, though it may sometimes be ambiguous depending on other overloads [2][3]. If you encounter ambiguity, explicitly constructing the string (e.g., std::string("path")) is the recommended fix [2][3]. 2. Windows, Wide Strings, and UTF-8: The UTXO-Z library uses standard C++ types for its API [1]. On Windows, if your filesystem paths require support for non-ASCII characters, ensure you are providing paths in the format expected by the underlying filesystem library (typically std::filesystem::path). std::filesystem::path natively supports both narrow (UTF-8/ANSI) and wide (UTF-16) string types, allowing you to pass narrow string literals, std::u8string (C++20), or wide strings (std::wstring) directly to the open method if the library implementation leverages std::filesystem internally. 3. Official Documentation: For the most accurate and up-to-date information on the UTXO-Z API, including any recent additions or changes to supported string types, refer to the official repository at github.com/utxo-z/utxo-z [1][4]. The library does not natively implement custom "u8string" overloads unless explicitly defined in its current header files; you should rely on standard library interop through std::filesystem::path where possible [1].

Citations:


🌐 Web query:

github utxo-z utxoz.hpp reference_db open implementation string literal

💡 Result:

The project utxo-z is a high-performance UTXO database designed for Bitcoin Cash, featuring an implementation of a hash table with open addressing to maximize cache locality [1][2]. In the library, the header file utxoz.hpp is the primary interface for interacting with the database [1]. The open method, used for opening the database, accepts a string literal (or std::string) as a path argument [1]. For example, to open a database, you can use: #include <utxoz/utxoz.hpp> // Open database — returns result<full_db> auto r = utxoz::db::open("./utxo_data", true); The implementation utilizes this design to allow for efficient storage and retrieval [1][2]. As of August 2026, the project is under active development, with version 0.8.0 having been released in June 2026 [3].

Citations:


Use a path-preserving overload for UTXO-Z open.

path.string() narrows the std::filesystem::path through the current code page on Windows before both utxoz::reference_db::open and utxoz::full_db::open receive it, so non-ASCII database directories can fail or open the wrong location. Forward a UTF-8 preserved input or a wide-path overload if UTXO-Z exposes one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/database/src/databases/utxoz_database.cpp` around lines 89 - 93, Update
the UTXO-Z open calls in the KTH_UTXOZ_REFERENCE_MODE branches to preserve the
original filesystem path instead of converting it with path.string(). Use the
available UTF-8-preserving or wide-path overload for both
utxoz::reference_db::open and utxoz::full_db::open, keeping remove_existing
unchanged.

Source: Learnings

Comment thread src/database/src/databases/utxoz_database.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/database/include/kth/database/databases/utxoz_database.hpp (2)

222-227: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate the reference payload length before copying it.

apply_delta_raw() currently assumes insert payloads are 8 bytes in reference mode. Copies at lines 225-226 read beyond raw.data when the insert has fewer bytes. Reject any insert whose payload size is not exactly 8 bytes before these copies and return the insert error instead of writing an invalid reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/database/include/kth/database/databases/utxoz_database.hpp` around lines
222 - 227, In apply_delta_raw() under KTH_UTXOZ_REFERENCE_MODE, validate that
the insert payload is exactly 8 bytes before the memcpy calls populating
file_number and tx_offset. For any other size, reject the insert and return the
existing insert error without copying or writing the reference.

178-208: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Propagate db_->erase() failures from both batch methods.

Both apply_delta() and apply_delta_raw() discard the wrapped erase result before returning result_code::success. If erase() reports a failure, return that failure instead so callers do not treat the batch as committed.

  • src/database/include/kth/database/databases/utxoz_database.hpp#L201-L203: Store and handle db_->erase(key, height) in apply_delta().
  • src/database/include/kth/database/databases/utxoz_database.hpp#L243-L244: Store and handle db_->erase(key, height) in apply_delta_raw().
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/database/include/kth/database/databases/utxoz_database.hpp` around lines
178 - 208, Propagate erase failures in both batch methods: update apply_delta()
at src/database/include/kth/database/databases/utxoz_database.hpp lines 178-208
and apply_delta_raw() at lines 222-245 to store the result of db_->erase(key,
height), return its failure as the appropriate result_code, and only return
success when every erase succeeds.
src/blockchain/src/utxo_builder.cpp (1)

921-937: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Replace debug-only header-index assertions with runtime validation.

Line 925 and Line 926 run before process_compact_block_utxos() at Line 930. In Release builds, KTH_ASSERT does not run. If active_at() returns null_index, the following header-index calls can access an invalid index. Return database::result_code::other after an explicit null-index and height check.

Proposed fix
 auto const idx = chain.headers().active_at(static_cast<int32_t>(h));
-KTH_ASSERT(idx != header_index::null_index);
-KTH_ASSERT(chain.headers().get_height(idx) == static_cast<int32_t>(h));
+if (idx == header_index::null_index ||
+    chain.headers().get_height(idx) != static_cast<int32_t>(h)) {
+    spdlog::critical("[utxo_builder] Header index has no active block at height {}", h);
+    co_return database::result_code::other;
+}
 auto const file_num = chain.headers().get_file_number(idx);

Based on learnings: KTH_ASSERT is a debug-only contract check and must not guard a Release-build condition that can produce invalid behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blockchain/src/utxo_builder.cpp` around lines 921 - 937, Replace the
KTH_ASSERT checks after active_at() in the reference-mode block with runtime
validation: return database::result_code::other when idx is
header_index::null_index or get_height(idx) does not equal h. Only call
get_file_number(), get_data_pos(), and process_compact_block_utxos() after both
checks pass.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/node/src/sync/block_tasks.cpp`:
- Around line 2199-2208: The block processing flow around
utxo_process_pending_deletions and set_utxo_built_height must not persist
batch_end before deferred deletions succeed. Move or defer the built-height
marker update until after the sweep completes successfully, and ensure failures
leave durable recovery state that prevents a restart from treating the
inconsistent batch as built.

In `@src/node/test/sync_harness.hpp`:
- Around line 175-182: Update the direct get_utxo() error handling in the helper
containing utxo_process_pending_lookups so it throws for every error except
database::result_code::key_not_found. Preserve the deferred lookup sweep for
key_not_found, while propagating other results such as
database::result_code::other instead of treating them as absent.

---

Outside diff comments:
In `@src/blockchain/src/utxo_builder.cpp`:
- Around line 921-937: Replace the KTH_ASSERT checks after active_at() in the
reference-mode block with runtime validation: return
database::result_code::other when idx is header_index::null_index or
get_height(idx) does not equal h. Only call get_file_number(), get_data_pos(),
and process_compact_block_utxos() after both checks pass.

In `@src/database/include/kth/database/databases/utxoz_database.hpp`:
- Around line 222-227: In apply_delta_raw() under KTH_UTXOZ_REFERENCE_MODE,
validate that the insert payload is exactly 8 bytes before the memcpy calls
populating file_number and tx_offset. For any other size, reject the insert and
return the existing insert error without copying or writing the reference.
- Around line 178-208: Propagate erase failures in both batch methods: update
apply_delta() at src/database/include/kth/database/databases/utxoz_database.hpp
lines 178-208 and apply_delta_raw() at lines 222-245 to store the result of
db_->erase(key, height), return its failure as the appropriate result_code, and
only return success when every erase succeeds.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 726e2718-0945-4eca-bfb2-a460597f28c4

📥 Commits

Reviewing files that changed from the base of the PR and between c111aa0 and deb63f6.

📒 Files selected for processing (12)
  • src/blockchain/include/kth/blockchain/interface/block_chain.hpp
  • src/blockchain/include/kth/blockchain/utxo_builder.hpp
  • src/blockchain/src/interface/block_chain.cpp
  • src/blockchain/src/utxo_builder.cpp
  • src/blockchain/src/validate/batch_validate.cpp
  • src/blockchain/test/reorg_undo_roundtrip.cpp
  • src/blockchain/test/utxoz_contract.cpp
  • src/blockchain/test/utxoz_roundtrip.cpp
  • src/database/include/kth/database/databases/utxoz_database.hpp
  • src/database/src/databases/utxoz_database.cpp
  • src/node/src/sync/block_tasks.cpp
  • src/node/test/sync_harness.hpp
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/blockchain/test/utxoz_contract.cpp
  • src/blockchain/test/utxoz_roundtrip.cpp
  • src/database/src/databases/utxoz_database.cpp

Comment thread src/node/src/sync/block_tasks.cpp Outdated
Comment thread src/node/test/sync_harness.hpp
@fpelliccioni
fpelliccioni force-pushed the deps/utxoz-0.9.0-reference-mode branch from deb63f6 to 25e5edb Compare August 10, 2026 14:48
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 31.57895% with 26 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.40%. Comparing base (1ed1b8a) to head (a168ea2).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
.../include/kth/database/databases/utxoz_database.hpp 20.00% 8 Missing ⚠️
src/blockchain/src/populate/populate_base.cpp 25.00% 6 Missing ⚠️
src/blockchain/src/utxo_builder.cpp 20.00% 4 Missing ⚠️
src/blockchain/src/interface/block_chain.cpp 40.00% 3 Missing ⚠️
src/blockchain/src/validate/batch_validate.cpp 40.00% 3 Missing ⚠️
...blockchain/include/kth/blockchain/utxo_builder.hpp 66.66% 1 Missing ⚠️
...c/blockchain/src/populate/populate_transaction.cpp 50.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master     #645       +/-   ##
===========================================
+ Coverage    0.34%   80.40%   +80.06%     
===========================================
  Files         239      289       +50     
  Lines       12621    14538     +1917     
===========================================
+ Hits           43    11689    +11646     
+ Misses      12578     2849     -9729     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@fpelliccioni
fpelliccioni force-pushed the deps/utxoz-0.9.0-reference-mode branch from 25e5edb to aa00f3d Compare August 10, 2026 16:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/blockchain/test/utxoz_contract.cpp (3)

118-119: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the vacuous is_integral_v assertion.

Line 116 already pins erase_result to utxoz::result<size_t>. Line 118 then asserts that this type is not integral, which the preceding assertion guarantees. The check can never fail independently, so it adds no coverage to a file whose purpose is to avoid checks that pass vacuously.

♻️ Proposed removal
     static_assert(std::is_same_v<erase_result, utxoz::result<size_t>>,
         "erase() no longer returns result<size_t>; revisit the failure-vs-absence split");
-    static_assert( ! std::is_integral_v<erase_result>,
-        "erase() returns a bare count again: a failure would read as a missing key");
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blockchain/test/utxoz_contract.cpp` around lines 118 - 119, Remove the
redundant static_assert using std::is_integral_v<erase_result> near the existing
erase_result type assertion, leaving the assertion that pins erase_result to
utxoz::result<size_t> unchanged.

68-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the comment on the tautological static_assert.

The static_assert at lines 70-76 selects both the alias and the expected type from KTH_UTXOZ_REFERENCE_MODE as seen in this translation unit. It cannot detect a CMake disagreement. The comment at lines 176-182 states this correctly, but the comment at lines 68-69 still claims the opposite.

The runtime check in the "this target and the database agree on the mode" test case is the one that detects divergence. Update the comment here to point at it, or remove the assertion.

♻️ Proposed comment fix
-// The mode this build selected must be the mode the wrapper reports. These are
-// set in two CMake lists and read in a third place, so they can disagree.
+// Pins the alias to the mode this translation unit was compiled with. This is a
+// tautology by construction and cannot detect a CMake disagreement; the runtime
+// utxoz_reference_mode() comparison in the "this target and the database agree
+// on the mode" test case is what catches divergent target definitions.
 static_assert(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blockchain/test/utxoz_contract.cpp` around lines 68 - 76, Update the
comment immediately above the static_assert in the UTXOZ mode test so it no
longer claims to detect disagreement between CMake-selected modes; explain that
the assertion only verifies the alias and expected type selected within this
translation unit, and point readers to the runtime “this target and the database
agree on the mode” check for cross-target divergence, or remove the assertion if
that clarification is not useful.

223-234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin the wrapper return type, as the other test cases do.

Line 224 asserts the return type of the library function utxoz::platform_durability(). Line 230 then calls utxoz_platform_durability(), which is the KTH wrapper. The wrapper return type is never asserted, so this test case does not apply the pattern used at lines 219-220 for sync().

Add a static_assert for the wrapper.

♻️ Proposed addition
     static_assert(std::is_same_v<decltype(utxoz::platform_durability()), utxoz::durability_level>,
         "platform_durability() no longer answers with a durability_level");
+
+    static_assert(std::is_same_v<decltype(utxoz_platform_durability()), utxoz::durability_level>,
+        "the wrapper must pass the durability_level up unchanged");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blockchain/test/utxoz_contract.cpp` around lines 223 - 234, Add a
static_assert beside the existing utxoz::platform_durability() assertion to pin
the return type of utxoz_platform_durability() to utxoz::durability_level,
matching the wrapper-type checks used by the neighboring sync() tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/blockchain/test/utxoz_contract.cpp`:
- Around line 378-383: Update the test around utxoz_database::find so reference
mode explicitly requires the lookup to fail, preventing fabricated entries from
passing; retain the existing conditional error-code check for full mode,
including the assertion that failures are not result_code::key_not_found.
- Around line 262-281: Test cleanup is skipped when Catch2 REQUIRE assertions
throw, leaving database handles and temporary directories behind. In
src/blockchain/test/utxoz_contract.cpp#L262-L281, add the anonymous-namespace
scope_exit helper and guard both first->close()—while preserving its required
ordering—and remove_all(dir); apply equivalent guards for db.close() and
remove_all(dir) at src/blockchain/test/utxoz_contract.cpp#L332-L341 and
src/blockchain/test/utxoz_contract.cpp#L352-L392. Ensure each guard executes
during normal completion and assertion unwinding.

---

Nitpick comments:
In `@src/blockchain/test/utxoz_contract.cpp`:
- Around line 118-119: Remove the redundant static_assert using
std::is_integral_v<erase_result> near the existing erase_result type assertion,
leaving the assertion that pins erase_result to utxoz::result<size_t> unchanged.
- Around line 68-76: Update the comment immediately above the static_assert in
the UTXOZ mode test so it no longer claims to detect disagreement between
CMake-selected modes; explain that the assertion only verifies the alias and
expected type selected within this translation unit, and point readers to the
runtime “this target and the database agree on the mode” check for cross-target
divergence, or remove the assertion if that clarification is not useful.
- Around line 223-234: Add a static_assert beside the existing
utxoz::platform_durability() assertion to pin the return type of
utxoz_platform_durability() to utxoz::durability_level, matching the
wrapper-type checks used by the neighboring sync() tests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 55e38862-260d-4444-b669-f3c5ec6cdd75

📥 Commits

Reviewing files that changed from the base of the PR and between deb63f6 and aa00f3d.

⛔ Files ignored due to path filters (2)
  • conan.lock is excluded by !**/*.lock, !conan.lock and included by none
  • conanfile.py is excluded by !**/conanfile.py and included by none
📒 Files selected for processing (6)
  • src/blockchain/include/kth/blockchain/populate/populate_base.hpp
  • src/blockchain/src/populate/populate_base.cpp
  • src/blockchain/src/populate/populate_transaction.cpp
  • src/blockchain/test/utxoz_contract.cpp
  • src/database/include/kth/database/databases/utxoz_database.hpp
  • src/database/src/databases/utxoz_database.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/database/include/kth/database/databases/utxoz_database.hpp
  • src/database/src/databases/utxoz_database.cpp

Comment thread src/blockchain/test/utxoz_contract.cpp Outdated
Comment on lines +262 to +281
auto first = utxoz_db::open_for_testing(dir.string(), true);
REQUIRE(first.has_value());

// The claim is held. A second open of the same path must fail rather than
// hand out a second writer — the whole reason KTH can stop policing this
// itself is that the library now does.
auto second = utxoz_db::open_for_testing(dir.string(), false);
REQUIRE_FALSE(second.has_value());
REQUIRE(second.error() == utxoz::error_code::database_in_use);

first->close();

// ...and once released, the path opens again. Without this half, a library
// that refused every open would pass the assertion above.
auto third = utxoz_db::open_for_testing(dir.string(), false);
REQUIRE(third.has_value());
third->close();

std::filesystem::remove_all(dir);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cleanup is skipped when an assertion fails in three test cases. Each test case writes its cleanup as trailing statements after the REQUIRE calls. Catch2 throws on a failed REQUIRE, so the trailing statements never run. The result is a held database handle and a leaked temporary directory, which can make later failures harder to read.

Introduce one RAII scope guard in the anonymous namespace and use it at each site.

  • src/blockchain/test/utxoz_contract.cpp#L262-L281: guard first->close() and remove_all(dir). This site leaks the exclusive database claim, which blocks any later open of the same path in the same process.
  • src/blockchain/test/utxoz_contract.cpp#L332-L341: guard db.close() and remove_all(dir) around the open-and-empty lookup checks.
  • src/blockchain/test/utxoz_contract.cpp#L352-L392: guard db.close() and remove_all(dir) around the materialisation checks.
🧪 Proposed scope guard

Add the helper to the anonymous namespace:

// Cleanup must run on the throwing path too: a failed Catch2 REQUIRE unwinds,
// and a skipped close() leaves the exclusive claim held for the whole process.
class scope_exit {
public:
    explicit scope_exit(std::function<void()> f) : f_(std::move(f)) {}
    ~scope_exit() { if (f_) f_(); }
    scope_exit(scope_exit const&) = delete;
    scope_exit& operator=(scope_exit const&) = delete;
private:
    std::function<void()> f_;
};

Then apply it at the anchor site:

     auto first = utxoz_db::open_for_testing(dir.string(), true);
     REQUIRE(first.has_value());
+    scope_exit cleanup{[&]{
+        std::error_code ignored;
+        std::filesystem::remove_all(dir, ignored);
+    }};
 
     auto second = utxoz_db::open_for_testing(dir.string(), false);
     REQUIRE_FALSE(second.has_value());
     REQUIRE(second.error() == utxoz::error_code::database_in_use);
 
     first->close();
@@
     auto third = utxoz_db::open_for_testing(dir.string(), false);
     REQUIRE(third.has_value());
     third->close();
-
-    std::filesystem::remove_all(dir);
 }

Note that first->close() must stay inside the body because the test depends on its ordering, so also guard it, for example with a separate flag or by closing in the guard when still open.

📍 Affects 1 file
  • src/blockchain/test/utxoz_contract.cpp#L262-L281 (this comment)
  • src/blockchain/test/utxoz_contract.cpp#L332-L341
  • src/blockchain/test/utxoz_contract.cpp#L352-L392
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blockchain/test/utxoz_contract.cpp` around lines 262 - 281, Test cleanup
is skipped when Catch2 REQUIRE assertions throw, leaving database handles and
temporary directories behind. In
src/blockchain/test/utxoz_contract.cpp#L262-L281, add the anonymous-namespace
scope_exit helper and guard both first->close()—while preserving its required
ordering—and remove_all(dir); apply equivalent guards for db.close() and
remove_all(dir) at src/blockchain/test/utxoz_contract.cpp#L332-L341 and
src/blockchain/test/utxoz_contract.cpp#L352-L392. Ensure each guard executes
during normal completion and assertion unwinding.

Comment thread src/blockchain/test/utxoz_contract.cpp Outdated
@fpelliccioni fpelliccioni changed the title deps: move to UTXO-Z 0.9.0, and to the contracts it changed underneath deps: move to UTXO-Z 0.9.1, and to the contracts it changed underneath Aug 10, 2026
@fpelliccioni
fpelliccioni force-pushed the deps/utxoz-0.9.0-reference-mode branch from aa00f3d to ea60e79 Compare August 10, 2026 19:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/blockchain/test/utxoz_contract.cpp (1)

397-405: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Both branches of this #ifdef assert the same two conditions.

Lines 398-399 and lines 403-404 are identical. The preprocessor branch adds no distinction here, only the comments differ.

Keep one pair of assertions and keep both explanations in the surrounding comment.

♻️ Proposed simplification
     auto const point = utxoz_database::key_to_point(key);
     auto const found = db.find(point, 0);
-#ifdef KTH_UTXOZ_REFERENCE_MODE
+    // Reference mode: the reference points at a block file no store can serve.
+    // Full mode: the stored bytes are not a serialised entry, so decoding fails.
+    // Neither outcome may be reported as the key being absent.
     REQUIRE_FALSE(found.has_value());
     REQUIRE(found.error() != result_code::key_not_found);
-#else
-    // Full mode: the stored bytes are not a serialised entry, so decoding must
-    // fail — and, again, must not be reported as the key being absent.
-    REQUIRE_FALSE(found.has_value());
-    REQUIRE(found.error() != result_code::key_not_found);
-#endif
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/blockchain/test/utxoz_contract.cpp` around lines 397 - 405, Remove the
redundant KTH_UTXOZ_REFERENCE_MODE conditional around the identical REQUIRE
assertions, retaining a single pair of assertions and combining both
reference-mode and full-mode explanations in the surrounding comment.
src/node/src/sync/block_tasks.cpp (1)

2168-2187: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Report the unapplied count from the drain result, not the pre-drain size.

deferred is read before the drain. On a failed drain the queue may be partially applied, so deferred overstates what remains. The message then reports a count that the store does not hold. Also deleted is bound but never used.

Use chain.utxo_deferred_deletions_size() after the failure for the remaining count, and bind only failed.

♻️ Proposed refactor
             auto drained = chain.utxo_process_pending_deletions();
             if ( ! drained) {
                 spdlog::critical("[utxo_build] the deferred deletion queue could not be "
                     "drained at batch {}-{}: {} deletion(s) remain unapplied, so the UTXO "
                     "set still holds outputs these blocks spent", batch_start, batch_end,
-                    deferred);
+                    chain.utxo_deferred_deletions_size());
                 on_fatal("the deferred deletion queue could not be drained");
                 co_return;
             }
-            auto& [deleted, failed] = *drained;
+            auto const& failed = drained->second;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/node/src/sync/block_tasks.cpp` around lines 2168 - 2187, In the deferred
deletion failure path around utxo_process_pending_deletions, report the
remaining queue size by calling chain.utxo_deferred_deletions_size() after the
drain fails instead of using the pre-drain deferred value. Also bind only failed
from the drain result, removing the unused deleted binding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/blockchain/test/utxoz_contract.cpp`:
- Around line 397-405: Remove the redundant KTH_UTXOZ_REFERENCE_MODE conditional
around the identical REQUIRE assertions, retaining a single pair of assertions
and combining both reference-mode and full-mode explanations in the surrounding
comment.

In `@src/node/src/sync/block_tasks.cpp`:
- Around line 2168-2187: In the deferred deletion failure path around
utxo_process_pending_deletions, report the remaining queue size by calling
chain.utxo_deferred_deletions_size() after the drain fails instead of using the
pre-drain deferred value. Also bind only failed from the drain result, removing
the unused deleted binding.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b43a49cf-74ef-44fe-85fd-a08a36328166

📥 Commits

Reviewing files that changed from the base of the PR and between aa00f3d and ea60e79.

📒 Files selected for processing (5)
  • src/blockchain/src/utxo_builder.cpp
  • src/blockchain/test/utxoz_contract.cpp
  • src/database/include/kth/database/databases/utxoz_database.hpp
  • src/node/src/sync/block_tasks.cpp
  • src/node/test/sync_harness.hpp
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/node/test/sync_harness.hpp
  • src/blockchain/src/utxo_builder.cpp
  • src/database/include/kth/database/databases/utxoz_database.hpp

The visible half of this bump is a rename: UTXO-Z's second storage mode is
called reference rather than compact, so compact_db becomes reference_db,
compact_find_result becomes reference_find_result, and the option that selects
it follows. That half is safe because getting it wrong does not compile.

The dangerous half is what four functions now return. process_pending_lookups,
process_pending_deletions and erase went from plain values to result<>, and the
compiler found all three. compact_all did not: it used to return void, so a
caller that kept invoking it as a statement compiled exactly as before while
discarding every failure. That one is the reason this is not a version-number
change.

erase is the one worth reading twice. It returned a count, and the natural
migration writes `*erased > 0 ? success : key_not_found` — which turns "the
erase failed" into "the key was not there", about an entry that may still exist
and still be spendable. Both drains have the same shape: an unreadable catalogue
is not an empty queue, and a caller reads absence as spent.

Also integrated, because they are new rather than changed: sync(), which close()
does not do for you, and platform_durability(), which says what a successful
sync is worth — under contents_only the file contents reach the disk and the
directory entries naming them do not. And the exclusive claim, where open() now
distinguishes another instance holding the database from a claim that could not
be attempted at all; the library's own header notes those send an operator
looking in different places, so they are logged as different things. The error
enum grew from five values to twenty-one, so failures are logged by name — the
numbers moved, and an old note now names a different fault.

Reporting the error was not enough: the first version of this logged it and
returned an empty pair anyway, which is the same defect wearing a log line. The
result now travels through utxoz_database, block_chain and every consumer.
batch_validate stops as a local failure instead of answering
missing_previous_output, which would reject a possibly-valid block on the
strength of a read that never happened; the UTXO builders refuse to advance
rather than reporting success. find() and find_raw() map only
utxoz::error_code::not_found to key_not_found — closed, catalog_unreadable and
recovery_required are read upstream as "queued, sweep it", and a sweep over a
database that cannot be read answers "absent", which the validator reads as
"spent".

process_compact_block_utxos gets an error channel for the same reason. A
negative file_number was guarded by KTH_ASSERT, which evaporates in Release and
leaves -1 to become UINT32_MAX in every reference the block writes. That is not
an impossible precondition — the header index and the block store are separate
files and a crash can leave them disagreeing — so it is reported, and both
callers stop the batch rather than storing entries that can never be resolved.

Eight negative controls assert the shape of these contracts rather than the
behaviour of a database, because a runtime test only fails after the wrong call
has been written and run, and they reach Knuth's own layers rather than only
UTXO-Z's types. Each is proven to fire: reverting compact() to its 0.8.0 form
fails the build with "utxoz_database::compact() must report whether compaction
happened"; returning void from block_chain::utxo_compact() fails with "the
failure dies between the wrapper and every caller"; and making the database
library report the opposite storage mode fails the coherence test with
"database library reports reference, this target was compiled as full". That
last one replaces a check that asked the same macro twice and could only ever
agree with itself — the option is declared in two CMake lists, so the answer
worth comparing is the one compiled into the database library against the one
compiled here.

The compaction OPERATION keeps its name throughout, and so do the things that
merely contain the word: BIP152 compact blocks, history_compact, stealth_compact,
chain::compact, CompactSize, and secp256k1's compact signatures. Each rename
rule names an exact identifier rather than the substring, so none of those could
be caught by accident.

Full mode remains the default, in the recipe and in both CMake lists. Reference
mode is compiled and tested here as a check: it is the half of the #ifdef that
nothing in CI builds, so a rename that broke it would go unnoticed until someone
turned the flag on.

One review comment is deliberately not addressed here. open() takes the path as
std::string_view, and path.string() converts through the active code page on
Windows, so a data directory with non-ASCII characters cannot be opened. UTXO-Z
0.9.0 exposes no std::filesystem::path or wide overload, and substituting
u8string() would be guessing at how the library reconstructs the path on the
other side. Reported as utxo-z/utxo-z#109 instead of patched blind.

Three further review points are fixed here. A reference the sweep FOUND but
could not materialise, and stored bytes that will not decode, are both reads
that failed against entries that demonstrably exist; neither is now demoted into
the not-found list or dropped silently. And the negative file_number check is
gated on reference mode, which is the only mode that stores that field — full
mode keeps the whole output and never reads it.

0.9.0 could not have landed. It documented the second list returned by
process_pending_lookups() as UNRESOLVED, not absent: a version file that could
not be read left its keys there, indistinguishable from keys that exist nowhere,
and Knuth reads that list as "spent". Nothing on this side could separate the
two. 0.9.1 closes it upstream — the sweep now fails with version_unreadable
rather than dropping unreachable keys into the answer, so the second list is
absence that was proven, and the failure arrives as an error through the
result<> chain this PR already wired.

Which makes a comment in this file wrong in a useful way: it claimed the
exhaustive switch over error_code would stop compiling when UTXO-Z added a code.
0.9.1 added version_unreadable and this built clean — the -Wswitch that would
have warned is neither enabled nor promoted here. The case was added by hand,
and the comment now says that adding one is a manual step on every bump.

populate_prevout() carried the same defect one layer out: get_utxo() already
returned expected<output_info, result_code> and every failure was read as
absence, which then probed the mempool. A miss there is indistinguishable from a
prevout that does not exist, so a disk that did not answer became a verdict
about the transaction. Only key_not_found now enables that fallback; anything
else returns error::operation_failed, and the chain from populate_prevout
through populate_inputs_sync to populate() carries it to whoever decides. That
chain needed the error channel: populate_prevout returned void and
populate_inputs_sync returned error::success unconditionally, so changing the
`if` alone would have dropped the answer one frame later.

The three behavioural controls close the gap the type assertions left: a
signature can be right while the body is wrong. Each has an exact negation and a
positive that stops it passing by always failing. find() on a closed database
must not answer key_not_found, and on an open empty one it must — otherwise an
implementation returning `other` for everything would pass the negation and
destroy the mempool fallback. An entry the store holds must never come back as
absent when materialising or decoding it fails, and the raw payload is asserted
present so the case is about the read and not an empty database. A negative file
number fails in reference mode and builds in full, with a valid number building
in both.

A later review round found the same "log it and carry on" shape in four more
places. apply_delta() and apply_delta_raw() discarded db_->erase() entirely, so
a storage failure ended in result_code::success and a batch was recorded as
applied while the outputs it spent were still in the set; not_found stays
tolerated, since a delta can legitimately delete a pruned output. apply_delta_raw
read the 8-byte reference with two memcpys off a caller-supplied buffer without
checking its length, so a short payload read past its end and a long one was
truncated into a reference pointing elsewhere. The sync harness swept the
deferred queue on any get_utxo error rather than only key_not_found. And
utxo_builder trusted KTH_ASSERT for a null header index, which is compiled out
in Release, where it would have gone on to read a file number from it —
block_tasks already refused at the same fork.

The ordering defect is the one that did not need a failure to bite. block_tasks
persisted utxo_built_height = batch_end and drained the deferred deletions
afterwards, though the comment beside it claimed the opposite order. A crash
between the two left a marker saying the batch was complete over a set that
still held every output those blocks spent, and the restart trusted the marker.
The drain now runs first, which closes that window in one direction: a crash
after the drain and before the marker replays the batch, which is safe. The
other direction is what #600 specifies and #602 implements — a durable record of
which of the two happened, so recovery does not infer it.
@fpelliccioni
fpelliccioni force-pushed the deps/utxoz-0.9.0-reference-mode branch from ea60e79 to a168ea2 Compare August 10, 2026 19:16
@fpelliccioni
fpelliccioni merged commit 6018863 into master Aug 10, 2026
32 checks passed
@fpelliccioni
fpelliccioni deleted the deps/utxoz-0.9.0-reference-mode branch August 10, 2026 19:54
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.

1 participant