Repository: bitcoin/bitcoin
File: src/test/fuzz/util/descriptor.cpp (C++)

Code before:
// Copyright (c) 2023-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <test/fuzz/util/descriptor.h>

void MockedDescriptorConverter::Init() {
    // The data to use as a private key or a seed for an xprv.
    std::array<std::byte, 32> key_data{std::byte{1}};
    // Generate keys of all kinds and store them in the keys array.
    for (size_t i{0}; i < TOTAL_KEYS_GENERATED; i++) {
        key_data[31] = std::byte(i);

        // If this is a "raw" key, generate a normal privkey. Otherwise generate
        // an extended one.
        if (IdIsCompPubKey(i) || IdIsUnCompPubKey(i) || IdIsXOnlyPubKey(i) || IdIsConstPrivKey(i)) {
            CKey privkey;
            privkey.Set(key_data.begin(), key_data.end(), !IdIsUnCompPubKey(i));
            if (IdIsCompPubKey(i) || IdIsUnCompPubKey(i)) {
                CPubKey pubkey{privkey.GetPubKey()};
                keys_str[i] = HexStr(pubkey);
            } else if (IdIsXOnlyPubKey(i)) {
                const XOnlyPubKey pubkey{privkey.GetPubKey()};
                keys_str[i] = HexStr(pubkey);
            } else {
                keys_str[i] = EncodeSecret(privkey);
            }
        } else {
            CExtKey ext_privkey;
            ext_privkey.SetSeed(key_data);
            if (IdIsXprv(i)) {
                keys_str[i] = EncodeExtKey(ext_privkey);
            } else {
                const CExtPubKey ext_pubkey{ext_privkey.Neuter()};
                keys_str[i] = EncodeExtPubKey(ext_pubkey);
            }
        }
    }
}

std::optional<uint8_t> MockedDescriptorConverter::IdxFromHex(std::string_view hex_characters) const {
    if (hex_characters.size() != 2) return {};
    auto idx = ParseHex(hex_characters);
    if (idx.size() != 1) return {};
    return idx[0];
}

std::optional<std::string> MockedDescriptorConverter::GetDescriptor(std::string_view mocked_desc) const {
    // The smallest fragment would be "pk(%00)"
    if (mocked_desc.size() < 7) return {};

    // The actual descriptor string to be returned.
    std::string desc;
    desc.reserve(mocked_desc.size());

    // Replace all occurrences of '%' followed by two hex characters with the corresponding key.
    for (size_t i = 0; i < mocked_desc.size();) {
        if (mocked_desc[i] == '%') {
            if (i + 3 >= mocked_desc.size()) return {};
            if (const auto idx = IdxFromHex(mocked_desc.substr(i + 1, 2))) {
                desc += keys_str[*idx];
                i += 3;
            } else {
                return {};
            }
        } else {
            desc += mocked_desc[i++];
        }
    }

    return desc;
}

bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth)
{
    auto depth{0};
    for (const auto& ch: buff) {
        if (ch == ',') {
            // A comma is always present between two key expressions, so we use that as a delimiter.
            depth = 0;
        } else if (ch == '/') {
            if (++depth > max_depth) return true;
        }
    }
    return false;
}


Review comment (suggestion):
nit: could be more consistent by not using a global for one and a default param for the other:

_(note: these diffs do not include the `return true` comment made further down)_

<details>
<summary>git diff on 0d961c1d97</summary>

```diff
diff --git a/src/test/fuzz/util/descriptor.cpp b/src/test/fuzz/util/descriptor.cpp
index c2aa6d9b6a..804fd1cd33 100644
--- a/src/test/fuzz/util/descriptor.cpp
+++ b/src/test/fuzz/util/descriptor.cpp
@@ -87,16 +87,13 @@ bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth)
     return false;
 }
 
-//! Maximum number of nested sub-fragments we'll allow in a descriptor.
-constexpr int MAX_NESTED_SUBS{10'000};
-
-bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs)
+bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs, const int max_nested_subs)
 {
     // We use a stack because there may be many nested sub-frags.
     std::stack<int> counts;
     for (const auto& ch: buff) {
         // The fuzzer may generate an input with a ton of parenthesis. Rule out pathological cases.
-        if (counts.size() > MAX_NESTED_SUBS) return false;
+        if (counts.size() > max_nested_subs) return false;
 
         if (ch == '(') {
             // A new fragment was opened, create a new sub-count for it and start as one since any fragment with
diff --git a/src/test/fuzz/util/descriptor.h b/src/test/fuzz/util/descriptor.h
index 77ebd11ebb..22205d41ab 100644
--- a/src/test/fuzz/util/descriptor.h
+++ b/src/test/fuzz/util/descriptor.h
@@ -57,11 +57,14 @@ bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth = MAX_DEPT
 
 //! Default maximum number of sub-fragments.
 constexpr int MAX_SUBS{1'000};
+//! Maximum number of nested sub-fragments we'll allow in a descriptor.
+constexpr int MAX_NESTED_SUBS{10'000};
 
 /**
  * Whether the buffer, if it represents a valid descriptor, contains a fragment with more
  * sub-fragments than the given maximum.
  */
-bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs = MAX_SUBS);
+bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs = MAX_SUBS,
+                       const int max_nested_subs = MAX_NESTED_SUBS);
 
 #endif // BITCOIN_TEST_FUZZ_UTIL_DESCRIPTOR_H

```
</details>

Also, this raises a narrowing warning:

```
  CXX      test/fuzz/util/libtest_fuzz_a-descriptor.o
test/fuzz/util/descriptor.cpp:96:27: warning: comparison of integers of different signs: 'std::stack<int>::size_type' (aka 'unsigned long') and 'const int' [-Wsign-compare]
        if (counts.size() > max_nested_subs) return false;
            ~~~~~~~~~~~~~ ^ ~~~~~~~~~~~~~~~
1 warning generated.
```

So using `size_t` for the max values might make sense?

<details>
<summary>git diff on 0d961c1d97</summary>

```diff
diff --git a/src/test/fuzz/util/descriptor.cpp b/src/test/fuzz/util/descriptor.cpp
index c2aa6d9b6a..e3b5218e15 100644
--- a/src/test/fuzz/util/descriptor.cpp
+++ b/src/test/fuzz/util/descriptor.cpp
@@ -87,16 +87,14 @@ bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth)
     return false;
 }
 
-//! Maximum number of nested sub-fragments we'll allow in a descriptor.
-constexpr int MAX_NESTED_SUBS{10'000};
-
-bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs)
+bool HasTooManySubFrag(const FuzzBufferType& buff, const size_t max_subs,
+                       const size_t max_nested_subs)
 {
     // We use a stack because there may be many nested sub-frags.
-    std::stack<int> counts;
+    std::stack<size_t> counts;
     for (const auto& ch: buff) {
         // The fuzzer may generate an input with a ton of parenthesis. Rule out pathological cases.
-        if (counts.size() > MAX_NESTED_SUBS) return false;
+        if (counts.size() > max_nested_subs) return false;
 
         if (ch == '(') {
             // A new fragment was opened, create a new sub-count for it and start as one since any fragment with
diff --git a/src/test/fuzz/util/descriptor.h b/src/test/fuzz/util/descriptor.h
index 77ebd11ebb..a379883be8 100644
--- a/src/test/fuzz/util/descriptor.h
+++ b/src/test/fuzz/util/descriptor.h
@@ -56,12 +56,15 @@ constexpr int MAX_DEPTH{2};
 bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth = MAX_DEPTH);
 
 //! Default maximum number of sub-fragments.
-constexpr int MAX_SUBS{1'000};
+constexpr size_t MAX_SUBS{1'000};
+//! Maximum number of nested sub-fragments we'll allow in a descriptor.
+constexpr size_t MAX_NESTED_SUBS{10'000};
 
 /**
  * Whether the buffer, if it represents a valid descriptor, contains a fragment with more
  * sub-fragments than the given maximum.
  */
-bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs = MAX_SUBS);
+bool HasTooManySubFrag(const FuzzBufferType& buff, const size_t max_subs = MAX_SUBS,
+                       const size_t max_nested_subs = MAX_NESTED_SUBS);
 
 #endif // BITCOIN_TEST_FUZZ_UTIL_DESCRIPTOR_H

```
</details>


Code after:
                return {};
            }
        } else {
            desc += mocked_desc[i++];
        }
    }

    return desc;
}

bool HasDeepDerivPath(const FuzzBufferType& buff, const int max_depth)
{
    auto depth{0};
    for (const auto& ch: buff) {
        if (ch == ',') {
            // A comma is always present between two key expressions, so we use that as a delimiter.
            depth = 0;
        } else if (ch == '/') {
            if (++depth > max_depth) return true;
        }
    }
    return false;
}

bool HasTooManySubFrag(const FuzzBufferType& buff, const int max_subs, const size_t max_nested_subs)
{
    // We use a stack because there may be many nested sub-frags.
    std::stack<int> counts;
    for (const auto& ch: buff) {
        // The fuzzer may generate an input with a ton of parentheses. Rule out pathological cases.
        if (counts.size() > max_nested_subs) return true;

        if (ch == '(') {
            // A new fragment was opened, create a new sub-count for it and start as one since any fragment with
            // parentheses has at least one sub.
            counts.push(1);
        } else if (ch == ',' && !counts.empty()) {
            // When encountering a comma, account for an additional sub in the last opened fragment. If it exceeds the
            // limit, bail.
            if (++counts.top() > max_subs) return true;
        } else if (ch == ')' && !counts.empty()) {
            // Fragment closed! Drop its sub count and resume to counting the number of subs for its parent.
            counts.pop();
        }
    }
    return false;
}

bool HasTooManyWrappers(const FuzzBufferType& buff, const int max_wrappers)
{
    // The number of nested wrappers. Nested wrappers are always characters which follow each other so we don't have to