|
4 | 4 |
|
5 | 5 | #include "bucket/BucketManager.h" |
6 | 6 | #include "bucket/test/BucketTestUtils.h" |
| 7 | +#include "catchup/ApplyCheckpointWork.h" |
7 | 8 | #include "catchup/DownloadApplyTxsWork.h" |
8 | 9 | #include "catchup/LedgerApplyManagerImpl.h" |
9 | 10 | #include "catchup/test/CatchupWorkTests.h" |
| 11 | +#include "crypto/SHA.h" |
10 | 12 | #include "herder/HerderPersistence.h" |
11 | 13 | #include "history/CheckpointBuilder.h" |
12 | 14 | #include "history/FileTransferInfo.h" |
|
26 | 28 | #include "test/test.h" |
27 | 29 | #include "util/Fs.h" |
28 | 30 | #include "util/Logging.h" |
| 31 | +#include "util/ProtocolVersion.h" |
29 | 32 | #include "work/WorkScheduler.h" |
30 | 33 |
|
31 | 34 | #include "historywork/BatchDownloadWork.h" |
@@ -1127,6 +1130,203 @@ TEST_CASE("History catchup with extra validation", "[history][publish]") |
1127 | 1130 | REQUIRE(catchupSimulation.catchupOffline(app, checkpointLedger, true)); |
1128 | 1131 | } |
1129 | 1132 |
|
| 1133 | +#ifdef CAP_0083 |
| 1134 | +TEST_CASE("History catchup over empty-tx-set ledgers", "[history][catchup]") |
| 1135 | +{ |
| 1136 | + CatchupSimulation catchupSimulation{}; |
| 1137 | + |
| 1138 | + // Generate a few random ledgers |
| 1139 | + while ( |
| 1140 | + catchupSimulation.getApp().getLedgerManager().getLastClosedLedgerNum() < |
| 1141 | + 8) |
| 1142 | + { |
| 1143 | + catchupSimulation.generateRandomLedger(); |
| 1144 | + } |
| 1145 | + |
| 1146 | + // One isolated empty-tx-set ledger, a normal ledger, then two consecutive |
| 1147 | + // empty-tx-set ledgers, all within the first published checkpoint. |
| 1148 | + catchupSimulation.generateEmptyTxSetLedger(); |
| 1149 | + catchupSimulation.generateRandomLedger(); |
| 1150 | + catchupSimulation.generateEmptyTxSetLedger(); |
| 1151 | + catchupSimulation.generateEmptyTxSetLedger(); |
| 1152 | + |
| 1153 | + auto checkpointLedger = catchupSimulation.getLastCheckpointLedger(1); |
| 1154 | + catchupSimulation.ensureOnlineCatchupPossible(checkpointLedger, 5); |
| 1155 | + |
| 1156 | + SECTION("offline") |
| 1157 | + { |
| 1158 | + auto app = catchupSimulation.createCatchupApplication( |
| 1159 | + std::numeric_limits<uint32_t>::max(), |
| 1160 | + Config::TESTDB_BUCKET_DB_PERSISTENT, "skip-offline"); |
| 1161 | + REQUIRE(catchupSimulation.catchupOffline(app, checkpointLedger, true)); |
| 1162 | + } |
| 1163 | + |
| 1164 | + SECTION("online") |
| 1165 | + { |
| 1166 | + auto app = catchupSimulation.createCatchupApplication( |
| 1167 | + std::numeric_limits<uint32_t>::max(), |
| 1168 | + Config::TESTDB_BUCKET_DB_PERSISTENT, "skip-online"); |
| 1169 | + REQUIRE(catchupSimulation.catchupOnline(app, checkpointLedger, 5)); |
| 1170 | + } |
| 1171 | +} |
| 1172 | + |
| 1173 | +TEST_CASE("History catchup rejects empty-tx-set ledgers with transactions", |
| 1174 | + "[history][catchup]") |
| 1175 | +{ |
| 1176 | + CatchupSimulation catchupSimulation{}; |
| 1177 | + auto& simApp = catchupSimulation.getApp(); |
| 1178 | + |
| 1179 | + while (simApp.getLedgerManager().getLastClosedLedgerNum() < 8) |
| 1180 | + { |
| 1181 | + catchupSimulation.generateRandomLedger(); |
| 1182 | + } |
| 1183 | + catchupSimulation.generateEmptyTxSetLedger(); |
| 1184 | + uint32_t const emptyTxSetSeq = |
| 1185 | + simApp.getLedgerManager().getLastClosedLedgerNum(); |
| 1186 | + |
| 1187 | + auto checkpointLedger = catchupSimulation.getLastCheckpointLedger(1); |
| 1188 | + catchupSimulation.ensureOfflineCatchupPossible(checkpointLedger); |
| 1189 | + |
| 1190 | + // Forge a non-empty TransactionHistoryEntry for the empty-tx-set ledger |
| 1191 | + // into the published transactions file, keeping the file sorted by |
| 1192 | + // ledgerSeq. |
| 1193 | + std::string archiveDir = |
| 1194 | + catchupSimulation.getHistoryConfigurator().getArchiveDirName(); |
| 1195 | + FileTransferInfo txFileInfo(FileType::HISTORY_FILE_TYPE_TRANSACTIONS, |
| 1196 | + checkpointLedger, simApp.getConfig()); |
| 1197 | + std::string gzPath = archiveDir + "/" + txFileInfo.remoteName(); |
| 1198 | + fs::checkGzipSuffix(gzPath); |
| 1199 | + auto nonGzPath = gzPath.substr(0, gzPath.size() - 3); |
| 1200 | + |
| 1201 | + auto& wm = simApp.getWorkScheduler(); |
| 1202 | + REQUIRE(wm.executeWork<GunzipFileWork>(gzPath)->getState() == |
| 1203 | + BasicWork::State::WORK_SUCCESS); |
| 1204 | + { |
| 1205 | + XDRInputFileStream in; |
| 1206 | + in.open(nonGzPath); |
| 1207 | + std::vector<TransactionHistoryEntry> txs; |
| 1208 | + TransactionHistoryEntry tx; |
| 1209 | + while (in && in.readOne(tx)) |
| 1210 | + { |
| 1211 | + txs.push_back(tx); |
| 1212 | + } |
| 1213 | + in.close(); |
| 1214 | + REQUIRE(!txs.empty()); |
| 1215 | + REQUIRE(std::filesystem::remove(nonGzPath)); |
| 1216 | + |
| 1217 | + // Copy an existing non-empty entry and retarget it at the empty-tx-set |
| 1218 | + // ledger |
| 1219 | + TransactionHistoryEntry forged = txs.front(); |
| 1220 | + forged.ledgerSeq = emptyTxSetSeq; |
| 1221 | + auto insertAt = |
| 1222 | + std::find_if(txs.begin(), txs.end(), [&](auto const& e) { |
| 1223 | + return e.ledgerSeq > emptyTxSetSeq; |
| 1224 | + }); |
| 1225 | + txs.insert(insertAt, forged); |
| 1226 | + |
| 1227 | + XDROutputFileStream out(simApp.getClock().getIOContext(), true); |
| 1228 | + out.open(nonGzPath); |
| 1229 | + for (auto const& e : txs) |
| 1230 | + { |
| 1231 | + out.writeOne(e); |
| 1232 | + } |
| 1233 | + out.close(); |
| 1234 | + } |
| 1235 | + REQUIRE(wm.executeWork<GzipFileWork>(nonGzPath)->getState() == |
| 1236 | + BasicWork::State::WORK_SUCCESS); |
| 1237 | + |
| 1238 | + // Catch up a fresh node through the tampered checkpoint, driving the |
| 1239 | + // works directly |
| 1240 | + auto app = catchupSimulation.createCatchupApplication( |
| 1241 | + std::numeric_limits<uint32_t>::max(), |
| 1242 | + Config::TESTDB_BUCKET_DB_PERSISTENT, "skip-tamper"); |
| 1243 | + |
| 1244 | + auto& appWm = app->getWorkScheduler(); |
| 1245 | + auto tmpDir = app->getTmpDirManager().tmpDir("skip-tamper-download"); |
| 1246 | + LedgerRange range = LedgerRange::inclusive( |
| 1247 | + LedgerManager::GENESIS_LEDGER_SEQ + 1, checkpointLedger); |
| 1248 | + CheckpointRange checkpointRange{range, app->getHistoryManager()}; |
| 1249 | + |
| 1250 | + auto downloadHeaders = appWm.executeWork<BatchDownloadWork>( |
| 1251 | + checkpointRange, FileType::HISTORY_FILE_TYPE_LEDGER, tmpDir); |
| 1252 | + REQUIRE(downloadHeaders->getState() == BasicWork::State::WORK_SUCCESS); |
| 1253 | + |
| 1254 | + auto lastApplied = app->getLedgerManager().getLastClosedLedgerHeader(); |
| 1255 | + auto work = appWm.executeWork<DownloadApplyTxsWork>( |
| 1256 | + tmpDir, range, lastApplied, /*waitForPublish=*/true, nullptr); |
| 1257 | + REQUIRE(work->getState() == BasicWork::State::WORK_FAILURE); |
| 1258 | + // Replay stops exactly at the skip ledger whose forged entry was rejected |
| 1259 | + REQUIRE(app->getLedgerManager().getLastClosedLedgerNum() == |
| 1260 | + emptyTxSetSeq - 1); |
| 1261 | +} |
| 1262 | + |
| 1263 | +TEST_CASE("ApplyCheckpointWork rejects malformed empty-tx-set ledger headers", |
| 1264 | + "[history][catchup]") |
| 1265 | +{ |
| 1266 | + auto runWithFabricatedHeader = |
| 1267 | + [](uint32_t genesisVersion, std::function<void(StellarValue&)> mutate) { |
| 1268 | + Config cfg(getTestConfig(0)); |
| 1269 | + cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION = genesisVersion; |
| 1270 | + VirtualClock clock; |
| 1271 | + auto app = createTestApplication(clock, cfg); |
| 1272 | + auto const& lcl = |
| 1273 | + app->getLedgerManager().getLastClosedLedgerHeader(); |
| 1274 | + |
| 1275 | + LedgerHeaderHistoryEntry entry; |
| 1276 | + entry.header.ledgerSeq = lcl.header.ledgerSeq + 1; |
| 1277 | + entry.header.previousLedgerHash = lcl.hash; |
| 1278 | + entry.header.ledgerVersion = lcl.header.ledgerVersion; |
| 1279 | + mutate(entry.header.scpValue); |
| 1280 | + entry.hash = sha256(xdr::xdr_to_opaque(entry.header)); |
| 1281 | + |
| 1282 | + auto checkpoint = HistoryManager::checkpointContainingLedger( |
| 1283 | + entry.header.ledgerSeq, app->getConfig()); |
| 1284 | + auto tmpDir = |
| 1285 | + app->getTmpDirManager().tmpDir("malformed-empty-tx-set-header"); |
| 1286 | + FileTransferInfo hi(tmpDir, FileType::HISTORY_FILE_TYPE_LEDGER, |
| 1287 | + checkpoint); |
| 1288 | + { |
| 1289 | + XDROutputFileStream out(app->getClock().getIOContext(), true); |
| 1290 | + out.open(hi.localPath_nogz()); |
| 1291 | + out.writeOne(entry); |
| 1292 | + } |
| 1293 | + // The transactions file must exist even when empty |
| 1294 | + FileTransferInfo ti( |
| 1295 | + tmpDir, FileType::HISTORY_FILE_TYPE_TRANSACTIONS, checkpoint); |
| 1296 | + { |
| 1297 | + XDROutputFileStream out(app->getClock().getIOContext(), true); |
| 1298 | + out.open(ti.localPath_nogz()); |
| 1299 | + } |
| 1300 | + |
| 1301 | + auto range = LedgerRange::inclusive(lcl.header.ledgerSeq, |
| 1302 | + entry.header.ledgerSeq); |
| 1303 | + auto w = app->getWorkScheduler().executeWork<ApplyCheckpointWork>( |
| 1304 | + tmpDir, range, OnFailureCallback{}); |
| 1305 | + return w->getState(); |
| 1306 | + }; |
| 1307 | + |
| 1308 | + SECTION("empty-tx-set hash without empty-tx-set value") |
| 1309 | + { |
| 1310 | + REQUIRE(runWithFabricatedHeader(Config::CURRENT_LEDGER_PROTOCOL_VERSION, |
| 1311 | + [](StellarValue& sv) { |
| 1312 | + sv.txSetHash = |
| 1313 | + Herder::EMPTY_TX_SET_HASH; |
| 1314 | + // ext stays STELLAR_VALUE_BASIC |
| 1315 | + }) == BasicWork::State::WORK_FAILURE); |
| 1316 | + } |
| 1317 | + |
| 1318 | + SECTION("empty-tx-set value before protocol support") |
| 1319 | + { |
| 1320 | + REQUIRE(runWithFabricatedHeader( |
| 1321 | + static_cast<uint32_t>(EMPTY_TX_SET_PROTOCOL_VERSION) - 1, |
| 1322 | + [](StellarValue& sv) { |
| 1323 | + sv.txSetHash = Herder::EMPTY_TX_SET_HASH; |
| 1324 | + sv.ext.v(STELLAR_VALUE_EMPTY_TX_SET); |
| 1325 | + }) == BasicWork::State::WORK_FAILURE); |
| 1326 | + } |
| 1327 | +} |
| 1328 | +#endif // CAP_0083 |
| 1329 | + |
1130 | 1330 | TEST_CASE("Publish works correctly post shadow removal", "[history]") |
1131 | 1331 | { |
1132 | 1332 | // Given a HAS, verify that appropriate levels have "next" cleared, while |
|
0 commit comments