#include "data/LedgerCache.hpp" #include "data/impl/LedgerCacheFile.hpp" #include "util/NameGenerator.hpp" #include "util/TmpFile.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace data::impl; struct LedgerCacheFileTestBase : ::testing::Test { struct DataSizeParams { size_t mapEntries; size_t deletedEntries; size_t blobSize; std::string description; }; enum class CorruptionType { InvalidVersion, CorruptedSeparator1, // After header CorruptedSeparator2, // After map hash CorruptedSeparator3, // After deleted hash MapKeyCorrupted, MapSeqCorrupted, MapBlobSizeCorrupted, MapBlobDataCorrupted, DeletedKeyCorrupted, DeletedSeqCorrupted, DeletedBlobSizeCorrupted, DeletedBlobDataCorrupted, HeaderLatestSeqCorrupted }; struct CorruptionParams { CorruptionType type; std::string description; }; struct EntryOffsets { size_t keyOffset; size_t seqOffset; size_t blobSizeOffset; size_t blobDataOffset; }; struct FileOffsets { size_t headerOffset; size_t separator1Offset; size_t mapStartOffset; std::vector mapEntries; size_t separator2Offset; size_t deletedStartOffset; std::vector deletedEntries; size_t separator3Offset; size_t hashOffset; static FileOffsets calculate(LedgerCacheFile::DataView const& dataView) { FileOffsets offsets{}; size_t currentOffset = 0; offsets.headerOffset = currentOffset; currentOffset += sizeof(LedgerCacheFile::Header); offsets.separator1Offset = currentOffset; currentOffset += 16; // Map entries offsets.mapStartOffset = currentOffset; for (auto const& [key, entry] : dataView.map) { EntryOffsets entryOffsets{}; entryOffsets.keyOffset = currentOffset; entryOffsets.seqOffset = currentOffset + 32; // uint256 size entryOffsets.blobSizeOffset = currentOffset + 32 + 4; // + uint32 size entryOffsets.blobDataOffset = currentOffset + 32 + 4 + 8; // + size_t size offsets.mapEntries.push_back(entryOffsets); currentOffset += 32 + 4 + 8 + entry.blob.size(); // key + seq + size + blob } // Separator 2 (after map entries) offsets.separator2Offset = currentOffset; currentOffset += 16; // Deleted entries offsets.deletedStartOffset = currentOffset; for (auto const& [key, entry] : dataView.deleted) { EntryOffsets entryOffsets{}; entryOffsets.keyOffset = currentOffset; entryOffsets.seqOffset = currentOffset + 32; entryOffsets.blobSizeOffset = currentOffset + 32 + 4; entryOffsets.blobDataOffset = currentOffset + 32 + 4 + 8; offsets.deletedEntries.push_back(entryOffsets); currentOffset += 32 + 4 + 8 + entry.blob.size(); } // Separator 3 (after deleted entries) offsets.separator3Offset = currentOffset; currentOffset += 16; // Overall file hash offsets.hashOffset = currentOffset; return offsets; } }; ~LedgerCacheFileTestBase() override { auto const pathWithNewPrefix = fmt::format("{}.new", tmpFile.path); if (std::filesystem::exists(pathWithNewPrefix)) std::filesystem::remove(pathWithNewPrefix); } static std::vector const kDATA_SIZE_PARAMS; static std::vector const kCORRUPTION_PARAMS; TmpFile tmpFile = TmpFile::empty(); static uint32_t constexpr kLATEST_SEQUENCE = 12345; static LedgerCacheFile::Data createTestData(size_t mapSize, size_t deletedSize, size_t blobSize) { LedgerCacheFile::Data data; data.latestSeq = kLATEST_SEQUENCE; for (size_t i = 0; i < mapSize; ++i) { ripple::uint256 key; std::memset(key.data(), static_cast(i), ripple::uint256::size()); data::LedgerCache::CacheEntry entry; entry.seq = static_cast(1000 + i); entry.blob.resize(blobSize); std::memset(entry.blob.data(), static_cast(i + 100), blobSize); data.map.emplace(key, std::move(entry)); } for (size_t i = 0; i < deletedSize; ++i) { ripple::uint256 key; std::memset(key.data(), static_cast(i + 200), ripple::uint256::size()); data::LedgerCache::CacheEntry entry; entry.seq = static_cast(2000 + i); entry.blob.resize(blobSize); std::memset(entry.blob.data(), static_cast(i + 250), blobSize); data.deleted.emplace(key, std::move(entry)); } return data; } static LedgerCacheFile::DataView toDataView(LedgerCacheFile::Data const& data) { return LedgerCacheFile::DataView{ .latestSeq = data.latestSeq, .map = data.map, .deleted = data.deleted }; } void corruptFile(CorruptionType type, LedgerCacheFile::DataView const& dataView) const { std::fstream file(tmpFile.path, std::ios::in | std::ios::out | std::ios::binary); ASSERT_TRUE(file.is_open()); auto const offsets = FileOffsets::calculate(dataView); switch (type) { case CorruptionType::InvalidVersion: file.seekp(offsets.headerOffset); { uint32_t invalidVersion = 999; file.write( reinterpret_cast(&invalidVersion), sizeof(invalidVersion) ); } break; case CorruptionType::CorruptedSeparator1: file.seekp(offsets.separator1Offset); { char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::CorruptedSeparator2: file.seekp(offsets.separator2Offset); { char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::CorruptedSeparator3: file.seekp(offsets.separator3Offset); { char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::MapKeyCorrupted: if (!offsets.mapEntries.empty()) { file.seekp(offsets.mapEntries[0].keyOffset); char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::MapSeqCorrupted: if (!offsets.mapEntries.empty()) { file.seekp(offsets.mapEntries[0].seqOffset); uint32_t const corruptSeq = std::numeric_limits::max(); file.write(reinterpret_cast(&corruptSeq), sizeof(corruptSeq)); } break; case CorruptionType::MapBlobSizeCorrupted: if (!offsets.mapEntries.empty()) { file.seekp(offsets.mapEntries[0].blobSizeOffset); size_t const corruptSize = std::numeric_limits::max(); file.write(reinterpret_cast(&corruptSize), sizeof(corruptSize)); } break; case CorruptionType::MapBlobDataCorrupted: if (!offsets.mapEntries.empty() && !dataView.map.begin()->second.blob.empty()) { file.seekp(offsets.mapEntries[0].blobDataOffset); char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::DeletedKeyCorrupted: if (!offsets.deletedEntries.empty()) { file.seekp(offsets.deletedEntries[0].keyOffset); char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::DeletedSeqCorrupted: if (!offsets.deletedEntries.empty()) { file.seekp(offsets.deletedEntries[0].seqOffset); uint32_t const corruptSeq = std::numeric_limits::max(); file.write(reinterpret_cast(&corruptSeq), sizeof(corruptSeq)); } break; case CorruptionType::DeletedBlobSizeCorrupted: if (!offsets.deletedEntries.empty()) { file.seekp(offsets.deletedEntries[0].blobSizeOffset); size_t const corruptSize = std::numeric_limits::max(); file.write(reinterpret_cast(&corruptSize), sizeof(corruptSize)); } break; case CorruptionType::DeletedBlobDataCorrupted: if (!offsets.deletedEntries.empty() && !dataView.deleted.begin()->second.blob.empty()) { file.seekp(offsets.deletedEntries[0].blobDataOffset); char const corruptByte = static_cast(0xFF); file.write(&corruptByte, 1); } break; case CorruptionType::HeaderLatestSeqCorrupted: file.seekp(offsets.headerOffset + sizeof(uint32_t)); // skip version { uint32_t corruptSeq = 0; // set to 0 to fail validation if minLatestSequence > 0 file.write(reinterpret_cast(&corruptSeq), sizeof(corruptSeq)); } break; } } static void verifyDataEquals(LedgerCacheFile::Data const& expected, LedgerCacheFile::Data const& actual) { EXPECT_EQ(expected.latestSeq, actual.latestSeq); EXPECT_EQ(expected.map.size(), actual.map.size()); EXPECT_EQ(expected.deleted.size(), actual.deleted.size()); for (auto const& [key, entry] : expected.map) { auto it = actual.map.find(key); ASSERT_NE(it, actual.map.end()) << "Key not found in actual map"; EXPECT_EQ(entry.seq, it->second.seq); EXPECT_EQ(entry.blob, it->second.blob); } for (auto const& [key, entry] : expected.deleted) { auto it = actual.deleted.find(key); ASSERT_NE(it, actual.deleted.end()) << "Key not found in actual deleted"; EXPECT_EQ(entry.seq, it->second.seq); EXPECT_EQ(entry.blob, it->second.blob); } } }; std::vector< LedgerCacheFileTestBase::DataSizeParams> const LedgerCacheFileTestBase::kDATA_SIZE_PARAMS = { {.mapEntries = 0, .deletedEntries = 0, .blobSize = 0, .description = "empty"}, {.mapEntries = 1, .deletedEntries = 0, .blobSize = 10, .description = "single_map_small_blob"}, {.mapEntries = 0, .deletedEntries = 1, .blobSize = 100, .description = "single_deleted_medium_blob"}, {.mapEntries = 5, .deletedEntries = 3, .blobSize = 1000, .description = "multiple_entries_large_blob"}, {.mapEntries = 10, .deletedEntries = 10, .blobSize = 50000, .description = "many_entries_huge_blob"} }; std::vector const LedgerCacheFileTestBase::kCORRUPTION_PARAMS = { {.type = CorruptionType::InvalidVersion, .description = "invalid_version"}, {.type = CorruptionType::CorruptedSeparator1, .description = "corrupted_separator1"}, {.type = CorruptionType::CorruptedSeparator2, .description = "corrupted_separator2"}, {.type = CorruptionType::CorruptedSeparator3, .description = "corrupted_separator3"}, {.type = CorruptionType::MapKeyCorrupted, .description = "map_key_corrupted"}, {.type = CorruptionType::MapSeqCorrupted, .description = "map_seq_corrupted"}, {.type = CorruptionType::MapBlobSizeCorrupted, .description = "map_blob_size_corrupted"}, {.type = CorruptionType::MapBlobDataCorrupted, .description = "map_blob_data_corrupted"}, {.type = CorruptionType::DeletedKeyCorrupted, .description = "deleted_key_corrupted"}, {.type = CorruptionType::DeletedSeqCorrupted, .description = "deleted_seq_corrupted"}, {.type = CorruptionType::DeletedBlobSizeCorrupted, .description = "deleted_blob_size_corrupted"}, {.type = CorruptionType::DeletedBlobDataCorrupted, .description = "deleted_blob_data_corrupted"}, {.type = CorruptionType::HeaderLatestSeqCorrupted, .description = "header_latest_seq_corrupted"} }; struct LedgerCacheFileTest : LedgerCacheFileTestBase, ::testing::WithParamInterface { static std::string roundTripParamName(::testing::TestParamInfo const& info) { return info.param.description; } }; INSTANTIATE_TEST_SUITE_P( AllDataSizes, LedgerCacheFileTest, ::testing::ValuesIn(LedgerCacheFileTestBase::kDATA_SIZE_PARAMS), LedgerCacheFileTest::roundTripParamName ); TEST_P(LedgerCacheFileTest, WriteAndReadData) { auto dataParams = GetParam(); LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(dataParams.mapEntries, dataParams.deletedEntries, dataParams.blobSize); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()) << "Failed to write: " << writeResult.error(); EXPECT_TRUE(std::filesystem::exists(tmpFile.path)); EXPECT_GT(std::filesystem::file_size(tmpFile.path), 0u); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()) << "Failed to read: " << readResult.error(); verifyDataEquals(testData, readResult.value()); } struct LedgerCacheFileCorruptionTest : LedgerCacheFileTestBase, ::testing::WithParamInterface { static std::string corruptionParamName(::testing::TestParamInfo const& info) { return info.param.description; } }; INSTANTIATE_TEST_SUITE_P( AllCorruptions, LedgerCacheFileCorruptionTest, ::testing::ValuesIn(LedgerCacheFileTestBase::kCORRUPTION_PARAMS), LedgerCacheFileCorruptionTest::corruptionParamName ); TEST_P(LedgerCacheFileCorruptionTest, HandleCorruption) { auto corruptionParams = GetParam(); LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(3, 2, 100); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()) << "Failed to write: " << writeResult.error(); corruptFile(corruptionParams.type, dataView); auto readResult = cacheFile.read(0); EXPECT_FALSE(readResult.has_value()) << "Should have failed to read corrupted file"; std::string const& error = readResult.error(); switch (corruptionParams.type) { case CorruptionType::InvalidVersion: EXPECT_THAT(error, ::testing::HasSubstr("wrong version")); break; case CorruptionType::CorruptedSeparator1: case CorruptionType::CorruptedSeparator2: case CorruptionType::CorruptedSeparator3: EXPECT_THAT(error, ::testing::HasSubstr("Separator verification failed")); break; case CorruptionType::MapKeyCorrupted: case CorruptionType::MapSeqCorrupted: EXPECT_FALSE(error.empty()); break; case CorruptionType::MapBlobSizeCorrupted: EXPECT_THAT( error, ::testing::AnyOf( ::testing::HasSubstr("Error reading cache file"), ::testing::HasSubstr("Failed to read blob"), ::testing::HasSubstr("Hash file corruption detected") ) ); break; case CorruptionType::MapBlobDataCorrupted: EXPECT_THAT( error, ::testing::AnyOf( ::testing::HasSubstr("Hash file corruption detected"), ::testing::HasSubstr("Error reading cache file") ) ); break; case CorruptionType::DeletedKeyCorrupted: case CorruptionType::DeletedSeqCorrupted: EXPECT_FALSE(error.empty()); break; case CorruptionType::DeletedBlobSizeCorrupted: EXPECT_THAT( error, ::testing::AnyOf( ::testing::HasSubstr("Error reading cache file"), ::testing::HasSubstr("Failed to read blob"), ::testing::HasSubstr("Hash file corruption detected") ) ); break; case CorruptionType::DeletedBlobDataCorrupted: EXPECT_THAT( error, ::testing::AnyOf( ::testing::HasSubstr("Hash file corruption detected"), ::testing::HasSubstr("Error reading cache file") ) ); break; case CorruptionType::HeaderLatestSeqCorrupted: EXPECT_THAT(error, ::testing::HasSubstr("Hash file corruption detected")); break; } } struct LedgerCacheFileEdgeCaseTest : LedgerCacheFileTestBase {}; TEST_F(LedgerCacheFileEdgeCaseTest, NonExistingFile) { LedgerCacheFile invalidPathFile("/invalid/path/file.cache"); auto testData = createTestData(1, 1, 10); auto dataView = toDataView(testData); auto writeResult = invalidPathFile.write(dataView); EXPECT_FALSE(writeResult.has_value()); EXPECT_THAT(writeResult.error(), ::testing::HasSubstr("Couldn't open file")); auto readResult = invalidPathFile.read(0); EXPECT_FALSE(readResult.has_value()); EXPECT_THAT(readResult.error(), ::testing::HasSubstr("Couldn't open file")); } TEST_F(LedgerCacheFileEdgeCaseTest, MaxSequenceNumber) { LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(1, 1, 10); testData.latestSeq = std::numeric_limits::max(); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, ZeroSizedBlobs) { LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(3, 2, 0); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, SpecialKeyPatterns) { LedgerCacheFile cacheFile(tmpFile.path); LedgerCacheFile::Data testData; testData.latestSeq = 100; ripple::uint256 zeroKey; std::memset(zeroKey.data(), 0, ripple::uint256::size()); testData.map.emplace(zeroKey, data::LedgerCache::CacheEntry{.seq = 1, .blob = {1, 2, 3}}); ripple::uint256 onesKey; std::memset(onesKey.data(), 0xFF, ripple::uint256::size()); testData.map.emplace(onesKey, data::LedgerCache::CacheEntry{.seq = 2, .blob = {4, 5, 6}}); ripple::uint256 altKey; for (size_t i = 0; i < ripple::uint256::size(); ++i) { altKey.data()[i] = static_cast(((i % 2) != 0u) ? 0xAA : 0x55); } testData.deleted.emplace(altKey, data::LedgerCache::CacheEntry{.seq = 3, .blob = {7, 8, 9}}); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, LargeBlobs) { LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(1, 1, 1024 * 1024); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, SequenceNumber) { LedgerCacheFile cacheFile(tmpFile.path); LedgerCacheFile::Data testData; testData.latestSeq = 0; ripple::uint256 key1, key2, key3; std::memset(key1.data(), 1, ripple::uint256::size()); std::memset(key2.data(), 2, ripple::uint256::size()); std::memset(key3.data(), 3, ripple::uint256::size()); testData.map.emplace(key1, data::LedgerCache::CacheEntry{.seq = 0, .blob = {1}}); testData.map.emplace( key2, data::LedgerCache::CacheEntry{.seq = std::numeric_limits::max(), .blob = {2}} ); testData.deleted.emplace( key3, data::LedgerCache::CacheEntry{.seq = std::numeric_limits::max() / 2, .blob = {3}} ); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, OnlyMapEntries) { LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(5, 0, 100); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, OnlyDeletedEntries) { LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(0, 5, 100); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(0); ASSERT_TRUE(readResult.has_value()); verifyDataEquals(testData, readResult.value()); } TEST_F(LedgerCacheFileEdgeCaseTest, WriteCreatesFileWithSuffixNew) { // The test causes failure of rename operation by creating destination as directory std::filesystem::remove(tmpFile.path); std::filesystem::create_directory(tmpFile.path); LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(1, 1, 10); auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); EXPECT_FALSE(writeResult.has_value()); auto newFilePath = fmt::format("{}.new", tmpFile.path); EXPECT_THAT(writeResult.error(), ::testing::HasSubstr(newFilePath)); EXPECT_TRUE(std::filesystem::exists(newFilePath)); EXPECT_TRUE(std::filesystem::is_regular_file(newFilePath)); } struct LedgerCacheFileMinSequenceValidationParams { uint32_t latestSeq; uint32_t minLatestSeq; bool shouldSucceed; std::string testName; }; struct LedgerCacheFileMinSequenceValidationTest : LedgerCacheFileTestBase, ::testing::WithParamInterface {}; INSTANTIATE_TEST_SUITE_P( LedgerCacheFileMinSequenceValidationTests, LedgerCacheFileMinSequenceValidationTest, ::testing::Values( LedgerCacheFileMinSequenceValidationParams{ .latestSeq = 1000u, .minLatestSeq = 500u, .shouldSucceed = true, .testName = "accept_when_min_less_than_latest" }, LedgerCacheFileMinSequenceValidationParams{ .latestSeq = 1000u, .minLatestSeq = 2000u, .shouldSucceed = false, .testName = "reject_when_min_greater_than_latest" }, LedgerCacheFileMinSequenceValidationParams{ .latestSeq = 1000u, .minLatestSeq = 1000u, .shouldSucceed = true, .testName = "accept_when_min_equals_latest" }, LedgerCacheFileMinSequenceValidationParams{ .latestSeq = 0u, .minLatestSeq = 0u, .shouldSucceed = true, .testName = "accept_zero_sequence" } ), tests::util::kNAME_GENERATOR ); TEST_P(LedgerCacheFileMinSequenceValidationTest, ValidateMinSequence) { auto const params = GetParam(); auto const latestSeq = params.latestSeq; auto const minLatestSeq = params.minLatestSeq; auto const shouldSucceed = params.shouldSucceed; LedgerCacheFile cacheFile(tmpFile.path); auto testData = createTestData(3, 2, 100); testData.latestSeq = latestSeq; auto dataView = toDataView(testData); auto writeResult = cacheFile.write(dataView); ASSERT_TRUE(writeResult.has_value()); auto readResult = cacheFile.read(minLatestSeq); if (shouldSucceed) { ASSERT_TRUE(readResult.has_value()) << "Expected read to succeed but got error: " << readResult.error(); EXPECT_EQ(readResult.value().latestSeq, latestSeq); } else { EXPECT_FALSE(readResult.has_value()) << "Expected read to fail but it succeeded"; EXPECT_THAT(readResult.error(), ::testing::HasSubstr("too low")); } }