Skip to content

Commit f9aa0cc

Browse files
committed
feat: Revisit Hash On-Init for AllBlocksHasher
* Move the `AllPreviousBlocksRootHashHasherSnapshot` to internal protos * All blocks hasher general cleanup and hardening * Configuration property for optional hashing from history * Remove time-based `AllPreviousBlocksRootHashHasherSnapshot` persistence and make it block based (configurable) * Consolidate verification additions from #2535 and #2666 Signed-off-by: Atanas Atanasov <a.v.atanasov98@gmail.com>
1 parent 5cb1dfe commit f9aa0cc

15 files changed

Lines changed: 811 additions & 189 deletions

File tree

block-node/block-verification/src/main/java/module-info.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
requires org.hiero.block.node.app.config;
2222
requires org.hiero.block.node.base;
2323
requires com.github.spotbugs.annotations;
24-
requires org.antlr.antlr4.runtime;
2524

2625
provides org.hiero.block.node.spi.BlockNodePlugin with
2726
VerificationServicePlugin;

block-node/block-verification/src/main/java/org/hiero/block/node/verification/AllBlocksHasherHandler.java

Lines changed: 169 additions & 140 deletions
Large diffs are not rendered by default.

block-node/block-verification/src/main/java/org/hiero/block/node/verification/VerificationConfig.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@
1212
///
1313
/// @param allBlocksHasherFilePath path to the root hash file for all previous blocks
1414
/// @param allBlocksHasherEnabled whether the all-blocks hasher is enabled
15+
/// @param rebuildAllBlocksHasherFromStore whether to rebuild the all-blocks hasher from the store in case we are not
16+
/// starting from genesis or we have no available persisted data
1517
/// @param allBlocksHasherPersistenceInterval how often (in blocks) the hasher persists its state
1618
/// @param tssParametersFilePath path where TSS parameters (ledger ID, address book, WRAPS VK)
1719
/// are persisted across restarts as a serialized `LedgerIdPublicationTransactionBody`.
1820
/// Written when block 0 is processed. Loaded on startup to restore full TSS state.
1921
@ConfigData("verification")
2022
public record VerificationConfig(
21-
@Loggable @ConfigProperty(defaultValue = "/opt/hiero/block-node/verification/rootHashOfAllPreviousBlocks.bin") Path allBlocksHasherFilePath,
22-
@Loggable @ConfigProperty(defaultValue = "true") boolean allBlocksHasherEnabled,
23-
@Loggable @ConfigProperty(defaultValue = "10") int allBlocksHasherPersistenceInterval,
24-
@Loggable @ConfigProperty(defaultValue = "/opt/hiero/block-node/verification/tss-parameters.bin") Path tssParametersFilePath) {}
23+
@Loggable @ConfigProperty(defaultValue = "/opt/hiero/block-node/verification/rootHashOfAllPreviousBlocks.bin") Path allBlocksHasherFilePath,
24+
@Loggable @ConfigProperty(defaultValue = "false") boolean allBlocksHasherEnabled,
25+
@Loggable @ConfigProperty(defaultValue = "false") boolean rebuildAllBlocksHasherFromStore,
26+
@Loggable @ConfigProperty(defaultValue = "100") int allBlocksHasherPersistenceInterval,
27+
@Loggable @ConfigProperty(defaultValue = "/opt/hiero/block-node/verification/tss-parameters.bin") Path tssParametersFilePath) {}
2528

2629
// spotless:on

block-node/block-verification/src/main/java/org/hiero/block/node/verification/VerificationServicePlugin.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,6 @@ public void init(BlockNodeContext context, ServiceBuilder serviceBuilder) {
167167
// initialize metrics
168168
initMetrics(context);
169169
LOGGER.log(DEBUG, "VerificationServicePlugin initialized successfully.");
170-
// initialize all previous blocks hasher if enabled and available
171-
initAllBlocksHasherIfEnabled();
172170
}
173171

174172
private void initAllBlocksHasherIfEnabled() {
@@ -195,13 +193,14 @@ private void initAllBlocksHasherIfEnabled() {
195193
/// {@inheritDoc}
196194
@Override
197195
public void start() {
196+
// initialize and start all previous blocks hasher if enabled and available
197+
initAllBlocksHasherIfEnabled();
198+
allBlocksHasherHandler.start();
198199
// register to listen to incoming block items
199200
// specify that we are cpu intensive and should be run on a separate non-virtual thread
200201
context.blockMessaging().registerBlockItemHandler(this, true, VerificationServicePlugin.class.getSimpleName());
201202
// we do not need to unregister the handler as it will be unregistered when the message service is stopped
202203
LOGGER.log(DEBUG, "VerificationServicePlugin started successfully.");
203-
204-
allBlocksHasherHandler.start();
205204
}
206205

207206
// ==== BlockItemHandler Methods ===================================================================================
@@ -281,7 +280,7 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
281280
this.previousVerifiedBlockNumber = currentBlockNumber;
282281
// Update streamingHasherAllPreviousBlocks
283282
allBlocksHasherHandler.appendLatestHashToAllPreviousBlocksStreamingHasher(
284-
this.previousBlockHash.toByteArray());
283+
this.previousBlockHash.toByteArray(), notification.blockNumber());
285284
} else {
286285
LOGGER.log(INFO, "Verification failed for block={0}", currentBlockNumber);
287286
sendFailureNotification(currentBlockNumber, BlockSource.PUBLISHER);
@@ -308,17 +307,22 @@ public void handleBlockItemsReceived(BlockItems blockItems) {
308307
}
309308

310309
private Bytes getRootOfAllPreviousBlocks() {
310+
final Bytes rootHash;
311311
if (allBlocksHasherHandler != null && allBlocksHasherHandler.isAvailable()) {
312312
if (allBlocksHasherHandler.getNumberOfBlocks() != currentBlockNumber
313313
&& (currentBlockNumber <= previousVerifiedBlockNumber || previousVerifiedBlockNumber == -1)) {
314314
// Backfill, re-send, or startup: defer to the block footer's values.
315315
// Forward gaps fall through to computeRootHash() instead — returning the wrong
316316
// root so verification fails and forces the publisher to resend the missing block.
317-
return null;
317+
rootHash = null;
318+
} else {
319+
final byte[] computedRootHash = allBlocksHasherHandler.computeRootHash();
320+
rootHash = computedRootHash == null ? null : Bytes.wrap(computedRootHash);
318321
}
319-
return Bytes.wrap(allBlocksHasherHandler.computeRootHash());
322+
} else {
323+
rootHash = null;
320324
}
321-
return null;
325+
return rootHash;
322326
}
323327

324328
private void persistTssParameters() {
@@ -420,7 +424,7 @@ public void handleBackfilled(BackfilledBlockNotification notification) {
420424
this.previousBlockHash = backfillNotification.blockHash();
421425
this.previousVerifiedBlockNumber = notification.blockNumber();
422426
allBlocksHasherHandler.appendLatestHashToAllPreviousBlocksStreamingHasher(
423-
backfillNotification.blockHash().toByteArray());
427+
backfillNotification.blockHash().toByteArray(), backfillNotification.blockNumber());
424428
}
425429
}
426430
// send the verification notification for the backfilled block

block-node/block-verification/src/main/java/org/hiero/block/node/verification/session/impl/ExtendedMerkleTreeSession.java

Lines changed: 213 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,18 @@
22
package org.hiero.block.node.verification.session.impl;
33

44
import static java.lang.System.Logger.Level.INFO;
5+
import static java.lang.System.Logger.Level.WARNING;
56
import static org.hiero.block.common.hasher.HashingUtilities.getBlockItemHash;
7+
import static org.hiero.block.common.hasher.HashingUtilities.hashInternalNode;
8+
import static org.hiero.block.common.hasher.HashingUtilities.hashInternalNodeSingleChild;
9+
import static org.hiero.block.common.hasher.HashingUtilities.hashLeaf;
610
import static org.hiero.block.common.hasher.HashingUtilities.noThrowSha384HashOf;
711

812
import com.hedera.cryptography.tss.TSS;
913
import com.hedera.hapi.block.stream.BlockProof;
14+
import com.hedera.hapi.block.stream.MerklePath;
15+
import com.hedera.hapi.block.stream.SiblingNode;
16+
import com.hedera.hapi.block.stream.StateProof;
1017
import com.hedera.hapi.block.stream.output.BlockFooter;
1118
import com.hedera.hapi.block.stream.output.BlockHeader;
1219
import com.hedera.hapi.node.transaction.SignedTransaction;
@@ -163,15 +170,30 @@ public VerificationNotification finalizeVerification(Bytes rootHashOfAllBlockHas
163170
// while we don't have state management, use the start of block state root hash from the footer
164171
Bytes startOfBlockStateRootHash = this.blockFooter.startOfBlockStateRootHash();
165172

166-
// for now, we only support TSS based signature proofs, we expect only 1 of these.
167-
// @todo(2019) extend to support other proof types as well
168-
BlockProof tssBasedProof = getSingle(blockProofs, BlockProof::hasSignedBlockProof);
169-
if (tssBasedProof != null) {
170-
return getVerificationResult(
171-
tssBasedProof, previousBlockHashToUse, rootOfAllPreviousBlockHashes, startOfBlockStateRootHash);
173+
try {
174+
// Try direct TSS proof first (most common case)
175+
BlockProof tssBasedProof = getSingle(blockProofs, BlockProof::hasSignedBlockProof);
176+
if (tssBasedProof != null) {
177+
return getVerificationResult(
178+
tssBasedProof, previousBlockHashToUse, rootOfAllPreviousBlockHashes, startOfBlockStateRootHash);
179+
}
180+
181+
// Try indirect (state) proof — used when TSS signing was delayed
182+
BlockProof stateBasedProof = getSingle(blockProofs, BlockProof::hasBlockStateProof);
183+
if (stateBasedProof != null) {
184+
return getStateProofVerificationResult(
185+
stateBasedProof,
186+
previousBlockHashToUse,
187+
rootOfAllPreviousBlockHashes,
188+
startOfBlockStateRootHash);
189+
}
190+
} catch (final IllegalStateException e) {
191+
LOGGER.log(WARNING, "Block [{0}] has malformed proof structure: %s".formatted(e.getMessage()), e);
192+
return new VerificationNotification(
193+
false, FailureType.BAD_BLOCK_PROOF, blockNumber, null, null, blockSource);
172194
}
173195

174-
// was not able to finalize verification yet
196+
// No supported proof type found
175197
return null;
176198
}
177199

@@ -227,6 +249,184 @@ protected Boolean verifySignature(@NonNull Bytes hash, @NonNull Bytes signature)
227249
}
228250
}
229251

252+
/// Verifies a block using an indirect (state) proof. A state proof proves that the target
253+
/// block's root hash is embedded in a later directly-signed block's merkle tree, forming
254+
/// a chain: target block hash -> merkle siblings -> signed block root -> TSS signature.
255+
///
256+
/// @param blockProof the block proof containing a state proof
257+
/// @param previousBlockHash the previous block hash
258+
/// @param rootOfAllPreviousBlockHashes the root hash of all previous block hashes
259+
/// @param startOfBlockStateRootHash the start of block state root hash
260+
/// @return VerificationNotification indicating the result of the verification
261+
protected VerificationNotification getStateProofVerificationResult(
262+
BlockProof blockProof,
263+
Bytes previousBlockHash,
264+
Bytes rootOfAllPreviousBlockHashes,
265+
Bytes startOfBlockStateRootHash) {
266+
267+
final Bytes blockRootHash = HashingUtilities.computeFinalBlockHash(
268+
blockHeader.blockTimestamp(),
269+
previousBlockHash,
270+
rootOfAllPreviousBlockHashes,
271+
startOfBlockStateRootHash,
272+
inputTreeHasher,
273+
outputTreeHasher,
274+
consensusHeaderHasher,
275+
stateChangesHasher,
276+
traceDataHasher);
277+
278+
final boolean verified = verifyStateProof(blockRootHash, blockProof.blockStateProof());
279+
280+
return new VerificationNotification(
281+
verified,
282+
verified ? null : FailureType.BAD_BLOCK_PROOF,
283+
blockNumber,
284+
blockRootHash,
285+
verified ? new BlockUnparsed(blockItems) : null,
286+
blockSource);
287+
}
288+
289+
/// Walks the merkle path chain in a state proof to reconstruct the directly-signed block's
290+
/// root hash, then verifies the TSS signature on that root.
291+
///
292+
/// A valid block state proof contains 3 paths:
293+
///
294+
/// - Path 0: timestamp leaf of the signed block
295+
/// - Path 1: sibling hashes from the target block through all `gap` (i.e. not directly signed) blocks to
296+
/// the signed block
297+
/// - Path 2: terminal (empty)
298+
///
299+
///
300+
/// Path 1's siblings are laid out as groups:
301+
///
302+
/// - Per gap block (4 siblings): prevBlockRootsHash (right), depth5Node2 (right), depth4Node2 (right),
303+
/// _already-hashed_ timestamp (left)
304+
/// - Final signed block (3 siblings): prevBlockRootsHash (right), depth5Node2 (right), depth4Node2 (right)
305+
///
306+
///
307+
/// @param blockRootHash the computed root hash of the target block
308+
/// @param stateProof the state proof to verify
309+
/// @return true if the proof chain and TSS signature are valid
310+
protected boolean verifyStateProof(@NonNull Bytes blockRootHash, @NonNull StateProof stateProof) {
311+
List<MerklePath> paths = stateProof.paths();
312+
if (paths.size() != 3) {
313+
LOGGER.log(WARNING, "Block {0} state proof has {1} paths, expected 3", blockNumber, paths.size());
314+
return false;
315+
}
316+
317+
MerklePath timestampPath = paths.get(0);
318+
MerklePath siblingPath = paths.get(1);
319+
MerklePath terminalPath = paths.get(2);
320+
321+
if (!terminalPath.siblings().isEmpty() || terminalPath.hasTimestampLeaf()) {
322+
LOGGER.log(WARNING, "Block {0} state proof path 2 (terminal) is unexpectedly non-empty", blockNumber);
323+
return false;
324+
}
325+
326+
if (!timestampPath.hasTimestampLeaf()) {
327+
LOGGER.log(WARNING, "Block {0} state proof path 0 (timestamp) is missing timestamp leaf", blockNumber);
328+
return false;
329+
}
330+
if (!stateProof.hasSignedBlockProof()) {
331+
LOGGER.log(WARNING, "Block {0} state proof is missing signed block proof", blockNumber);
332+
return false;
333+
}
334+
335+
List<SiblingNode> siblings = siblingPath.siblings();
336+
int totalSiblings = siblings.size();
337+
// Must have at least 3 (signed block siblings) and remainder must be groups of 4 (gap blocks)
338+
if (totalSiblings < 3 || (totalSiblings - 3) % 4 != 0) {
339+
LOGGER.log(
340+
WARNING,
341+
"Block {0} state proof sibling count {1} is invalid (need >= 3, remainder must be multiple of 4)",
342+
blockNumber,
343+
totalSiblings);
344+
return false;
345+
}
346+
// Starting hash must be present and non-empty to avoid propagating incorrect hashes
347+
if (!siblingPath.hasHash() || siblingPath.hash().length() == 0) {
348+
LOGGER.log(
349+
WARNING, "Block {0} state proof path 1 (sibling) has missing or empty starting hash", blockNumber);
350+
return false;
351+
}
352+
for (SiblingNode sibling : siblings) {
353+
if (sibling.hash().length() == 0) {
354+
LOGGER.log(WARNING, "Block {0} state proof contains a sibling node with an empty hash", blockNumber);
355+
return false;
356+
}
357+
}
358+
359+
// siblingPath.hash() is the previous block's (T-1) root hash. Walking the siblings in groups
360+
// first reconstructs the target block T's root hash, then each subsequent gap block's root hash,
361+
// until we reach the signed block. After the first iteration, current equals T's reconstructed
362+
// root hash — verify it matches the independently computed blockRootHash to ensure block content integrity.
363+
byte[] current = siblingPath.hash().toByteArray();
364+
int index = 0;
365+
boolean firstIteration = true;
366+
367+
// Process gap blocks (including the target block itself) — each contributes 4 siblings
368+
while (totalSiblings - index > 3) {
369+
SiblingNode prevBlockRootsHash = siblings.get(index);
370+
SiblingNode depth5Node2Sibling = siblings.get(index + 1);
371+
SiblingNode depth4Node2Sibling = siblings.get(index + 2);
372+
SiblingNode hashedTimestampSibling = siblings.get(index + 3);
373+
374+
byte[] depth5Node1 = combineSibling(current, prevBlockRootsHash);
375+
byte[] depth4Node1 = combineSibling(depth5Node1, depth5Node2Sibling);
376+
byte[] depth3Node1 = combineSibling(depth4Node1, depth4Node2Sibling);
377+
byte[] depth2Node2 = hashInternalNodeSingleChild(depth3Node1);
378+
// The timestamp sibling hash is already hashLeaf(rawTimestamp) — a 48-byte node value.
379+
// Combine as depth2Node1 (left) with depth2Node2 (right) to reconstruct the gap block root.
380+
current = hashInternalNode(hashedTimestampSibling.hash().toByteArray(), depth2Node2);
381+
382+
if (firstIteration) {
383+
// current now holds the reconstructed target block root hash; verify it matches
384+
// the hash computed from the actual block content we received.
385+
final Bytes reconstructed = Bytes.wrap(current);
386+
if (!blockRootHash.equals(reconstructed)) {
387+
LOGGER.log(
388+
WARNING,
389+
"Block {0} state proof integrity check failed: hash reconstructed from path 1 siblings"
390+
+ " [{1}] does not match block root hash computed from block content [{2}]",
391+
blockNumber,
392+
reconstructed,
393+
blockRootHash);
394+
return false;
395+
}
396+
firstIteration = false;
397+
}
398+
399+
index += 4;
400+
}
401+
402+
// Process the signed block's 3 siblings
403+
byte[] depth5Node1 = combineSibling(current, siblings.get(index));
404+
byte[] depth4Node1 = combineSibling(depth5Node1, siblings.get(index + 1));
405+
byte[] depth3Node1 = combineSibling(depth4Node1, siblings.get(index + 2));
406+
407+
// Reconstruct the signed block's root hash using Path 0's raw timestamp bytes
408+
byte[] depth2Node2 = hashInternalNodeSingleChild(depth3Node1);
409+
byte[] hashedTimestampLeaf = hashLeaf(timestampPath.timestampLeaf().toByteArray());
410+
byte[] signedBlockRoot = hashInternalNode(hashedTimestampLeaf, depth2Node2);
411+
412+
// Verify TSS signature on the signed block's root hash
413+
return verifySignature(
414+
Bytes.wrap(signedBlockRoot), stateProof.signedBlockProof().blockSignature());
415+
}
416+
417+
/// Combines the current hash with a sibling node, respecting the sibling's position.
418+
///
419+
/// @param current the current hash being walked up the tree
420+
/// @param sibling the sibling node with position and hash
421+
/// @return the parent hash
422+
private static byte[] combineSibling(byte[] current, SiblingNode sibling) {
423+
if (sibling.isLeft()) {
424+
return hashInternalNode(sibling.hash().toByteArray(), current);
425+
} else {
426+
return hashInternalNode(current, sibling.hash().toByteArray());
427+
}
428+
}
429+
230430
/// Extracts a [LedgerIdPublicationTransactionBody] from a signed transaction, if present.
231431
/// Pure parsing — no side effects.
232432
///
@@ -255,14 +455,13 @@ private LedgerIdPublicationTransactionBody findLedgerIdPublication(Bytes signedT
255455
return body.ledgerIdPublicationOrThrow();
256456
}
257457

258-
public static <T> T getSingle(List<T> list, Predicate<T> predicate) {
458+
/// Returns the single element matching the predicate, or null if none match.
459+
private <T> T getSingle(List<T> list, Predicate<T> predicate) {
259460
List<T> filtered = list.stream().filter(predicate).toList();
260-
261-
if (filtered.size() != 1) {
262-
throw new IllegalStateException(String.format(
263-
"Expected exactly 1 element matching predicate [%s], but found %d.", predicate, filtered.size()));
461+
if (filtered.size() > 1) {
462+
throw new IllegalStateException(
463+
"Expected at most 1 element matching predicate, but found " + filtered.size());
264464
}
265-
266-
return filtered.get(0);
465+
return filtered.isEmpty() ? null : filtered.get(0);
267466
}
268467
}

0 commit comments

Comments
 (0)