Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.List;
import org.apache.logging.log4j.LogManager;
Expand All @@ -30,6 +32,11 @@
* hasher state) and a recent wrapped record hashes file, validates their consistency,
* computes the Merkle block hashes for the range, and writes the results back to state.
*
* <p>If a jumpstart actually runs and successfully computes a {@link Result} above, {@link #execute}
* also truncates the on-disk wrapped record hashes file to empty (when writing to it is enabled),
* since that data has now been consumed and folded into the result. This must stay downstream of
* the read above.
*
* <p>TODO: Delete this in the release after receiving/injecting the jumpstart historical hashes data.
*/
public class WrappedRecordBlockHashMigration {
Expand Down Expand Up @@ -61,7 +68,9 @@
"Resuming calculation of wrapped record file hashes until next attempt";

/**
* Executes the wrapped record block hash migration if enabled.
* Executes the wrapped record block hash migration if enabled, then truncates the on-disk
* wrapped record hashes file to empty if a jumpstart actually ran and consumed it (and writing
* to the file is enabled).
*
* @param streamMode the current stream mode
* @param recordsConfig the block record stream configuration
Expand All @@ -78,6 +87,20 @@
requireNonNull(recordsConfig);
requireNonNull(jumpstartConfig);

try {
runJumpstartMigration(streamMode, recordsConfig, jumpstartConfig, migrationAlreadyApplied);
} finally {
if (result != null) {
truncateHashesFileIfWritingEnabled(recordsConfig);
}
}
}

private void runJumpstartMigration(
@NonNull final StreamMode streamMode,
@NonNull final BlockRecordStreamConfig recordsConfig,
@NonNull final BlockStreamJumpstartConfig jumpstartConfig,
final boolean migrationAlreadyApplied) {
if (migrationAlreadyApplied) {
if (jumpstartConfig.blockNum() < 0) {
log.info("Jumpstart migration already applied (votingComplete=true) and no jumpstart config, skipping");
Expand All @@ -100,6 +123,31 @@
}
}

/**
* Truncates the on-disk wrapped record hashes file to empty when writing to it is enabled. Only
* called when a jumpstart actually ran and consumed the file's contents this execution (see
* {@link #execute}), since that is the data this truncation would otherwise discard.
*/
private void truncateHashesFileIfWritingEnabled(@NonNull final BlockRecordStreamConfig recordsConfig) {
if (!recordsConfig.writeWrappedRecordFileBlockHashesToDisk()) {
return;
}
if (isBlank(recordsConfig.wrappedRecordHashesDir())) {
return;

Check warning on line 136 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/WrappedRecordBlockHashMigration.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/WrappedRecordBlockHashMigration.java#L136

Added line #L136 was not covered by tests
}
final var file = Paths.get(recordsConfig.wrappedRecordHashesDir())
.resolve(WrappedRecordFileBlockHashesDiskWriter.DEFAULT_FILE_NAME);
if (!Files.exists(file)) {
return;
}
try (final var ignored =
Files.newByteChannel(file, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
log.info("Truncated wrapped record hashes file {} now that the jumpstart migration has run", file);
} catch (final IOException e) {
log.warn("Failed to truncate wrapped record hashes file {}", file, e);

Check warning on line 147 in hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/WrappedRecordBlockHashMigration.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-app/src/main/java/com/hedera/node/app/records/impl/WrappedRecordBlockHashMigration.java#L146-L147

Added lines #L146 - L147 were not covered by tests
}
}

private void executeInternal(
@NonNull final BlockRecordStreamConfig recordsConfig,
@NonNull final BlockStreamJumpstartConfig jumpstartConfig)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,104 @@ void loadRecentHashesParsesLargeFileWithoutException() throws Exception {
assertThat(result.entries().size()).isEqualTo(entryCount);
}

@Test
void truncatesFileWhenWritingEnabledAfterSuccessfulMigration() throws Exception {
final List<WrappedRecordFileBlockHashes> entries = new ArrayList<>();
for (long i = 90; i <= 100; i++) {
entries.add(entry(i));
}
final var recentHashesDir = createRecentHashesDir(entries);
final var file = recentHashesDir.resolve(WrappedRecordFileBlockHashesDiskWriter.DEFAULT_FILE_NAME);
final var config = recordsConfigWith(RECORDS, true, b -> b.withValue(
"hedera.recordStream.wrappedRecordHashesDir", recentHashesDir.toString())
.withValue("hedera.recordStream.writeWrappedRecordFileBlockHashesToDisk", true));

subject.execute(StreamMode.RECORDS, config, jumpstartConfig(98, 4, 1), false);

// The migration itself must have read the pre-truncation file contents successfully.
assertThat(subject.result()).isNotNull();
assertThat(Files.exists(file)).isTrue();
assertThat(Files.size(file)).isZero();
}

@Test
void doesNotTruncateFileWhenJumpstartNotConfigured() throws Exception {
final List<WrappedRecordFileBlockHashes> entries = new ArrayList<>();
for (long i = 90; i <= 100; i++) {
entries.add(entry(i));
}
final var recentHashesDir = createRecentHashesDir(entries);
final var file = recentHashesDir.resolve(WrappedRecordFileBlockHashesDiskWriter.DEFAULT_FILE_NAME);
final var config = recordsConfigWith(RECORDS, true, b -> b.withValue(
"hedera.recordStream.wrappedRecordHashesDir", recentHashesDir.toString())
.withValue("hedera.recordStream.writeWrappedRecordFileBlockHashesToDisk", true));

// No jumpstart config populated (blockNum defaults to -1), so the migration itself is a no-op
// and must not touch the file, even though writing is enabled.
subject.execute(StreamMode.RECORDS, config, defaultJumpstartConfig(), false);

assertNull(subject.result());
assertThat(Files.size(file)).isGreaterThan(0L);
}

@Test
void doesNotTruncateFileWhenValidationFailsSoJumpstartDoesNotRun() throws Exception {
final var config = enabledRecordsConfig(createRecentHashesDir(List.of(entry(100), entry(101))));
final var file =
tempDir.resolve("recent-hashes").resolve(WrappedRecordFileBlockHashesDiskWriter.DEFAULT_FILE_NAME);
// previousWrappedRecordBlockHash has the wrong length, so validation fails before any hashes are computed.
final var badConfig = new BlockStreamJumpstartConfig(
100,
Bytes.wrap(new byte[32]),
4,
1,
List.of(Bytes.wrap(new byte[HASH_SIZE])),
Bytes.wrap(new byte[HASH_SIZE]),
Bytes.wrap(new byte[HASH_SIZE]));

subject.execute(StreamMode.RECORDS, config, badConfig, false);

assertNull(subject.result());
assertThat(Files.size(file)).isGreaterThan(0L);
}

@Test
void doesNotTruncateFileWhenWritingDisabled() throws Exception {
final List<WrappedRecordFileBlockHashes> entries = new ArrayList<>();
for (long i = 90; i <= 100; i++) {
entries.add(entry(i));
}
final var recentHashesDir = createRecentHashesDir(entries);
final var file = recentHashesDir.resolve(WrappedRecordFileBlockHashesDiskWriter.DEFAULT_FILE_NAME);
final var config = recordsConfigWith(RECORDS, true, b -> b.withValue(
"hedera.recordStream.wrappedRecordHashesDir", recentHashesDir.toString())
.withValue("hedera.recordStream.writeWrappedRecordFileBlockHashesToDisk", false));

subject.execute(StreamMode.RECORDS, config, jumpstartConfig(98, 4, 1), false);

assertThat(subject.result()).isNotNull();
assertThat(Files.size(file)).isGreaterThan(0L);
}

@Test
void truncateHelperDoesNotFailWhenFileMissing() throws Exception {
final var config = recordsConfigWith(RECORDS, true, b -> b.withValue(
"hedera.recordStream.wrappedRecordHashesDir",
tempDir.resolve("missing-dir").toString())
.withValue("hedera.recordStream.writeWrappedRecordFileBlockHashesToDisk", true));

final Method truncate = WrappedRecordBlockHashMigration.class.getDeclaredMethod(
"truncateHashesFileIfWritingEnabled", BlockRecordStreamConfig.class);
truncate.setAccessible(true);
assertDoesNotThrow(() -> {
try {
truncate.invoke(subject, config);
} catch (final java.lang.reflect.InvocationTargetException e) {
throw e.getCause();
}
});
}

private Path createRecentHashesDir(List<WrappedRecordFileBlockHashes> entries) throws Exception {
final var dir = tempDir.resolve("recent-hashes");
Files.createDirectories(dir);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public record BlockRecordStreamConfig(
@ConfigProperty(defaultValue = "concurrent") @NetworkProperty
String streamFileProducer,

@ConfigProperty(defaultValue = "false") @NetworkProperty
@ConfigProperty(defaultValue = "true") @NetworkProperty
boolean writeWrappedRecordFileBlockHashesToDisk,

@ConfigProperty(defaultValue = "/opt/hgcapp/wrappedRecordHashes") @NodeProperty
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -959,20 +959,18 @@ public static GetWrappedRecordHashesOp getWrappedRecordHashes(
}

/**
* Verifies the node's jumpstart hash computation via three-way comparison:
* file entries, .rcd replay, and the node's logged hash.
* Verifies the node's jumpstart hash computation by independently replaying {@code .rcd} files
* from the jumpstart block through the freeze block and comparing against the node's logged hash.
*
* @param jumpstartConfig the jumpstart config properties
* @param wrappedHashes per-block entries from the wrapped record hashes file
* @param nodeComputedHash the hash the node logged during migration
* @param freezeBlockNum the last block the migration processed
*/
public static VerifyJumpstartHashOp verifyJumpstartHash(
@NonNull final BlockStreamJumpstartConfig jumpstartConfig,
@NonNull final List<WrappedRecordFileBlockHashes> wrappedHashes,
@NonNull final String nodeComputedHash,
@NonNull final String freezeBlockNum) {
return new VerifyJumpstartHashOp(jumpstartConfig, wrappedHashes, nodeComputedHash, freezeBlockNum);
return new VerifyJumpstartHashOp(jumpstartConfig, nodeComputedHash, freezeBlockNum);
}

/**
Expand Down
Loading
Loading