Skip to content

Commit 2d5b424

Browse files
committed
Merge bitcoin#32351: test: avoid stack overflow in FindChallenges via manual iteration
7e8ef95 refactor: Fix Sonar rule `cpp:S4998` - avoid unique_ptr const& as parameter (Lőrinc) e400ac5 refactor: simplify repeated comparisons in `FindChallenges` (Lőrinc) f670836 test: remove old recursive `FindChallenges_recursive` implementation (Lőrinc) b80d0bd test: avoid stack overflow in `FindChallenges` via manual iteration (Lőrinc) Pull request description: `FindChallenges` explores the `Miniscript` node tree by going deep into the first child's subtree, then the second, and so on - effectively performing a pre-order Traversal (Depth-First Search) recursively, using the call stack which can result in stack overflows on Windows debug builds. This change replaces the recursive implementation with an iterative version using an explicit stack. The new implementation also performs a pre-order depth-first traversal, though it processes children in right-to-left order (rather than left-to-right) due to the LIFO nature of the stack. Since both versions store results in a `std::set`, which automatically sorts and deduplicates elements, the exact traversal order doesn't affect the final result. It is an alternative to increasing the Windows stack size, as proposed in bitcoin#32349, and addresses the issue raised in bitcoin#32341 by avoiding deep recursion altogether. The change is done in two commits: * add a new iterative `FindChallenges` method and rename the old method to `*_recursive` (to simplify the next commit where we remove it), asserting that its result matches the original; * remove the original recursive implementation. This approach avoids ignoring the `misc-no-recursion` warning as well. I tried modifying the new method to store results in a vector instead, but it demonstrated that the deduplication provided by `std::set` was necessary. One example showing the need for deduplication: Recursive (using set): ``` (6, 9070746) (6, 19532513) (6, 3343376967) ``` Iterative (using vector attempt): ``` (6, 19532513) (6, 9070746) (6, 3343376967) (6, 9070746) // Duplicate entry ``` The performance of the test is the same as before, with the recursive method. Fixes bitcoin#32341 ACKs for top commit: achow101: ACK 7e8ef95 sipa: utACK 7e8ef95 hodlinator: re-ACK 7e8ef95 Tree-SHA512: 9e52eff82a7d76f5d37e3b74c508f08e5fced5386dad504bed111b27ed2b529008a6dd12a5116f009609a94c7ee7ebe3e80a759dda55dd1cb3ae52078f65ec71
2 parents 0ed5f37 + 7e8ef95 commit 2d5b424

File tree

1 file changed

+23
-22
lines changed

1 file changed

+23
-22
lines changed

src/test/miniscript_tests.cpp

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -297,28 +297,29 @@ using miniscript::operator""_mst;
297297
using Node = miniscript::Node<CPubKey>;
298298

299299
/** Compute all challenges (pubkeys, hashes, timelocks) that occur in a given Miniscript. */
300-
// NOLINTNEXTLINE(misc-no-recursion)
301-
std::set<Challenge> FindChallenges(const NodeRef& ref) {
300+
std::set<Challenge> FindChallenges(const Node* root)
301+
{
302302
std::set<Challenge> chal;
303-
for (const auto& key : ref->keys) {
304-
chal.emplace(ChallengeType::PK, ChallengeNumber(key));
305-
}
306-
if (ref->fragment == miniscript::Fragment::OLDER) {
307-
chal.emplace(ChallengeType::OLDER, ref->k);
308-
} else if (ref->fragment == miniscript::Fragment::AFTER) {
309-
chal.emplace(ChallengeType::AFTER, ref->k);
310-
} else if (ref->fragment == miniscript::Fragment::SHA256) {
311-
chal.emplace(ChallengeType::SHA256, ChallengeNumber(ref->data));
312-
} else if (ref->fragment == miniscript::Fragment::RIPEMD160) {
313-
chal.emplace(ChallengeType::RIPEMD160, ChallengeNumber(ref->data));
314-
} else if (ref->fragment == miniscript::Fragment::HASH256) {
315-
chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->data));
316-
} else if (ref->fragment == miniscript::Fragment::HASH160) {
317-
chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->data));
318-
}
319-
for (const auto& sub : ref->subs) {
320-
auto sub_chal = FindChallenges(sub);
321-
chal.insert(sub_chal.begin(), sub_chal.end());
303+
304+
for (std::vector stack{root}; !stack.empty();) {
305+
const auto* ref{stack.back()};
306+
stack.pop_back();
307+
308+
for (const auto& key : ref->keys) {
309+
chal.emplace(ChallengeType::PK, ChallengeNumber(key));
310+
}
311+
switch (ref->fragment) {
312+
case Fragment::OLDER: chal.emplace(ChallengeType::OLDER, ref->k); break;
313+
case Fragment::AFTER: chal.emplace(ChallengeType::AFTER, ref->k); break;
314+
case Fragment::SHA256: chal.emplace(ChallengeType::SHA256, ChallengeNumber(ref->data)); break;
315+
case Fragment::RIPEMD160: chal.emplace(ChallengeType::RIPEMD160, ChallengeNumber(ref->data)); break;
316+
case Fragment::HASH256: chal.emplace(ChallengeType::HASH256, ChallengeNumber(ref->data)); break;
317+
case Fragment::HASH160: chal.emplace(ChallengeType::HASH160, ChallengeNumber(ref->data)); break;
318+
default: break;
319+
}
320+
for (const auto& sub : ref->subs) {
321+
stack.push_back(sub.get());
322+
}
322323
}
323324
return chal;
324325
}
@@ -347,7 +348,7 @@ struct MiniScriptTest : BasicTestingSetup {
347348
/** Run random satisfaction tests. */
348349
void TestSatisfy(const KeyConverter& converter, const std::string& testcase, const NodeRef& node) {
349350
auto script = node->ToScript(converter);
350-
auto challenges = FindChallenges(node); // Find all challenges in the generated miniscript.
351+
const auto challenges{FindChallenges(node.get())}; // Find all challenges in the generated miniscript.
351352
std::vector<Challenge> challist(challenges.begin(), challenges.end());
352353
for (int iter = 0; iter < 3; ++iter) {
353354
std::shuffle(challist.begin(), challist.end(), m_rng);

0 commit comments

Comments
 (0)