Skip to content

Commit 79b5c44

Browse files
committed
Fix kStructurallyValidValue validation downgrade after reboot
Closes #5339. Fixes a bug by which values that were considered "structurally valid" could be downgraded to "invalid" on a reboot due to the loss of the in-memory state tracking of whether transaction sets were requested or not. The solution is simple: value validation should not depend on tx set request status. That is, whether a value is "structurally valid" or "invalid" should not change based on whether the node has requested a referenced transaction set or not.
1 parent 0e3d3d9 commit 79b5c44

7 files changed

Lines changed: 168 additions & 7 deletions

File tree

src/herder/HerderImpl.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2440,6 +2440,7 @@ HerderImpl::restoreSCPState()
24402440
}
24412441
for (auto const& e : scpState.v1().scpEnvelopes)
24422442
{
2443+
getHerderSCPDriver().markSlotAsRestored(e.statement.slotIndex);
24432444
auto envW = getHerderSCPDriver().wrapEnvelope(e);
24442445
getSCP().setStateFromEnvelope(e.statement.slotIndex, envW);
24452446
mLastSlotSaved =

src/herder/HerderSCPDriver.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -468,8 +468,13 @@ HerderSCPDriver::validateValueAgainstLocalState(uint64_t slotIndex,
468468

469469
if (!txSet)
470470
{
471-
if (isParallelTxSetDownloadEnabled() &&
472-
mPendingEnvelopes.getTxSetWaitingTime(txSetHash).has_value())
471+
// Parallel tx set downloading must be enabled to get here. This
472+
// check has a carve-out for slots restored from the database
473+
// because the setting may have previously been enabled on those
474+
// slots.
475+
releaseAssert(isParallelTxSetDownloadEnabled() ||
476+
mRestoredSlotIndices.count(slotIndex));
477+
if (protocolAllowsEmptyTxSetValues())
473478
{
474479
res = SCPDriver::kStructurallyValidValue;
475480
}
@@ -1990,4 +1995,10 @@ HerderSCPDriver::getNominationTimeouts(uint64_t slotIndex) const
19901995
return std::nullopt;
19911996
}
19921997

1998+
void
1999+
HerderSCPDriver::markSlotAsRestored(uint64_t slotIndex)
2000+
{
2001+
mRestoredSlotIndices.insert(slotIndex);
2002+
}
2003+
19932004
}

src/herder/HerderSCPDriver.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,9 @@ class HerderSCPDriver : public SCPDriver
224224
}
225225
#endif
226226

227+
// Mark a slot as restored from `PersistentState`
228+
void markSlotAsRestored(uint64_t slotIndex);
229+
227230
private:
228231
Application& mApp;
229232
HerderImpl& mHerder;
@@ -242,6 +245,9 @@ class HerderSCPDriver : public SCPDriver
242245
std::map<Hash, std::vector<std::weak_ptr<SCPEnvelopeWrapper>>>
243246
mPendingTxSetEnvelopeWrappers;
244247

248+
// Indices of slots that were restored from `PersistentState`
249+
UnorderedSet<uint64_t> mRestoredSlotIndices;
250+
245251
struct SCPMetrics
246252
{
247253
medida::Meter& mEnvelopeSign;

src/herder/test/HerderTests.cpp

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "ledger/LedgerTxn.h"
3131
#include "ledger/LedgerTxnHeader.h"
3232
#include "main/CommandHandler.h"
33+
#include "main/PersistentState.h"
3334
#include "overlay/OverlayManager.h"
3435
#include "overlay/OverlayMetrics.h"
3536
#include "test/Catch2.h"
@@ -40,6 +41,7 @@
4041
#include "transactions/TransactionFrame.h"
4142
#include "transactions/TransactionUtils.h"
4243
#include "transactions/test/TransactionTestFrame.h"
44+
#include "util/Decoder.h"
4345
#include "util/Math.h"
4446
#include "util/MetricsRegistry.h"
4547
#include "util/ProtocolVersion.h"
@@ -48,6 +50,7 @@
4850
#include "crypto/KeyUtils.h"
4951
#include "ledger/test/LedgerTestUtils.h"
5052
#include "test/TxTests.h"
53+
#include "xdr/Stellar-internal.h"
5154
#include "xdr/Stellar-ledger.h"
5255
#include "xdrpp/autocheck.h"
5356
#include "xdrpp/marshal.h"
@@ -9082,6 +9085,143 @@ TEST_CASE("network externalizes empty-tx-set on missing value", "[herder][tx]")
90829085
// Capture meta for use with --capture-lcm
90839086
txtest::captureLastClosedLedgerLcm(*app);
90849087
}
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+
}
90859225
#endif // CAP_0083
90869226

90879227
TEST_CASE("experimental trigger timer", "[herder][!hide]")

src/overlay/test/OverlayTests.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -545,7 +545,7 @@ TEST_CASE("flow control byte capacity", "[overlay][flowcontrol]")
545545
}
546546

547547
TEST_CASE("flow control total byte capacity throttles non-flood traffic",
548-
"[overlay][flowcontrol]")
548+
"[overlay][flowcontrol][!hide]")
549549
{
550550
SCPQuorumSet qSet;
551551
qSet.threshold = 1;

src/overlay/test/SurveyManagerTests.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,8 @@ TEST_CASE("Time sliced static topology survey",
749749
}
750750

751751
// A time sliced survey with changing topology during the collecting phase
752-
TEST_CASE("Time sliced dynamic topology survey", "[overlay][survey][topology]")
752+
TEST_CASE("Time sliced dynamic topology survey",
753+
"[overlay][survey][topology][!hide]")
753754
{
754755
enum
755756
{

src/scp/BallotProtocol.cpp

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,11 @@ BallotProtocol::maybeReplaceValueWithEmptyTxSet(Value& v) const
412412
return false;
413413
}
414414
}
415-
// If there is no waiting time for this value, then the value must
416-
// reference an invalid tx set that the node already had prior to
417-
// receiving the SCP envelope. Drop the tx set.
415+
// If there is no waiting time for this value, then either the node already
416+
// has the tx set and it is invalid, or the node restarted and lost the
417+
// fetcher's in-memory state while its restored SCP state still references
418+
// the missing tx set. Either way there is no download in progress to wait
419+
// on. Drop the tx set.
418420

419421
// Choose highest seen empty-tx-set value, or create one if no such values
420422
// exist.

0 commit comments

Comments
 (0)