Skip to content

Commit 85c35d3

Browse files
CAP-83: Harden previousLedgerHash check in combineCandidates (#5380)
This change moves a correctness check for a transaction set's `previousLedgerHash` up before calling `prepareForApply` on that transaction set.
2 parents c444c07 + 1054fe4 commit 85c35d3

2 files changed

Lines changed: 81 additions & 19 deletions

File tree

src/herder/HerderSCPDriver.cpp

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,27 +1066,31 @@ HerderSCPDriver::combineCandidates(uint64_t slotIndex,
10661066
}
10671067
// else: EmptyTxSet -> cTxSet stays null, handled by existing
10681068

1069-
// Only valid applicable tx sets should be combined.
1070-
auto cApplicableTxSet =
1071-
cTxSet ? cTxSet->prepareForApply(mApp, lcl.header) : nullptr;
1072-
if (!cTxSet || cTxSet->previousLedgerHash() == lcl.hash)
1069+
// Prefer applicable tx sets. `compareTxSets` down-ranks
1070+
// non-applicable ones. Without CAP-0083 every candidate here is
1071+
// applicable (a mismatched one can't be ratified), so the
1072+
// non-applicable case doesn't arise. Under CAP-0083 a
1073+
// non-applicable candidate may be selected and is replaced with an
1074+
// empty-tx-set value during balloting.
1075+
ApplicableTxSetFrameConstPtr cApplicableTxSet = nullptr;
1076+
if (cTxSet && cTxSet->previousLedgerHash() == lcl.hash)
10731077
{
1078+
cApplicableTxSet = cTxSet->prepareForApply(mApp, lcl.header);
1079+
}
10741080

1075-
if (highest == candidateValues.cend() ||
1076-
compareTxSets(
1077-
highestApplicableTxSet, cApplicableTxSet,
1078-
highest->txSetHash, sv.txSetHash,
1079-
highestTxSet
1080-
? std::make_optional(highestTxSet->encodedSize())
1081-
: std::nullopt,
1082-
cTxSet ? std::make_optional(cTxSet->encodedSize())
1083-
: std::nullopt,
1084-
lcl.header, candidatesHash))
1085-
{
1086-
highest = it;
1087-
highestTxSet = cTxSet;
1088-
highestApplicableTxSet = std::move(cApplicableTxSet);
1089-
}
1081+
if (highest == candidateValues.cend() ||
1082+
compareTxSets(highestApplicableTxSet, cApplicableTxSet,
1083+
highest->txSetHash, sv.txSetHash,
1084+
highestTxSet ? std::make_optional(
1085+
highestTxSet->encodedSize())
1086+
: std::nullopt,
1087+
cTxSet ? std::make_optional(cTxSet->encodedSize())
1088+
: std::nullopt,
1089+
lcl.header, candidatesHash))
1090+
{
1091+
highest = it;
1092+
highestTxSet = cTxSet;
1093+
highestApplicableTxSet = std::move(cApplicableTxSet);
10901094
}
10911095
}
10921096
if (highest == candidateValues.cend())

src/herder/test/HerderTests.cpp

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3362,6 +3362,64 @@ TEST_CASE("SCP Driver", "[herder][acceptance]")
33623362
}
33633363
}
33643364

3365+
// Test combineCandidates handling of candidates where
3366+
// previousLedgerHash != LCL.hash
3367+
TEST_CASE("combineCandidates with mismatched previousLedgerHash candidate",
3368+
"[herder][bug]")
3369+
{
3370+
Config cfg(getTestConfig());
3371+
3372+
VirtualClock clock;
3373+
auto app = createTestApplication(clock, cfg);
3374+
auto& herder = dynamic_cast<HerderImpl&>(app->getHerder());
3375+
auto& pe = herder.getPendingEnvelopes();
3376+
auto& driver = herder.getHerderSCPDriver();
3377+
3378+
auto const& lcl = app->getLedgerManager().getLastClosedLedgerHeader();
3379+
uint32_t const ver = lcl.header.ledgerVersion;
3380+
uint64_t const closeTime = lcl.header.scpValue.closeTime + 1;
3381+
uint64_t const slotIndex = lcl.header.ledgerSeq + 1;
3382+
3383+
// Two structurally-valid empty tx sets that differ only in
3384+
// previousLedgerHash.
3385+
auto goodTxSet = TxSetXDRFrame::makeEmpty(lcl.hash, ver); // matches LCL
3386+
auto badTxSet = TxSetXDRFrame::makeEmpty(sha256("not the LCL hash"),
3387+
ver); // mismatched
3388+
3389+
ValueWrapperPtrSet candidates;
3390+
// Register the tx set so combineCandidates' getTxSet() returns it, then add
3391+
// a candidate value referencing it.
3392+
auto addCandidate = [&](TxSetXDRFrameConstPtr const& txSet) {
3393+
pe.addTxSet(txSet->getContentsHash(), slotIndex, txSet);
3394+
StellarValue sv =
3395+
herder.makeStellarValue(txSet->getContentsHash(), closeTime,
3396+
emptyUpgradeSteps, cfg.NODE_SEED);
3397+
candidates.emplace(driver.wrapValue(xdr::xdr_to_opaque(sv)));
3398+
};
3399+
auto combinedTxSetHash = [&]() {
3400+
ValueWrapperPtr result =
3401+
driver.combineCandidates(slotIndex, candidates);
3402+
StellarValue sv;
3403+
xdr::xdr_from_opaque(result->getValue(), sv);
3404+
return sv.txSetHash;
3405+
};
3406+
3407+
SECTION("prefer applicable candidate over mismatched candidate")
3408+
{
3409+
addCandidate(goodTxSet);
3410+
addCandidate(badTxSet);
3411+
REQUIRE(combinedTxSetHash() == goodTxSet->getContentsHash());
3412+
}
3413+
3414+
SECTION("all candidates have mismatched previousLedgerHash")
3415+
{
3416+
// If the *only* option is a candidate with a mismatched
3417+
// previousLedgerHash, choose it.
3418+
addCandidate(badTxSet);
3419+
REQUIRE(combinedTxSetHash() == badTxSet->getContentsHash());
3420+
}
3421+
}
3422+
33653423
TEST_CASE("SCP State", "[herder]")
33663424
{
33673425
SecretKey nodeKeys[3];

0 commit comments

Comments
 (0)