|
30 | 30 | #include "ledger/LedgerTxn.h" |
31 | 31 | #include "ledger/LedgerTxnHeader.h" |
32 | 32 | #include "main/CommandHandler.h" |
| 33 | +#include "main/PersistentState.h" |
33 | 34 | #include "overlay/OverlayManager.h" |
34 | 35 | #include "overlay/OverlayMetrics.h" |
35 | 36 | #include "test/Catch2.h" |
|
40 | 41 | #include "transactions/TransactionFrame.h" |
41 | 42 | #include "transactions/TransactionUtils.h" |
42 | 43 | #include "transactions/test/TransactionTestFrame.h" |
| 44 | +#include "util/Decoder.h" |
43 | 45 | #include "util/Math.h" |
44 | 46 | #include "util/MetricsRegistry.h" |
45 | 47 | #include "util/ProtocolVersion.h" |
|
48 | 50 | #include "crypto/KeyUtils.h" |
49 | 51 | #include "ledger/test/LedgerTestUtils.h" |
50 | 52 | #include "test/TxTests.h" |
| 53 | +#include "xdr/Stellar-internal.h" |
51 | 54 | #include "xdr/Stellar-ledger.h" |
52 | 55 | #include "xdrpp/autocheck.h" |
53 | 56 | #include "xdrpp/marshal.h" |
@@ -9082,6 +9085,143 @@ TEST_CASE("network externalizes empty-tx-set on missing value", "[herder][tx]") |
9082 | 9085 | // Capture meta for use with --capture-lcm |
9083 | 9086 | txtest::captureLastClosedLedgerLcm(*app); |
9084 | 9087 | } |
| 9088 | + |
| 9089 | +// Test that the node properly handles a restart when voting on a value whose tx |
| 9090 | +// set it has not successfully downloaded |
| 9091 | +TEST_CASE("SCP state restore with missing tx set", "[herder]") |
| 9092 | +{ |
| 9093 | + auto cfg = getTestConfig(0, Config::TESTDB_BUCKET_DB_PERSISTENT); |
| 9094 | + cfg.MANUAL_CLOSE = false; |
| 9095 | + // Test with parallel tx set downloading both enabled and disabled. The |
| 9096 | + // disabled case tests a node operator shutting down a node with parallel tx |
| 9097 | + // set downloading enabled, then flipping the flag off and restarting the |
| 9098 | + // node. |
| 9099 | + bool const parallelTxSetDownload = GENERATE(true, false); |
| 9100 | + CAPTURE(parallelTxSetDownload); |
| 9101 | + cfg.EXPERIMENTAL_PARALLEL_TX_SET_DOWNLOAD = parallelTxSetDownload; |
| 9102 | + |
| 9103 | + auto const peerKey = SecretKey::fromSeed(sha256("scp state restore peer")); |
| 9104 | + auto const& peerPk = peerKey.getPublicKey(); |
| 9105 | + auto const selfPk = cfg.NODE_SEED.getPublicKey(); |
| 9106 | + |
| 9107 | + // {self, peer} with threshold 2, so that {peer} alone is v-blocking |
| 9108 | + cfg.QUORUM_SET.validators.emplace_back(peerPk); |
| 9109 | + cfg.QUORUM_SET.threshold = 2; |
| 9110 | + |
| 9111 | + // Tx set hash deliberately fake: never downloaded, so never persisted |
| 9112 | + Hash fakeTxSetHash; |
| 9113 | + fakeTxSetHash.fill(0xAB); |
| 9114 | + |
| 9115 | + uint64 slot = 0; |
| 9116 | + Value value; |
| 9117 | + |
| 9118 | + // Create the node's database and persist SCP state for slot LCL+1 that |
| 9119 | + // ballots on `fakeTxSetHash` without persisting any tx set. This simulates |
| 9120 | + // a node emitting a PREPARE for a value whose tx set is still downloading. |
| 9121 | + { |
| 9122 | + VirtualClock clock; |
| 9123 | + auto app = createTestApplication(clock, cfg, /*newDB*/ true, |
| 9124 | + /*startApp*/ false); |
| 9125 | + auto& herder = static_cast<HerderImpl&>(app->getHerder()); |
| 9126 | + auto const& lcl = app->getLedgerManager().getLastClosedLedgerHeader(); |
| 9127 | + slot = lcl.header.ledgerSeq + 1; |
| 9128 | + |
| 9129 | + auto sv = herder.makeStellarValue(fakeTxSetHash, app->timeNow() + 1, |
| 9130 | + emptyUpgradeSteps, cfg.NODE_SEED); |
| 9131 | + value = xdr::xdr_to_opaque(sv); |
| 9132 | + |
| 9133 | + SCPEnvelope env; |
| 9134 | + env.statement.slotIndex = slot; |
| 9135 | + env.statement.nodeID = selfPk; |
| 9136 | + env.statement.pledges.type(SCP_ST_PREPARE); |
| 9137 | + auto& prep = env.statement.pledges.prepare(); |
| 9138 | + prep.ballot.counter = 1; |
| 9139 | + prep.ballot.value = value; |
| 9140 | + prep.quorumSetHash = herder.getSCP().getLocalNode()->getQuorumSetHash(); |
| 9141 | + herder.signEnvelope(cfg.NODE_SEED, env); |
| 9142 | + |
| 9143 | + PersistedSCPState scpState; |
| 9144 | + scpState.v(1); |
| 9145 | + scpState.v1().scpEnvelopes.emplace_back(env); |
| 9146 | + scpState.v1().quorumSets.emplace_back( |
| 9147 | + herder.getSCP().getLocalQuorumSet()); |
| 9148 | + app->getPersistentState().setSCPStateV1ForSlot( |
| 9149 | + slot, decoder::encode_b64(xdr::xdr_to_opaque(scpState)), |
| 9150 | + /*txSets*/ {}); |
| 9151 | + } |
| 9152 | + |
| 9153 | + // Restart on the same database, restoring the persisted SCP state. |
| 9154 | + VirtualClock clock; |
| 9155 | + auto app = createTestApplication(clock, cfg, /*newDB*/ false); |
| 9156 | + auto& herder = static_cast<HerderImpl&>(app->getHerder()); |
| 9157 | + auto& driver = herder.getHerderSCPDriver(); |
| 9158 | + |
| 9159 | + // The ballot state was restored |
| 9160 | + REQUIRE(!herder.getSCP().getLatestMessagesSend(slot).empty()); |
| 9161 | + |
| 9162 | + // The restored value's tx set is missing and nothing is fetching it, but |
| 9163 | + // the value is still structurally valid |
| 9164 | + REQUIRE(driver.validateValue(slot, value, /*nomination*/ false) == |
| 9165 | + SCPDriver::kStructurallyValidValue); |
| 9166 | + |
| 9167 | + // The peer's view of the slot: it timed out waiting for the missing tx |
| 9168 | + // set and moved on to the corresponding empty-tx-set value. |
| 9169 | + Value const emptyValue = driver.makeEmptyTxSetValueFromValue(value); |
| 9170 | + |
| 9171 | + auto makePrepareFromPeer = [&](bool includePrepared) { |
| 9172 | + SCPEnvelope env; |
| 9173 | + env.statement.slotIndex = slot; |
| 9174 | + env.statement.nodeID = peerPk; |
| 9175 | + env.statement.pledges.type(SCP_ST_PREPARE); |
| 9176 | + auto& prep = env.statement.pledges.prepare(); |
| 9177 | + prep.ballot.counter = 2; |
| 9178 | + prep.ballot.value = emptyValue; |
| 9179 | + if (includePrepared) |
| 9180 | + { |
| 9181 | + prep.prepared.activate() = SCPBallot(1, emptyValue); |
| 9182 | + } |
| 9183 | + prep.quorumSetHash = herder.getSCP().getLocalNode()->getQuorumSetHash(); |
| 9184 | + herder.signEnvelope(peerKey, env); |
| 9185 | + return env; |
| 9186 | + }; |
| 9187 | + |
| 9188 | + auto latestSelfMessage = [&]() -> SCPEnvelope const* { |
| 9189 | + auto const* e = herder.getSCP().getLatestMessage(selfPk); |
| 9190 | + REQUIRE(e != nullptr); |
| 9191 | + REQUIRE(e->statement.pledges.type() == SCP_ST_PREPARE); |
| 9192 | + return e; |
| 9193 | + }; |
| 9194 | + |
| 9195 | + SECTION("peer accepted the empty-tx-set value as prepared") |
| 9196 | + { |
| 9197 | + // The v-blocking peer accepted (1, emptyValue) as prepared, which |
| 9198 | + // makes the node accept it as prepared too and re-emit its own |
| 9199 | + // statement. The node then abandons its ballot on the restored value |
| 9200 | + // in favor of the empty-tx-set value the peer is ahead on. |
| 9201 | + REQUIRE(herder.recvSCPEnvelope(makePrepareFromPeer(true)) == |
| 9202 | + Herder::ENVELOPE_STATUS_READY); |
| 9203 | + |
| 9204 | + auto const& prep = latestSelfMessage()->statement.pledges.prepare(); |
| 9205 | + REQUIRE(prep.ballot.counter == 2); |
| 9206 | + REQUIRE(prep.ballot.value == emptyValue); |
| 9207 | + REQUIRE(prep.prepared); |
| 9208 | + REQUIRE(prep.prepared->value == emptyValue); |
| 9209 | + } |
| 9210 | + |
| 9211 | + SECTION("peer is v-blocking ahead") |
| 9212 | + { |
| 9213 | + // The v-blocking peer is on a higher ballot counter, so the node |
| 9214 | + // abandons its ballot. Since nothing is downloading the missing tx |
| 9215 | + // set, the node replaces the restored value with the empty-tx-set |
| 9216 | + // value when bumping. |
| 9217 | + REQUIRE(herder.recvSCPEnvelope(makePrepareFromPeer(false)) == |
| 9218 | + Herder::ENVELOPE_STATUS_READY); |
| 9219 | + |
| 9220 | + auto const& prep = latestSelfMessage()->statement.pledges.prepare(); |
| 9221 | + REQUIRE(prep.ballot.counter == 2); |
| 9222 | + REQUIRE(prep.ballot.value == emptyValue); |
| 9223 | + } |
| 9224 | +} |
9085 | 9225 | #endif // CAP_0083 |
9086 | 9226 |
|
9087 | 9227 | TEST_CASE("experimental trigger timer", "[herder][!hide]") |
|
0 commit comments