Skip to content

Commit bbc6e0a

Browse files
committed
refactor(domain): turn header into an immutable value type
The old header inherited from header_basis, mixed in hash_memoizer to cache the block hash, and carried a mutable `validation` struct holding height and median_time_past stamped from outside. The cache was a thread hazard, the inheritance leaked basis internals, and the validation struct was a side-channel for height/MTP that the value type should never own. Replace it with two interchangeable value types selected at compile time via `KTH_USE_HEADER_MEMBERS`: - header_raw (default): 80-byte contiguous storage, fields decoded on access. Wins on the IBD hot path — faster deserialization, zero-copy raw access for wire-handling code. - header_members: traditional struct, field-per-member. Wins on repeated full-record access (wallets, explorers). Both expose the same surface plus a free function `chain::hash(header)` in lieu of the cached `header.hash()` member. The validation side-channel is gone: block_pool and branch now read height and MTP off the block's chain_state, which the validator already populates. block_organizer no longer stamps the header. C-API regenerated via kth-tools (no hand edits). The generator picks up `chain::hash(header const&)` automatically thanks to a new `extra_namespaces=['kth::domain::chain']` on the header config, so `kth_chain_header_hash` is exposed as a normal accessor. Caught while wiring up the tests, fixed in passing: - block_entry::parent() returned `hash_digest const&` but the new header decodes previous_block_hash from raw bytes into a temporary — every caller was holding a dangling reference. - Same dangle in header_organizer.cpp and internal_database test. Old files (header.cpp, header_basis.cpp, hash_memoizer.hpp, original message/header.{hpp,cpp}) are renamed to `.disabled` so any stragglers trip the compiler instead of silently linking against legacy code.
1 parent 3dbae80 commit bbc6e0a

52 files changed

Lines changed: 2881 additions & 631 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/blockchain/include/kth/blockchain/pools/block_entry.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ struct KB_API block_entry {
3434
hash_digest const& hash() const;
3535

3636
/// The hash table entry's parent (preceding block) hash.
37-
hash_digest const& parent() const;
37+
/// Returned by value: the new header value type decodes
38+
/// `previous_block_hash()` from raw bytes into a temporary, so a
39+
/// reference would dangle.
40+
hash_digest parent() const;
3841

3942
/// The hash table entry's child (succeeding block) hashes.
4043
hash_list const& children() const;

src/blockchain/src/interface/block_chain.cpp

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ bool block_chain::start(uint32_t disk_magic) {
185185
// Only genesis in DB - just add genesis to index
186186
auto const genesis = get_header(0);
187187
if (genesis) {
188-
auto const hash = genesis->hash();
188+
auto const hash = domain::chain::hash(*genesis);
189189
auto const [inserted, idx, capacity_warning] = header_index_.add(hash, *genesis);
190190
if ( ! inserted) {
191191
spdlog::error("[blockchain] Failed to initialize header index with genesis block.");
@@ -214,7 +214,7 @@ bool block_chain::start(uint32_t disk_magic) {
214214

215215
size_t height = from;
216216
for (auto const& header : *headers_result) {
217-
auto const hash = header.hash();
217+
auto const hash = domain::chain::hash(header);
218218
auto const [inserted, idx, capacity_warning] = header_index_.add(hash, header);
219219
if ( ! inserted && height > 0) {
220220
// Genesis might already be added, ignore that case
@@ -404,7 +404,7 @@ ::asio::awaitable<code> block_chain::store_chunk(
404404
// Each block writes to its own index entry — safe without locks.
405405
for (size_t i = 0; i < n; ++i) {
406406
auto const& block = blocks[i];
407-
auto const block_hash = block->header().hash();
407+
auto const block_hash = domain::chain::hash(block->header());
408408
auto const idx = header_index_.find(block_hash);
409409
if (idx != header_index::null_index) {
410410
auto const& data_pos = (*positions)[i];
@@ -535,7 +535,7 @@ ::asio::awaitable<code> block_chain::store_block(
535535
}
536536

537537
// 2. Update header_index with block file position
538-
auto const block_hash = block->header().hash();
538+
auto const block_hash = domain::chain::hash(block->header());
539539
auto const idx = header_index_.find(block_hash);
540540
if (idx != header_index::null_index) {
541541
header_index_.set_block_pos(idx, static_cast<int16_t>(pos.file), pos.pos);
@@ -867,7 +867,7 @@ std::expected<hash_digest, database::result_code> block_chain::get_block_hash(si
867867
if ( ! result) {
868868
return std::unexpected(result.error());
869869
}
870-
return result->hash();
870+
return domain::chain::hash(*result);
871871
}
872872

873873
bool block_chain::header_exists(hash_digest const& block_hash) const {
@@ -1111,7 +1111,7 @@ block_chain::fetch_block_hash_timestamp(size_t height) const {
11111111
co_return std::unexpected(error::not_found);
11121112
}
11131113

1114-
co_return std::tuple{result->hash(), result->timestamp(), height};
1114+
co_return std::tuple{domain::chain::hash(*result), result->timestamp(), height};
11151115
}
11161116

11171117
// DEPRECATED: Block storage moved to flat files (blk*.dat)
@@ -1299,7 +1299,7 @@ block_chain::fetch_locator_block_hashes(get_blocks_const_ptr locator,
12991299
break;
13001300
}
13011301
static auto const id = inventory::type_id::block;
1302-
hashes->inventories().emplace_back(id, result->hash());
1302+
hashes->inventories().emplace_back(id, domain::chain::hash(*result));
13031303
}
13041304

13051305
co_return hashes;
@@ -1369,7 +1369,7 @@ block_chain::fetch_block_locator(block::indexes const& heights) const {
13691369
if ( ! result) {
13701370
co_return std::unexpected(error::not_found);
13711371
}
1372-
hashes.push_back(result->hash());
1372+
hashes.push_back(domain::chain::hash(*result));
13731373
}
13741374

13751375
co_return message;

src/blockchain/src/pools/block_entry.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ hash_digest const& block_entry::hash() const {
3030
}
3131

3232
// Not callable if the entry is a search key.
33-
hash_digest const& block_entry::parent() const {
33+
hash_digest block_entry::parent() const {
3434
KTH_ASSERT(block_);
3535
return block_->header().previous_block_hash();
3636
}

src/blockchain/src/pools/block_organizer.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,10 @@ ::asio::awaitable<code> block_organizer::organize(block_const_ptr block, bool he
168168

169169
auto& top_block = branch->top()->validation;
170170
top_block.error = error::success;
171-
172-
auto& top_header = branch->top()->header().validation;
173-
top_header.median_time_past = top_block.state->median_time_past();
174-
top_header.height = branch->top_height();
171+
// The header used to carry mutable validation.median_time_past /
172+
// .height stamped here. The header is now a pure value type; both
173+
// values are already available on the block's chain_state and the
174+
// branch's top_height(), so we no longer mutate the header.
175175

176176
auto const work = branch->work();
177177
auto const first_height = branch->height() + 1u;

src/blockchain/src/pools/block_pool.cpp

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,14 @@ void block_pool::add(block_const_ptr valid_block) {
3131
////KTH_ASSERT( ! block->validation.error);
3232
block_entry entry{ valid_block };
3333

34-
// Not all blocks will have validation state.
35-
////KTH_ASSERT(block->validation.state);
36-
auto height = valid_block->header().validation.height;
34+
// Height previously came from header().validation.height; the header
35+
// is now a pure value type, so we read it from the block's chain_state
36+
// which is populated by the validator before the block reaches the pool.
37+
// Fall back to 0 to preserve the legacy semantics when an unvalidated
38+
// block (no chain_state) is added — e.g. dedup-only test paths.
39+
auto height = valid_block->validation.state
40+
? static_cast<size_t>(valid_block->validation.state->height())
41+
: size_t{0};
3742
auto const& left = blocks_.left;
3843

3944
// Caller ensure the entry does not exist by using get_path, but
@@ -94,7 +99,8 @@ void block_pool::remove(block_const_ptr_list_const_ptr accepted_blocks) {
9499

95100
// Copy the entry so that it can be deleted and replanted with height.
96101
auto const copy = it->first;
97-
auto const height = copy.block()->header().validation.height;
102+
KTH_ASSERT(copy.block()->validation.state);
103+
auto const height = static_cast<size_t>(copy.block()->validation.state->height());
98104
KTH_ASSERT(it->second == 0);
99105

100106
// Critical Section
@@ -116,7 +122,9 @@ void block_pool::prune(hash_list const& hashes, size_t minimum_height) {
116122
auto const it = left.find(block_entry{ hash });
117123
KTH_ASSERT(it != left.end());
118124

119-
auto const height = it->first.block()->header().validation.height;
125+
KTH_ASSERT(it->first.block()->validation.state);
126+
auto const height = static_cast<size_t>(
127+
it->first.block()->validation.state->height());
120128

121129
// Delete all roots and expired non-roots and recurse their children.
122130
if (it->second != 0 || height < minimum_height) {

src/blockchain/src/pools/branch.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,12 @@ uint32_t branch::height_at(size_t index) const {
124124
// private
125125
uint32_t branch::median_time_past_at(size_t index) const {
126126
KTH_ASSERT(index < size());
127-
return (*blocks_)[index]->header().validation.median_time_past;
127+
// MTP used to live on header.validation.median_time_past; the header
128+
// is now a pure value type, so we read it from the block's chain_state
129+
// (populated by the validator before the block reaches the branch).
130+
auto const& block = *(*blocks_)[index];
131+
KTH_ASSERT(block.validation.state);
132+
return block.validation.state->median_time_past();
128133
}
129134

130135
// TODO(legacy): absorb into the main chain for speed and code consolidation.

src/blockchain/src/pools/header_organizer.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ header_organize_result header_organizer::add_headers(domain::message::header::li
109109
// it matches an older block in our chain. If so, these are stale/duplicate headers
110110
// that can be silently skipped.
111111
if (!headers.empty()) {
112-
auto const& first_prev_hash = headers.front().previous_block_hash();
112+
auto const first_prev_hash = headers.front().previous_block_hash();
113113
spdlog::debug("[header_organizer] First header prev_hash: {}",
114114
encode_hash(first_prev_hash));
115115

@@ -142,7 +142,7 @@ header_organize_result header_organizer::add_headers(domain::message::header::li
142142
for (auto const& header : headers) {
143143
// Compute hash first (needed for validation)
144144
KTH_STATS_TIME_START(hash);
145-
auto const hash = header.hash();
145+
auto const hash = domain::chain::hash(header);
146146
KTH_STATS_TIME_END(global_sync_stats(), hash, hash_time_ns, hash_calls);
147147

148148
// Validate header with full chain-state validation

src/blockchain/src/validate/validate_header.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ code validate_header::check(domain::chain::header const& header) const {
3333
// Delegate to header's check() which validates:
3434
// 1. Proof of work (hash <= target derived from bits)
3535
// 2. Timestamp not too far in future (2 hours)
36-
return header.check(retarget_);
36+
auto const hash = domain::chain::hash(header);
37+
return header.check(hash, retarget_);
3738
}
3839

3940
// Chain-context validation
@@ -48,7 +49,7 @@ code validate_header::accept(domain::chain::header const& header, size_t height,
4849
return error::store_block_missing_parent;
4950
}
5051

51-
auto const hash = header.hash();
52+
auto const hash = domain::chain::hash(header);
5253

5354
// Validate against checkpoints
5455
if (is_checkpoint_conflict(hash, height)) {

src/blockchain/test/block_entry.cpp

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,9 @@ TEST_CASE("block entry construct2 default block hash round trips", "[block en
3636

3737
TEST_CASE("block entry parent hash42 expected", "[block entry tests]")
3838
{
39-
auto const block = std::make_shared<domain::message::block>();
40-
block->header().set_previous_block_hash(hash42);
39+
domain::chain::header const hdr{0, hash42, null_hash, 0, 0, 0};
40+
auto const block = std::make_shared<domain::message::block>(
41+
hdr, domain::chain::transaction::list{});
4142
block_entry instance(block);
4243
REQUIRE(instance.parent() == hash42);
4344
}
@@ -65,8 +66,9 @@ TEST_CASE("block entry add child two expected order", "[block entry tests]")
6566
auto const child1 = std::make_shared<const domain::message::block>();
6667
instance.add_child(child1);
6768

68-
auto const child2 = std::make_shared<domain::message::block>();
69-
child2->header().set_previous_block_hash(hash42);
69+
domain::chain::header const hdr{0, hash42, null_hash, 0, 0, 0};
70+
auto const child2 = std::make_shared<domain::message::block>(
71+
hdr, domain::chain::transaction::list{});
7072
instance.add_child(child2);
7173

7274
REQUIRE(instance.children().size() == 2u);

src/blockchain/test/block_index_bench/benchmarks.cpp

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,21 @@ class block_pool_fixture : public kth::blockchain::block_pool {
7474

7575
// Create a minimal block with just a header (no transactions)
7676
kth::block_const_ptr create_kth_block(kth::hash_digest const& prev_hash, uint32_t height) {
77-
// Create header with the given previous hash
78-
kth::domain::chain::header hdr;
79-
hdr.set_version(1);
80-
hdr.set_previous_block_hash(prev_hash);
81-
hdr.set_merkle(kth::null_hash); // Empty merkle
82-
hdr.set_timestamp(1231006505 + height * 600); // ~10 min per block
83-
hdr.set_bits(0x1d00ffff);
84-
hdr.set_nonce(height);
85-
86-
// Set validation height (required by block_pool)
87-
hdr.validation.height = height;
88-
89-
// Create block with empty transactions (just header)
77+
// Header is now a value type with no setters and no validation struct.
78+
// Pass every field through the 6-arg ctor.
79+
kth::domain::chain::header const hdr{
80+
/*version*/ 1u,
81+
/*prev*/ prev_hash,
82+
/*merkle*/ kth::null_hash,
83+
/*timestamp*/ uint32_t(1231006505u + height * 600u), // ~10 min per block
84+
/*bits*/ 0x1d00ffffu,
85+
/*nonce*/ height,
86+
};
87+
88+
// The previous benchmark stamped height onto hdr.validation; block_pool
89+
// now reads height from block.validation.state. This benchmark exercises
90+
// the header_index path which does not consult block.validation.state,
91+
// so no extra setup is needed here.
9092
auto block = std::make_shared<kth::domain::message::block>(
9193
hdr,
9294
kth::domain::chain::transaction::list{}

0 commit comments

Comments
 (0)