diff options
Diffstat (limited to 'src/test')
94 files changed, 2970 insertions, 904 deletions
diff --git a/src/test/.gitignore b/src/test/.gitignore new file mode 100644 index 0000000000..036df1430c --- /dev/null +++ b/src/test/.gitignore @@ -0,0 +1,2 @@ +# capnp generated files +*.capnp.* diff --git a/src/test/README.md b/src/test/README.md index 90d0e7102d..bab1a28f61 100644 --- a/src/test/README.md +++ b/src/test/README.md @@ -15,7 +15,8 @@ that runs all of the unit tests. The main source file for the test library is fo Unit tests will be automatically compiled if dependencies were met in `./configure` and tests weren't explicitly disabled. -After configuring, they can be run with `make check`. +After configuring, they can be run with `make check`, which includes unit tests from +subtrees, or `make && make -C src check-unit` for just the unit tests. To run the unit tests manually, launch `src/test/test_bitcoin`. To recompile after a test file was modified, run `make` and then run the test again. If you @@ -41,17 +42,18 @@ test_bitcoin --log_level=all --run_test=getarg_tests ``` `log_level` controls the verbosity of the test framework, which logs when a -test case is entered, for example. `test_bitcoin` also accepts the command -line arguments accepted by `bitcoind`. Use `--` to separate both types of -arguments: +test case is entered, for example. + +`test_bitcoin` also accepts some of the command line arguments accepted by +`bitcoind`. Use `--` to separate these sets of arguments: ```bash test_bitcoin --log_level=all --run_test=getarg_tests -- -printtoconsole=1 ``` -The `-printtoconsole=1` after the two dashes redirects the debug log, which -would normally go to a file in the test datadir -(`BasicTestingSetup::m_path_root`), to the standard terminal output. +The `-printtoconsole=1` after the two dashes sends debug logging, which +normally goes only to `debug.log` within the data directory, also to the +standard terminal output. ... or to run just the doubledash test: @@ -59,7 +61,42 @@ would normally go to a file in the test datadir test_bitcoin --run_test=getarg_tests/doubledash ``` -Run `test_bitcoin --help` for the full list. +`test_bitcoin` creates a temporary working (data) directory with a randomly +generated pathname within `test_common_Bitcoin Core/`, which in turn is within +the system's temporary directory (see +[`temp_directory_path`](https://en.cppreference.com/w/cpp/filesystem/temp_directory_path)). +This data directory looks like a simplified form of the standard `bitcoind` data +directory. Its content will vary depending on the test, but it will always +have a `debug.log` file, for example. + +The location of the temporary data directory can be specified with the +`-testdatadir` option. This can make debugging easier. The directory +path used is the argument path appended with +`/test_common_Bitcoin Core/<test-name>/datadir`. +The directory path is created if necessary. +Specifying this argument also causes the data directory +not to be removed after the last test. This is useful for looking at +what the test wrote to `debug.log` after it completes, for example. +(The directory is removed at the start of the next test run, +so no leftover state is used.) + +```bash +$ test_bitcoin --run_test=getarg_tests/doubledash -- -testdatadir=/somewhere/mydatadir +Test directory (will not be deleted): "/somewhere/mydatadir/test_common_Bitcoin Core/getarg_tests/doubledash/datadir +Running 1 test case... + +*** No errors detected +$ ls -l '/somewhere/mydatadir/test_common_Bitcoin Core/getarg_tests/doubledash/datadir' +total 8 +drwxrwxr-x 2 admin admin 4096 Nov 27 22:45 blocks +-rw-rw-r-- 1 admin admin 1003 Nov 27 22:45 debug.log +``` + +If you run an entire test suite, such as `--run_test=getarg_tests`, or all the test suites +(by not specifying `--run_test`), a separate directory +will be created for each individual test. + +Run `test_bitcoin --help` for the full list of tests. ### Adding test cases diff --git a/src/test/addrman_tests.cpp b/src/test/addrman_tests.cpp index 5bd4f271cd..9668a85484 100644 --- a/src/test/addrman_tests.cpp +++ b/src/test/addrman_tests.cpp @@ -1068,7 +1068,7 @@ BOOST_AUTO_TEST_CASE(load_addrman_corrupted) BOOST_AUTO_TEST_CASE(addrman_update_address) { - // Tests updating nTime via Connected() and nServices via SetServices() + // Tests updating nTime via Connected() and nServices via SetServices() and Add() auto addrman = std::make_unique<AddrMan>(EMPTY_NETGROUPMAN, DETERMINISTIC, GetCheckRatio(m_node)); CNetAddr source{ResolveIP("252.2.2.2")}; CAddress addr{CAddress(ResolveService("250.1.1.1", 8333), NODE_NONE)}; @@ -1095,6 +1095,32 @@ BOOST_AUTO_TEST_CASE(addrman_update_address) BOOST_CHECK_EQUAL(vAddr2.size(), 1U); BOOST_CHECK(vAddr2.at(0).nTime >= start_time + 10000s); BOOST_CHECK_EQUAL(vAddr2.at(0).nServices, NODE_NETWORK_LIMITED); + + // Updating an existing addr through Add() (used in gossip relay) can add additional services but can't remove existing ones. + CAddress addr_v2{CAddress(ResolveService("250.1.1.1", 8333), NODE_P2P_V2)}; + addr_v2.nTime = start_time; + BOOST_CHECK(!addrman->Add({addr_v2}, source)); + std::vector<CAddress> vAddr3{addrman->GetAddr(/*max_addresses=*/0, /*max_pct=*/0, /*network=*/std::nullopt)}; + BOOST_CHECK_EQUAL(vAddr3.size(), 1U); + BOOST_CHECK_EQUAL(vAddr3.at(0).nServices, NODE_P2P_V2 | NODE_NETWORK_LIMITED); + + // SetServices() (used when we connected to them) overwrites existing service flags + addrman->SetServices(addr, NODE_NETWORK); + std::vector<CAddress> vAddr4{addrman->GetAddr(/*max_addresses=*/0, /*max_pct=*/0, /*network=*/std::nullopt)}; + BOOST_CHECK_EQUAL(vAddr4.size(), 1U); + BOOST_CHECK_EQUAL(vAddr4.at(0).nServices, NODE_NETWORK); + + // Promoting to Tried does not affect the service flags + BOOST_CHECK(addrman->Good(addr)); // addr has NODE_NONE + std::vector<CAddress> vAddr5{addrman->GetAddr(/*max_addresses=*/0, /*max_pct=*/0, /*network=*/std::nullopt)}; + BOOST_CHECK_EQUAL(vAddr5.size(), 1U); + BOOST_CHECK_EQUAL(vAddr5.at(0).nServices, NODE_NETWORK); + + // Adding service flags even works when the addr is in Tried + BOOST_CHECK(!addrman->Add({addr_v2}, source)); + std::vector<CAddress> vAddr6{addrman->GetAddr(/*max_addresses=*/0, /*max_pct=*/0, /*network=*/std::nullopt)}; + BOOST_CHECK_EQUAL(vAddr6.size(), 1U); + BOOST_CHECK_EQUAL(vAddr6.at(0).nServices, NODE_NETWORK | NODE_P2P_V2); } BOOST_AUTO_TEST_CASE(addrman_size) diff --git a/src/test/argsman_tests.cpp b/src/test/argsman_tests.cpp index 1f46efe464..340208a1c9 100644 --- a/src/test/argsman_tests.cpp +++ b/src/test/argsman_tests.cpp @@ -904,7 +904,7 @@ BOOST_FIXTURE_TEST_CASE(util_ArgsMerge, ArgsMergeTestingSetup) // If check below fails, should manually dump the results with: // - // ARGS_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=util_tests/util_ArgsMerge + // ARGS_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=argsman_tests/util_ArgsMerge // // And verify diff against previous results to make sure the changes are expected. // @@ -1007,7 +1007,7 @@ BOOST_FIXTURE_TEST_CASE(util_ChainMerge, ChainMergeTestingSetup) // If check below fails, should manually dump the results with: // - // CHAIN_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=util_tests/util_ChainMerge + // CHAIN_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=argsman_tests/util_ChainMerge // // And verify diff against previous results to make sure the changes are expected. // diff --git a/src/test/blockencodings_tests.cpp b/src/test/blockencodings_tests.cpp index 763f0f897e..05355fb21d 100644 --- a/src/test/blockencodings_tests.cpp +++ b/src/test/blockencodings_tests.cpp @@ -14,7 +14,7 @@ #include <boost/test/unit_test.hpp> -std::vector<std::pair<uint256, CTransactionRef>> extra_txn; +std::vector<CTransactionRef> extra_txn; BOOST_FIXTURE_TEST_SUITE(blockencodings_tests, RegTestingSetup) @@ -126,7 +126,7 @@ public: explicit TestHeaderAndShortIDs(const CBlock& block) : TestHeaderAndShortIDs(CBlockHeaderAndShortTxIDs{block}) {} - uint64_t GetShortID(const uint256& txhash) const { + uint64_t GetShortID(const Wtxid& txhash) const { DataStream stream{}; stream << *this; CBlockHeaderAndShortTxIDs base; @@ -155,8 +155,8 @@ BOOST_AUTO_TEST_CASE(NonCoinbasePreforwardRTTest) shortIDs.prefilledtxn.resize(1); shortIDs.prefilledtxn[0] = {1, block.vtx[1]}; shortIDs.shorttxids.resize(2); - shortIDs.shorttxids[0] = shortIDs.GetShortID(block.vtx[0]->GetHash()); - shortIDs.shorttxids[1] = shortIDs.GetShortID(block.vtx[2]->GetHash()); + shortIDs.shorttxids[0] = shortIDs.GetShortID(block.vtx[0]->GetWitnessHash()); + shortIDs.shorttxids[1] = shortIDs.GetShortID(block.vtx[2]->GetWitnessHash()); DataStream stream{}; stream << shortIDs; @@ -226,7 +226,7 @@ BOOST_AUTO_TEST_CASE(SufficientPreforwardRTTest) shortIDs.prefilledtxn[0] = {0, block.vtx[0]}; shortIDs.prefilledtxn[1] = {1, block.vtx[2]}; // id == 1 as it is 1 after index 1 shortIDs.shorttxids.resize(1); - shortIDs.shorttxids[0] = shortIDs.GetShortID(block.vtx[1]->GetHash()); + shortIDs.shorttxids[0] = shortIDs.GetShortID(block.vtx[1]->GetWitnessHash()); DataStream stream{}; stream << shortIDs; diff --git a/src/test/bswap_tests.cpp b/src/test/bswap_tests.cpp index 2be7122fc1..fe48e39c41 100644 --- a/src/test/bswap_tests.cpp +++ b/src/test/bswap_tests.cpp @@ -16,9 +16,9 @@ BOOST_AUTO_TEST_CASE(bswap_tests) uint16_t e1 = 0x3412; uint32_t e2 = 0xbc9a7856; uint64_t e3 = 0xbc9a78563412f0de; - BOOST_CHECK(bswap_16(u1) == e1); - BOOST_CHECK(bswap_32(u2) == e2); - BOOST_CHECK(bswap_64(u3) == e3); + BOOST_CHECK(internal_bswap_16(u1) == e1); + BOOST_CHECK(internal_bswap_32(u2) == e2); + BOOST_CHECK(internal_bswap_64(u3) == e3); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/coinstatsindex_tests.cpp b/src/test/coinstatsindex_tests.cpp index cc1ec49d41..08814c1499 100644 --- a/src/test/coinstatsindex_tests.cpp +++ b/src/test/coinstatsindex_tests.cpp @@ -70,7 +70,7 @@ BOOST_FIXTURE_TEST_CASE(coinstatsindex_initial_sync, TestChain100Setup) // SyncWithValidationInterfaceQueue() call below is also needed to ensure // TSAN always sees the test thread waiting for the notification thread, and // avoid potential false positive reports. - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); // Shutdown sequence (c.f. Shutdown() in init.cpp) coin_stats_index.Stop(); diff --git a/src/test/common_url_tests.cpp b/src/test/common_url_tests.cpp new file mode 100644 index 0000000000..065c7d97bc --- /dev/null +++ b/src/test/common_url_tests.cpp @@ -0,0 +1,75 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit/. + +#include <common/url.h> + +#include <string> + +#include <boost/test/unit_test.hpp> + +BOOST_AUTO_TEST_SUITE(common_url_tests) + +// These test vectors were ported from test/regress.c in the libevent library +// which used to be a dependency of the UrlDecode function. + +BOOST_AUTO_TEST_CASE(encode_decode_test) { + BOOST_CHECK_EQUAL(UrlDecode("Hello"), "Hello"); + BOOST_CHECK_EQUAL(UrlDecode("99"), "99"); + BOOST_CHECK_EQUAL(UrlDecode(""), ""); + BOOST_CHECK_EQUAL(UrlDecode("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789-.~_"), + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789-.~_"); + BOOST_CHECK_EQUAL(UrlDecode("%20"), " "); + BOOST_CHECK_EQUAL(UrlDecode("%FF%F0%E0"), "\xff\xf0\xe0"); + BOOST_CHECK_EQUAL(UrlDecode("%01%19"), "\x01\x19"); + BOOST_CHECK_EQUAL(UrlDecode("http%3A%2F%2Fwww.ietf.org%2Frfc%2Frfc3986.txt"), + "http://www.ietf.org/rfc/rfc3986.txt"); + BOOST_CHECK_EQUAL(UrlDecode("1%2B2%3D3"), "1+2=3"); +} + +BOOST_AUTO_TEST_CASE(decode_malformed_test) { + BOOST_CHECK_EQUAL(UrlDecode("%%xhello th+ere \xff"), "%%xhello th+ere \xff"); + + BOOST_CHECK_EQUAL(UrlDecode("%"), "%"); + BOOST_CHECK_EQUAL(UrlDecode("%%"), "%%"); + BOOST_CHECK_EQUAL(UrlDecode("%%%"), "%%%"); + BOOST_CHECK_EQUAL(UrlDecode("%%%%"), "%%%%"); + + BOOST_CHECK_EQUAL(UrlDecode("+"), "+"); + BOOST_CHECK_EQUAL(UrlDecode("++"), "++"); + + BOOST_CHECK_EQUAL(UrlDecode("?"), "?"); + BOOST_CHECK_EQUAL(UrlDecode("??"), "??"); + + BOOST_CHECK_EQUAL(UrlDecode("%G1"), "%G1"); + BOOST_CHECK_EQUAL(UrlDecode("%2"), "%2"); + BOOST_CHECK_EQUAL(UrlDecode("%ZX"), "%ZX"); + + BOOST_CHECK_EQUAL(UrlDecode("valid%20string%G1"), "valid string%G1"); + BOOST_CHECK_EQUAL(UrlDecode("%20invalid%ZX"), " invalid%ZX"); + BOOST_CHECK_EQUAL(UrlDecode("%20%G1%ZX"), " %G1%ZX"); + + BOOST_CHECK_EQUAL(UrlDecode("%1 "), "%1 "); + BOOST_CHECK_EQUAL(UrlDecode("% 9"), "% 9"); + BOOST_CHECK_EQUAL(UrlDecode(" %Z "), " %Z "); + BOOST_CHECK_EQUAL(UrlDecode(" % X"), " % X"); + + BOOST_CHECK_EQUAL(UrlDecode("%%ffg"), "%\xffg"); + BOOST_CHECK_EQUAL(UrlDecode("%fg"), "%fg"); + + BOOST_CHECK_EQUAL(UrlDecode("%-1"), "%-1"); + BOOST_CHECK_EQUAL(UrlDecode("%1-"), "%1-"); +} + +BOOST_AUTO_TEST_CASE(decode_lowercase_hex_test) { + BOOST_CHECK_EQUAL(UrlDecode("%f0%a0%b0"), "\xf0\xa0\xb0"); +} + +BOOST_AUTO_TEST_CASE(decode_internal_nulls_test) { + std::string result1{"\0\0x\0\0", 5}; + BOOST_CHECK_EQUAL(UrlDecode("%00%00x%00%00"), result1); + std::string result2{"abc\0\0", 5}; + BOOST_CHECK_EQUAL(UrlDecode("abc%00%00"), result2); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/compress_tests.cpp b/src/test/compress_tests.cpp index 264b47b07c..13c2740553 100644 --- a/src/test/compress_tests.cpp +++ b/src/test/compress_tests.cpp @@ -4,6 +4,7 @@ #include <compressor.h> #include <script/script.h> +#include <test/util/random.h> #include <test/util/setup_common.h> #include <stdint.h> @@ -131,4 +132,36 @@ BOOST_AUTO_TEST_CASE(compress_script_to_uncompressed_pubkey_id) BOOST_CHECK_EQUAL(out[0], 0x04 | (script[65] & 0x01)); // least significant bit (lsb) of last char of pubkey is mapped into out[0] } +BOOST_AUTO_TEST_CASE(compress_p2pk_scripts_not_on_curve) +{ + XOnlyPubKey x_not_on_curve; + do { + x_not_on_curve = XOnlyPubKey(g_insecure_rand_ctx.randbytes(32)); + } while (x_not_on_curve.IsFullyValid()); + + // Check that P2PK script with uncompressed pubkey [=> OP_PUSH65 <0x04 .....> OP_CHECKSIG] + // which is not fully valid (i.e. point is not on curve) can't be compressed + std::vector<unsigned char> pubkey_raw(65, 0); + pubkey_raw[0] = 4; + std::copy(x_not_on_curve.begin(), x_not_on_curve.end(), &pubkey_raw[1]); + CPubKey pubkey_not_on_curve(pubkey_raw); + assert(pubkey_not_on_curve.IsValid()); + assert(!pubkey_not_on_curve.IsFullyValid()); + CScript script = CScript() << ToByteVector(pubkey_not_on_curve) << OP_CHECKSIG; + BOOST_CHECK_EQUAL(script.size(), 67U); + + CompressedScript out; + bool done = CompressScript(script, out); + BOOST_CHECK_EQUAL(done, false); + + // Check that compressed P2PK script with uncompressed pubkey that is not fully + // valid (i.e. x coordinate of the pubkey is not on curve) can't be decompressed + CompressedScript compressed_script(x_not_on_curve.begin(), x_not_on_curve.end()); + for (unsigned int compression_id : {4, 5}) { + CScript uncompressed_script; + bool success = DecompressScript(uncompressed_script, compression_id, compressed_script); + BOOST_CHECK_EQUAL(success, false); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/crypto_tests.cpp b/src/test/crypto_tests.cpp index 0a6378adf4..46acc6fc9f 100644 --- a/src/test/crypto_tests.cpp +++ b/src/test/crypto_tests.cpp @@ -1060,28 +1060,6 @@ BOOST_AUTO_TEST_CASE(hkdf_hmac_sha256_l32_tests) "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d"); } -BOOST_AUTO_TEST_CASE(countbits_tests) -{ - FastRandomContext ctx; - for (unsigned int i = 0; i <= 64; ++i) { - if (i == 0) { - // Check handling of zero. - BOOST_CHECK_EQUAL(CountBits(0), 0U); - } else if (i < 10) { - for (uint64_t j = uint64_t{1} << (i - 1); (j >> i) == 0; ++j) { - // Exhaustively test up to 10 bits - BOOST_CHECK_EQUAL(CountBits(j), i); - } - } else { - for (int k = 0; k < 1000; k++) { - // Randomly test 1000 samples of each length above 10 bits. - uint64_t j = (uint64_t{1}) << (i - 1) | ctx.randbits(i - 1); - BOOST_CHECK_EQUAL(CountBits(j), i); - } - } - } -} - BOOST_AUTO_TEST_CASE(sha256d64) { for (int i = 0; i <= 32; ++i) { diff --git a/src/test/data/tx_valid.json b/src/test/data/tx_valid.json index b874f6f26c..70df0d0f69 100644 --- a/src/test/data/tx_valid.json +++ b/src/test/data/tx_valid.json @@ -319,6 +319,10 @@ [[["0000000000000000000000000000000000000000000000000000000000000100", 0, "HASH160 0x14 0x7c17aff532f22beb54069942f9bf567a66133eaf EQUAL"]], "0200000001000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000", "NONE"], +["Valid CHECKSEQUENCEVERIFY even with negative tx version number"], +[[["0000000000000000000000000000000000000000000000000000000000000100", 0, "HASH160 0x14 0x7c17aff532f22beb54069942f9bf567a66133eaf EQUAL"]], +"ffffffff01000100000000000000000000000000000000000000000000000000000000000000000000030251b2010000000100000000000000000000000000", "NONE"], + ["Valid P2WPKH (Private key of segwit tests is L5AQtV2HDm4xGsseLokK2VAT2EtYKcTm3c7HwqnJBFt9LdaQULsM)"], [[["0000000000000000000000000000000000000000000000000000000000000100", 0, "0x00 0x14 0x4c9c3dfac4207d5d8cb89df5722cb3d712385e3f", 1000]], "0100000000010100010000000000000000000000000000000000000000000000000000000000000000000000ffffffff01e8030000000000001976a9144c9c3dfac4207d5d8cb89df5722cb3d712385e3f88ac02483045022100cfb07164b36ba64c1b1e8c7720a56ad64d96f6ef332d3d37f9cb3c96477dc44502200a464cd7a9cf94cd70f66ce4f4f0625ef650052c7afcfe29d7d7e01830ff91ed012103596d3451025c19dbbdeb932d6bf8bfb4ad499b95b6f88db8899efac102e5fc7100000000", "NONE"], diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp index 0fef8c5906..5e9ae78681 100644 --- a/src/test/denialofservice_tests.cpp +++ b/src/test/denialofservice_tests.cpp @@ -16,7 +16,6 @@ #include <test/util/net.h> #include <test/util/random.h> #include <test/util/setup_common.h> -#include <timedata.h> #include <util/string.h> #include <util/time.h> #include <validation.h> @@ -72,7 +71,6 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction) /*local_services=*/ServiceFlags(NODE_NETWORK | NODE_WITNESS), /*version=*/PROTOCOL_VERSION, /*relay_txs=*/true); - TestOnlyResetTimeData(); // This test requires that we have a chain with non-zero work. { diff --git a/src/test/feefrac_tests.cpp b/src/test/feefrac_tests.cpp new file mode 100644 index 0000000000..5af3c3d7ed --- /dev/null +++ b/src/test/feefrac_tests.cpp @@ -0,0 +1,85 @@ +// Copyright (c) 2024-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 <util/feefrac.h> +#include <random.h> + +#include <boost/test/unit_test.hpp> + +BOOST_AUTO_TEST_SUITE(feefrac_tests) + +BOOST_AUTO_TEST_CASE(feefrac_operators) +{ + FeeFrac p1{1000, 100}, p2{500, 300}; + FeeFrac sum{1500, 400}; + FeeFrac diff{500, -200}; + FeeFrac empty{0, 0}; + FeeFrac zero_fee{0, 1}; // zero-fee allowed + + BOOST_CHECK(empty == FeeFrac{}); // same as no-args + + BOOST_CHECK(p1 == p1); + BOOST_CHECK(p1 + p2 == sum); + BOOST_CHECK(p1 - p2 == diff); + + FeeFrac p3{2000, 200}; + BOOST_CHECK(p1 != p3); // feefracs only equal if both fee and size are same + BOOST_CHECK(p2 != p3); + + FeeFrac p4{3000, 300}; + BOOST_CHECK(p1 == p4-p3); + BOOST_CHECK(p1 + p3 == p4); + + // Fee-rate comparison + BOOST_CHECK(p1 > p2); + BOOST_CHECK(p1 >= p2); + BOOST_CHECK(p1 >= p4-p3); + BOOST_CHECK(!(p1 >> p3)); // not strictly better + BOOST_CHECK(p1 >> p2); // strictly greater feerate + + BOOST_CHECK(p2 < p1); + BOOST_CHECK(p2 <= p1); + BOOST_CHECK(p1 <= p4-p3); + BOOST_CHECK(!(p3 << p1)); // not strictly worse + BOOST_CHECK(p2 << p1); // strictly lower feerate + + // "empty" comparisons + BOOST_CHECK(!(p1 >> empty)); // << will always result in false + BOOST_CHECK(!(p1 << empty)); + BOOST_CHECK(!(empty >> empty)); + BOOST_CHECK(!(empty << empty)); + + // empty is always bigger than everything else + BOOST_CHECK(empty > p1); + BOOST_CHECK(empty > p2); + BOOST_CHECK(empty > p3); + BOOST_CHECK(empty >= p1); + BOOST_CHECK(empty >= p2); + BOOST_CHECK(empty >= p3); + + // check "max" values for comparison + FeeFrac oversized_1{4611686000000, 4000000}; + FeeFrac oversized_2{184467440000000, 100000}; + + BOOST_CHECK(oversized_1 < oversized_2); + BOOST_CHECK(oversized_1 <= oversized_2); + BOOST_CHECK(oversized_1 << oversized_2); + BOOST_CHECK(oversized_1 != oversized_2); + + // Tests paths that use double arithmetic + FeeFrac busted{(static_cast<int64_t>(INT32_MAX)) + 1, INT32_MAX}; + BOOST_CHECK(!(busted < busted)); + + FeeFrac max_fee{2100000000000000, INT32_MAX}; + BOOST_CHECK(!(max_fee < max_fee)); + BOOST_CHECK(!(max_fee > max_fee)); + BOOST_CHECK(max_fee <= max_fee); + BOOST_CHECK(max_fee >= max_fee); + + FeeFrac max_fee2{1, 1}; + BOOST_CHECK(max_fee >= max_fee2); + +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/fuzz/addition_overflow.cpp b/src/test/fuzz/addition_overflow.cpp index 7b84bfda20..071e5fb029 100644 --- a/src/test/fuzz/addition_overflow.cpp +++ b/src/test/fuzz/addition_overflow.cpp @@ -11,14 +11,6 @@ #include <string> #include <vector> -#if defined(__has_builtin) -#if __has_builtin(__builtin_add_overflow) -#define HAVE_BUILTIN_ADD_OVERFLOW -#endif -#elif defined(__GNUC__) -#define HAVE_BUILTIN_ADD_OVERFLOW -#endif - namespace { template <typename T> void TestAdditionOverflow(FuzzedDataProvider& fuzzed_data_provider) @@ -32,7 +24,7 @@ void TestAdditionOverflow(FuzzedDataProvider& fuzzed_data_provider) assert(is_addition_overflow_custom == AdditionOverflow(j, i)); assert(maybe_add == CheckedAdd(j, i)); assert(sat_add == SaturatingAdd(j, i)); -#if defined(HAVE_BUILTIN_ADD_OVERFLOW) +#ifndef _MSC_VER T result_builtin; const bool is_addition_overflow_builtin = __builtin_add_overflow(i, j, &result_builtin); assert(is_addition_overflow_custom == is_addition_overflow_builtin); diff --git a/src/test/fuzz/addrman.cpp b/src/test/fuzz/addrman.cpp index ece396aadf..8a54cc656d 100644 --- a/src/test/fuzz/addrman.cpp +++ b/src/test/fuzz/addrman.cpp @@ -64,26 +64,13 @@ FUZZ_TARGET(data_stream_addr_man, .init = initialize_addrman) CNetAddr RandAddr(FuzzedDataProvider& fuzzed_data_provider, FastRandomContext& fast_random_context) { CNetAddr addr; - if (fuzzed_data_provider.remaining_bytes() > 1 && fuzzed_data_provider.ConsumeBool()) { - addr = ConsumeNetAddr(fuzzed_data_provider); - } else { - // The networks [1..6] correspond to CNetAddr::BIP155Network (private). - static const std::map<uint8_t, uint8_t> net_len_map = {{1, ADDR_IPV4_SIZE}, - {2, ADDR_IPV6_SIZE}, - {4, ADDR_TORV3_SIZE}, - {5, ADDR_I2P_SIZE}, - {6, ADDR_CJDNS_SIZE}}; - uint8_t net = fast_random_context.randrange(5) + 1; // [1..5] - if (net == 3) { - net = 6; + assert(!addr.IsValid()); + for (size_t i = 0; i < 8 && !addr.IsValid(); ++i) { + if (fuzzed_data_provider.remaining_bytes() > 1 && fuzzed_data_provider.ConsumeBool()) { + addr = ConsumeNetAddr(fuzzed_data_provider); + } else { + addr = ConsumeNetAddr(fuzzed_data_provider, &fast_random_context); } - - DataStream s{}; - - s << net; - s << fast_random_context.randbytes(net_len_map.at(net)); - - s >> CAddress::V2_NETWORK(addr); } // Return a dummy IPv4 5.5.5.5 if we generated an invalid address. diff --git a/src/test/fuzz/banman.cpp b/src/test/fuzz/banman.cpp index 4a040c56de..b26151f63c 100644 --- a/src/test/fuzz/banman.cpp +++ b/src/test/fuzz/banman.cpp @@ -70,11 +70,13 @@ FUZZ_TARGET(banman, .init = initialize_banman) fuzzed_data_provider, [&] { CNetAddr net_addr{ConsumeNetAddr(fuzzed_data_provider)}; - const std::optional<CNetAddr>& addr{LookupHost(net_addr.ToStringAddr(), /*fAllowLookup=*/false)}; - if (addr.has_value() && addr->IsValid()) { - net_addr = *addr; - } else { - contains_invalid = true; + if (!net_addr.IsCJDNS() || !net_addr.IsValid()) { + const std::optional<CNetAddr>& addr{LookupHost(net_addr.ToStringAddr(), /*fAllowLookup=*/false)}; + if (addr.has_value() && addr->IsValid()) { + net_addr = *addr; + } else { + contains_invalid = true; + } } ban_man.Ban(net_addr, ConsumeBanTimeOffset(fuzzed_data_provider), fuzzed_data_provider.ConsumeBool()); }, diff --git a/src/test/fuzz/bip324.cpp b/src/test/fuzz/bip324.cpp index 37c41f3895..8210e75cee 100644 --- a/src/test/fuzz/bip324.cpp +++ b/src/test/fuzz/bip324.cpp @@ -17,7 +17,7 @@ namespace { void Initialize() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::MAIN); } diff --git a/src/test/fuzz/descriptor_parse.cpp b/src/test/fuzz/descriptor_parse.cpp index 6ea315d4e2..b9a5560ffb 100644 --- a/src/test/fuzz/descriptor_parse.cpp +++ b/src/test/fuzz/descriptor_parse.cpp @@ -55,7 +55,7 @@ static void TestDescriptor(const Descriptor& desc, FlatSigningProvider& sig_prov void initialize_descriptor_parse() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::MAIN); } diff --git a/src/test/fuzz/deserialize.cpp b/src/test/fuzz/deserialize.cpp index ebc5673e71..c9a3bc86ac 100644 --- a/src/test/fuzz/deserialize.cpp +++ b/src/test/fuzz/deserialize.cpp @@ -33,7 +33,6 @@ #include <optional> #include <stdexcept> #include <stdint.h> -#include <unistd.h> using node::SnapshotMetadata; diff --git a/src/test/fuzz/feefrac.cpp b/src/test/fuzz/feefrac.cpp new file mode 100644 index 0000000000..2c7553360e --- /dev/null +++ b/src/test/fuzz/feefrac.cpp @@ -0,0 +1,123 @@ +// Copyright (c) 2024 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 <util/feefrac.h> +#include <test/fuzz/FuzzedDataProvider.h> +#include <test/fuzz/fuzz.h> +#include <test/fuzz/util.h> + +#include <compare> +#include <cstdint> +#include <iostream> + +namespace { + +/** Compute a * b, represented in 4x32 bits, highest limb first. */ +std::array<uint32_t, 4> Mul128(uint64_t a, uint64_t b) +{ + std::array<uint32_t, 4> ret{0, 0, 0, 0}; + + /** Perform ret += v << (32 * pos), at 128-bit precision. */ + auto add_fn = [&](uint64_t v, int pos) { + uint64_t accum{0}; + for (int i = 0; i + pos < 4; ++i) { + // Add current value at limb pos in ret. + accum += ret[3 - pos - i]; + // Add low or high half of v. + if (i == 0) accum += v & 0xffffffff; + if (i == 1) accum += v >> 32; + // Store lower half of result in limb pos in ret. + ret[3 - pos - i] = accum & 0xffffffff; + // Leave carry in accum. + accum >>= 32; + } + // Make sure no overflow. + assert(accum == 0); + }; + + // Multiply the 4 individual limbs (schoolbook multiply, with base 2^32). + add_fn((a & 0xffffffff) * (b & 0xffffffff), 0); + add_fn((a >> 32) * (b & 0xffffffff), 1); + add_fn((a & 0xffffffff) * (b >> 32), 1); + add_fn((a >> 32) * (b >> 32), 2); + return ret; +} + +/* comparison helper for std::array */ +std::strong_ordering compare_arrays(const std::array<uint32_t, 4>& a, const std::array<uint32_t, 4>& b) { + for (size_t i = 0; i < a.size(); ++i) { + if (a[i] != b[i]) return a[i] <=> b[i]; + } + return std::strong_ordering::equal; +} + +std::strong_ordering MulCompare(int64_t a1, int64_t a2, int64_t b1, int64_t b2) +{ + // Compute and compare signs. + int sign_a = (a1 == 0 ? 0 : a1 < 0 ? -1 : 1) * (a2 == 0 ? 0 : a2 < 0 ? -1 : 1); + int sign_b = (b1 == 0 ? 0 : b1 < 0 ? -1 : 1) * (b2 == 0 ? 0 : b2 < 0 ? -1 : 1); + if (sign_a != sign_b) return sign_a <=> sign_b; + + // Compute absolute values. + uint64_t abs_a1 = static_cast<uint64_t>(a1), abs_a2 = static_cast<uint64_t>(a2); + uint64_t abs_b1 = static_cast<uint64_t>(b1), abs_b2 = static_cast<uint64_t>(b2); + // Use (~x + 1) instead of the equivalent (-x) to silence the linter; mod 2^64 behavior is + // intentional here. + if (a1 < 0) abs_a1 = ~abs_a1 + 1; + if (a2 < 0) abs_a2 = ~abs_a2 + 1; + if (b1 < 0) abs_b1 = ~abs_b1 + 1; + if (b2 < 0) abs_b2 = ~abs_b2 + 1; + + // Compute products of absolute values. + auto mul_abs_a = Mul128(abs_a1, abs_a2); + auto mul_abs_b = Mul128(abs_b1, abs_b2); + if (sign_a < 0) { + return compare_arrays(mul_abs_b, mul_abs_a); + } else { + return compare_arrays(mul_abs_a, mul_abs_b); + } +} + +} // namespace + +FUZZ_TARGET(feefrac) +{ + FuzzedDataProvider provider(buffer.data(), buffer.size()); + + int64_t f1 = provider.ConsumeIntegral<int64_t>(); + int32_t s1 = provider.ConsumeIntegral<int32_t>(); + if (s1 == 0) f1 = 0; + FeeFrac fr1(f1, s1); + assert(fr1.IsEmpty() == (s1 == 0)); + + int64_t f2 = provider.ConsumeIntegral<int64_t>(); + int32_t s2 = provider.ConsumeIntegral<int32_t>(); + if (s2 == 0) f2 = 0; + FeeFrac fr2(f2, s2); + assert(fr2.IsEmpty() == (s2 == 0)); + + // Feerate comparisons + auto cmp_feerate = MulCompare(f1, s2, f2, s1); + assert(FeeRateCompare(fr1, fr2) == cmp_feerate); + assert((fr1 << fr2) == std::is_lt(cmp_feerate)); + assert((fr1 >> fr2) == std::is_gt(cmp_feerate)); + + // Compare with manual invocation of FeeFrac::Mul. + auto cmp_mul = FeeFrac::Mul(f1, s2) <=> FeeFrac::Mul(f2, s1); + assert(cmp_mul == cmp_feerate); + + // Same, but using FeeFrac::MulFallback. + auto cmp_fallback = FeeFrac::MulFallback(f1, s2) <=> FeeFrac::MulFallback(f2, s1); + assert(cmp_fallback == cmp_feerate); + + // Total order comparisons + auto cmp_total = std::is_eq(cmp_feerate) ? (s2 <=> s1) : cmp_feerate; + assert((fr1 <=> fr2) == cmp_total); + assert((fr1 < fr2) == std::is_lt(cmp_total)); + assert((fr1 > fr2) == std::is_gt(cmp_total)); + assert((fr1 <= fr2) == std::is_lteq(cmp_total)); + assert((fr1 >= fr2) == std::is_gteq(cmp_total)); + assert((fr1 == fr2) == std::is_eq(cmp_total)); + assert((fr1 != fr2) == std::is_neq(cmp_total)); +} diff --git a/src/test/fuzz/feeratediagram.cpp b/src/test/fuzz/feeratediagram.cpp new file mode 100644 index 0000000000..1a9c5ee946 --- /dev/null +++ b/src/test/fuzz/feeratediagram.cpp @@ -0,0 +1,133 @@ +// Copyright (c) 2023 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 <stdint.h> + +#include <vector> + +#include <util/feefrac.h> +#include <policy/rbf.h> + +#include <test/fuzz/fuzz.h> +#include <test/fuzz/util.h> + +#include <assert.h> + +namespace { + +/** Takes the pre-computed and topologically-valid chunks and generates a fee diagram which starts at FeeFrac of (0, 0) */ +std::vector<FeeFrac> BuildDiagramFromChunks(const Span<const FeeFrac> chunks) +{ + std::vector<FeeFrac> diagram; + diagram.reserve(chunks.size() + 1); + + diagram.emplace_back(0, 0); + for (auto& chunk : chunks) { + diagram.emplace_back(diagram.back() + chunk); + } + return diagram; +} + + +/** Evaluate a diagram at a specific size, returning the fee as a fraction. + * + * Fees in diagram cannot exceed 2^32, as the returned evaluation could overflow + * the FeeFrac::fee field in the result. */ +FeeFrac EvaluateDiagram(int32_t size, Span<const FeeFrac> diagram) +{ + assert(diagram.size() > 0); + unsigned not_above = 0; + unsigned not_below = diagram.size() - 1; + // If outside the range of diagram, extend begin/end. + if (size < diagram[not_above].size) return {diagram[not_above].fee, 1}; + if (size > diagram[not_below].size) return {diagram[not_below].fee, 1}; + // Perform bisection search to locate the diagram segment that size is in. + while (not_below > not_above + 1) { + unsigned mid = (not_below + not_above) / 2; + if (diagram[mid].size <= size) not_above = mid; + if (diagram[mid].size >= size) not_below = mid; + } + // If the size matches a transition point between segments, return its fee. + if (not_below == not_above) return {diagram[not_below].fee, 1}; + // Otherwise, interpolate. + auto dir_coef = diagram[not_below] - diagram[not_above]; + assert(dir_coef.size > 0); + // Let A = diagram[not_above] and B = diagram[not_below] + const auto& point_a = diagram[not_above]; + // We want to return: + // A.fee + (B.fee - A.fee) / (B.size - A.size) * (size - A.size) + // = A.fee + dir_coef.fee / dir_coef.size * (size - A.size) + // = (A.fee * dir_coef.size + dir_coef.fee * (size - A.size)) / dir_coef.size + assert(size >= point_a.size); + return {point_a.fee * dir_coef.size + dir_coef.fee * (size - point_a.size), dir_coef.size}; +} + +std::weak_ordering CompareFeeFracWithDiagram(const FeeFrac& ff, Span<const FeeFrac> diagram) +{ + return FeeRateCompare(FeeFrac{ff.fee, 1}, EvaluateDiagram(ff.size, diagram)); +} + +std::partial_ordering CompareDiagrams(Span<const FeeFrac> dia1, Span<const FeeFrac> dia2) +{ + bool all_ge = true; + bool all_le = true; + for (const auto p1 : dia1) { + auto cmp = CompareFeeFracWithDiagram(p1, dia2); + if (std::is_lt(cmp)) all_ge = false; + if (std::is_gt(cmp)) all_le = false; + } + for (const auto p2 : dia2) { + auto cmp = CompareFeeFracWithDiagram(p2, dia1); + if (std::is_lt(cmp)) all_le = false; + if (std::is_gt(cmp)) all_ge = false; + } + if (all_ge && all_le) return std::partial_ordering::equivalent; + if (all_ge && !all_le) return std::partial_ordering::greater; + if (!all_ge && all_le) return std::partial_ordering::less; + return std::partial_ordering::unordered; +} + +void PopulateChunks(FuzzedDataProvider& fuzzed_data_provider, std::vector<FeeFrac>& chunks) +{ + chunks.clear(); + + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 50) + { + chunks.emplace_back(fuzzed_data_provider.ConsumeIntegralInRange<int64_t>(INT32_MIN>>1, INT32_MAX>>1), fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(1, 1000000)); + } + return; +} + +} // namespace + +FUZZ_TARGET(build_and_compare_feerate_diagram) +{ + // Generate a random set of chunks + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + std::vector<FeeFrac> chunks1, chunks2; + FeeFrac empty{0, 0}; + + PopulateChunks(fuzzed_data_provider, chunks1); + PopulateChunks(fuzzed_data_provider, chunks2); + + std::vector<FeeFrac> diagram1{BuildDiagramFromChunks(chunks1)}; + std::vector<FeeFrac> diagram2{BuildDiagramFromChunks(chunks2)}; + + assert(diagram1.front() == empty); + assert(diagram2.front() == empty); + + auto real = CompareChunks(chunks1, chunks2); + auto sim = CompareDiagrams(diagram1, diagram2); + assert(real == sim); + + // Do explicit evaluation at up to 1000 points, and verify consistency with the result. + LIMITED_WHILE(fuzzed_data_provider.remaining_bytes(), 1000) { + int32_t size = fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(0, diagram2.back().size); + auto eval1 = EvaluateDiagram(size, diagram1); + auto eval2 = EvaluateDiagram(size, diagram2); + auto cmp = FeeRateCompare(eval1, eval2); + if (std::is_lt(cmp)) assert(!std::is_gt(real)); + if (std::is_gt(cmp)) assert(!std::is_lt(real)); + } +} diff --git a/src/test/fuzz/fuzz.cpp b/src/test/fuzz/fuzz.cpp index 6de480ff15..f9915187bd 100644 --- a/src/test/fuzz/fuzz.cpp +++ b/src/test/fuzz/fuzz.cpp @@ -25,7 +25,6 @@ #include <memory> #include <string> #include <tuple> -#include <unistd.h> #include <utility> #include <vector> @@ -35,6 +34,8 @@ __AFL_FUZZ_INIT(); const std::function<void(const std::string&)> G_TEST_LOG_FUN{}; +const std::function<std::string()> G_TEST_GET_FULL_NAME{}; + /** * A copy of the command line arguments that start with `--`. * First `LLVMFuzzerInitialize()` is called, which saves the arguments to `g_args`. @@ -81,7 +82,7 @@ static const TypeTestOneInput* g_test_one_input{nullptr}; void initialize() { // Terminate immediately if a fuzzing harness ever tries to create a TCP socket. - CreateSock = [](const CService&) -> std::unique_ptr<Sock> { std::terminate(); }; + CreateSock = [](const sa_family_t&) -> std::unique_ptr<Sock> { std::terminate(); }; // Terminate immediately if a fuzzing harness ever tries to perform a DNS lookup. g_dns_lookup = [](const std::string& name, bool allow_lookup) { @@ -133,9 +134,9 @@ void initialize() #if defined(PROVIDE_FUZZ_MAIN_FUNCTION) static bool read_stdin(std::vector<uint8_t>& data) { - uint8_t buffer[1024]; - ssize_t length = 0; - while ((length = read(STDIN_FILENO, buffer, 1024)) > 0) { + std::istream::char_type buffer[1024]; + std::streamsize length; + while ((std::cin.read(buffer, 1024), length = std::cin.gcount()) > 0) { data.insert(data.end(), buffer, buffer + length); } return length == 0; diff --git a/src/test/fuzz/headerssync.cpp b/src/test/fuzz/headerssync.cpp index 62f6bbaffe..1aa878bd6d 100644 --- a/src/test/fuzz/headerssync.cpp +++ b/src/test/fuzz/headerssync.cpp @@ -108,7 +108,7 @@ FUZZ_TARGET(headers_sync_state, .init = initialize_headers_sync_state_fuzz) // If we get to redownloading, the presynced headers need // to have the min amount of work on them. - assert(CalculateHeadersWork(all_headers) >= min_work); + assert(CalculateClaimedHeadersWork(all_headers) >= min_work); } } diff --git a/src/test/fuzz/integer.cpp b/src/test/fuzz/integer.cpp index 9b97958a5d..db246bb84e 100644 --- a/src/test/fuzz/integer.cpp +++ b/src/test/fuzz/integer.cpp @@ -80,7 +80,6 @@ FUZZ_TARGET(integer, .init = initialize_integer) static const uint256 u256_max(uint256S("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")); const std::vector<uint256> v256{u256, u256_min, u256_max}; (void)ComputeMerkleRoot(v256); - (void)CountBits(u64); (void)DecompressAmount(u64); { if (std::optional<CAmount> parsed = ParseMoney(FormatMoney(i64))) { @@ -213,7 +212,6 @@ FUZZ_TARGET(integer, .init = initialize_integer) { const ServiceFlags service_flags = (ServiceFlags)u64; - (void)HasAllDesirableServiceFlags(service_flags); (void)MayHaveUsefulAddressDB(service_flags); } diff --git a/src/test/fuzz/key.cpp b/src/test/fuzz/key.cpp index 9e1e318e02..d389a29575 100644 --- a/src/test/fuzz/key.cpp +++ b/src/test/fuzz/key.cpp @@ -32,7 +32,7 @@ void initialize_key() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::REGTEST); } diff --git a/src/test/fuzz/key_io.cpp b/src/test/fuzz/key_io.cpp index 5f98f2b7f1..aefdefe233 100644 --- a/src/test/fuzz/key_io.cpp +++ b/src/test/fuzz/key_io.cpp @@ -14,7 +14,7 @@ void initialize_key_io() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::MAIN); } diff --git a/src/test/fuzz/message.cpp b/src/test/fuzz/message.cpp index b5c95441f8..75baaa2754 100644 --- a/src/test/fuzz/message.cpp +++ b/src/test/fuzz/message.cpp @@ -19,7 +19,7 @@ void initialize_message() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::REGTEST); } diff --git a/src/test/fuzz/miniscript.cpp b/src/test/fuzz/miniscript.cpp index 0d0ee59ab3..f10007222c 100644 --- a/src/test/fuzz/miniscript.cpp +++ b/src/test/fuzz/miniscript.cpp @@ -391,6 +391,7 @@ std::optional<NodeInfo> ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& bool allow_K = (type_needed == ""_mst) || (type_needed << "K"_mst); bool allow_V = (type_needed == ""_mst) || (type_needed << "V"_mst); bool allow_W = (type_needed == ""_mst) || (type_needed << "W"_mst); + static constexpr auto B{"B"_mst}, K{"K"_mst}, V{"V"_mst}, W{"W"_mst}; switch (provider.ConsumeIntegral<uint8_t>()) { case 0: @@ -440,22 +441,22 @@ std::optional<NodeInfo> ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& } case 11: if (!(allow_B || allow_K || allow_V)) return {}; - return {{{"B"_mst, type_needed, type_needed}, Fragment::ANDOR}}; + return {{{B, type_needed, type_needed}, Fragment::ANDOR}}; case 12: if (!(allow_B || allow_K || allow_V)) return {}; - return {{{"V"_mst, type_needed}, Fragment::AND_V}}; + return {{{V, type_needed}, Fragment::AND_V}}; case 13: if (!allow_B) return {}; - return {{{"B"_mst, "W"_mst}, Fragment::AND_B}}; + return {{{B, W}, Fragment::AND_B}}; case 15: if (!allow_B) return {}; - return {{{"B"_mst, "W"_mst}, Fragment::OR_B}}; + return {{{B, W}, Fragment::OR_B}}; case 16: if (!allow_V) return {}; - return {{{"B"_mst, "V"_mst}, Fragment::OR_C}}; + return {{{B, V}, Fragment::OR_C}}; case 17: if (!allow_B) return {}; - return {{{"B"_mst, "B"_mst}, Fragment::OR_D}}; + return {{{B, B}, Fragment::OR_D}}; case 18: if (!(allow_B || allow_K || allow_V)) return {}; return {{{type_needed, type_needed}, Fragment::OR_I}}; @@ -472,25 +473,25 @@ std::optional<NodeInfo> ConsumeNodeStable(MsCtx script_ctx, FuzzedDataProvider& } case 20: if (!allow_W) return {}; - return {{{"B"_mst}, Fragment::WRAP_A}}; + return {{{B}, Fragment::WRAP_A}}; case 21: if (!allow_W) return {}; - return {{{"B"_mst}, Fragment::WRAP_S}}; + return {{{B}, Fragment::WRAP_S}}; case 22: if (!allow_B) return {}; - return {{{"K"_mst}, Fragment::WRAP_C}}; + return {{{K}, Fragment::WRAP_C}}; case 23: if (!allow_B) return {}; - return {{{"V"_mst}, Fragment::WRAP_D}}; + return {{{V}, Fragment::WRAP_D}}; case 24: if (!allow_V) return {}; - return {{{"B"_mst}, Fragment::WRAP_V}}; + return {{{B}, Fragment::WRAP_V}}; case 25: if (!allow_B) return {}; - return {{{"B"_mst}, Fragment::WRAP_J}}; + return {{{B}, Fragment::WRAP_J}}; case 26: if (!allow_B) return {}; - return {{{"B"_mst}, Fragment::WRAP_N}}; + return {{{B}, Fragment::WRAP_N}}; case 27: { if (!allow_B || !IsTapscript(script_ctx)) return {}; const auto k = provider.ConsumeIntegral<uint16_t>(); @@ -528,20 +529,23 @@ struct SmartInfo { /* Construct a set of interesting type requirements to reason with (sections of BKVWzondu). */ std::vector<Type> types; + static constexpr auto B_mst{"B"_mst}, K_mst{"K"_mst}, V_mst{"V"_mst}, W_mst{"W"_mst}; + static constexpr auto d_mst{"d"_mst}, n_mst{"n"_mst}, o_mst{"o"_mst}, u_mst{"u"_mst}, z_mst{"z"_mst}; + static constexpr auto NONE_mst{""_mst}; for (int base = 0; base < 4; ++base) { /* select from B,K,V,W */ - Type type_base = base == 0 ? "B"_mst : base == 1 ? "K"_mst : base == 2 ? "V"_mst : "W"_mst; + Type type_base = base == 0 ? B_mst : base == 1 ? K_mst : base == 2 ? V_mst : W_mst; for (int zo = 0; zo < 3; ++zo) { /* select from z,o,(none) */ - Type type_zo = zo == 0 ? "z"_mst : zo == 1 ? "o"_mst : ""_mst; + Type type_zo = zo == 0 ? z_mst : zo == 1 ? o_mst : NONE_mst; for (int n = 0; n < 2; ++n) { /* select from (none),n */ if (zo == 0 && n == 1) continue; /* z conflicts with n */ if (base == 3 && n == 1) continue; /* W conflicts with n */ - Type type_n = n == 0 ? ""_mst : "n"_mst; + Type type_n = n == 0 ? NONE_mst : n_mst; for (int d = 0; d < 2; ++d) { /* select from (none),d */ if (base == 2 && d == 1) continue; /* V conflicts with d */ - Type type_d = d == 0 ? ""_mst : "d"_mst; + Type type_d = d == 0 ? NONE_mst : d_mst; for (int u = 0; u < 2; ++u) { /* select from (none),u */ if (base == 2 && u == 1) continue; /* V conflicts with u */ - Type type_u = u == 0 ? ""_mst : "u"_mst; + Type type_u = u == 0 ? NONE_mst : u_mst; Type type = type_base | type_zo | type_n | type_d | type_u; types.push_back(type); } @@ -683,7 +687,7 @@ struct SmartInfo /* Find which types are useful. The fuzzer logic only cares about constructing * B,V,K,W nodes, so any type that isn't needed in any recipe (directly or * indirectly) for the construction of those is uninteresting. */ - std::set<Type> useful_types{"B"_mst, "V"_mst, "K"_mst, "W"_mst}; + std::set<Type> useful_types{B_mst, V_mst, K_mst, W_mst}; // Find the transitive closure by adding types until the set of types does not change. while (true) { size_t set_size = useful_types.size(); @@ -1197,7 +1201,7 @@ void TestNode(const MsCtx script_ctx, const NodeRef& node, FuzzedDataProvider& p void FuzzInit() { - ECC_Start(); + static ECC_Context ecc_context{}; TEST_DATA.Init(); } diff --git a/src/test/fuzz/multiplication_overflow.cpp b/src/test/fuzz/multiplication_overflow.cpp index fbe4d061bf..a762a4dfe3 100644 --- a/src/test/fuzz/multiplication_overflow.cpp +++ b/src/test/fuzz/multiplication_overflow.cpp @@ -2,10 +2,6 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#if defined(HAVE_CONFIG_H) -#include <config/bitcoin-config.h> -#endif - #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> @@ -21,7 +17,7 @@ void TestMultiplicationOverflow(FuzzedDataProvider& fuzzed_data_provider) const T i = fuzzed_data_provider.ConsumeIntegral<T>(); const T j = fuzzed_data_provider.ConsumeIntegral<T>(); const bool is_multiplication_overflow_custom = MultiplicationOverflow(i, j); -#if defined(HAVE_BUILTIN_MUL_OVERFLOW) +#ifndef _MSC_VER T result_builtin; const bool is_multiplication_overflow_builtin = __builtin_mul_overflow(i, j, &result_builtin); assert(is_multiplication_overflow_custom == is_multiplication_overflow_builtin); diff --git a/src/test/fuzz/net.cpp b/src/test/fuzz/net.cpp index c882bd766a..e8b1480c5b 100644 --- a/src/test/fuzz/net.cpp +++ b/src/test/fuzz/net.cpp @@ -77,3 +77,40 @@ FUZZ_TARGET(net, .init = initialize_net) (void)node.HasPermission(net_permission_flags); (void)node.ConnectedThroughNetwork(); } + +FUZZ_TARGET(local_address, .init = initialize_net) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + CService service{ConsumeService(fuzzed_data_provider)}; + CNode node{ConsumeNode(fuzzed_data_provider)}; + { + LOCK(g_maplocalhost_mutex); + mapLocalHost.clear(); + } + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) { + CallOneOf( + fuzzed_data_provider, + [&] { + service = ConsumeService(fuzzed_data_provider); + }, + [&] { + const bool added{AddLocal(service, fuzzed_data_provider.ConsumeIntegralInRange<int>(0, LOCAL_MAX - 1))}; + if (!added) return; + assert(service.IsRoutable()); + assert(IsLocal(service)); + assert(SeenLocal(service)); + }, + [&] { + (void)RemoveLocal(service); + }, + [&] { + (void)SeenLocal(service); + }, + [&] { + (void)IsLocal(service); + }, + [&] { + (void)GetLocalAddress(node); + }); + } +} diff --git a/src/test/fuzz/net_permissions.cpp b/src/test/fuzz/net_permissions.cpp index ae343602e9..811c0de4b9 100644 --- a/src/test/fuzz/net_permissions.cpp +++ b/src/test/fuzz/net_permissions.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include <net_permissions.h> +#include <netbase.h> #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> @@ -17,7 +18,7 @@ FUZZ_TARGET(net_permissions) { FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); - const std::string s = fuzzed_data_provider.ConsumeRandomLengthString(32); + const std::string s = fuzzed_data_provider.ConsumeRandomLengthString(1000); const NetPermissionFlags net_permission_flags = ConsumeWeakEnum(fuzzed_data_provider, ALL_NET_PERMISSION_FLAGS); NetWhitebindPermissions net_whitebind_permissions; @@ -31,8 +32,9 @@ FUZZ_TARGET(net_permissions) } NetWhitelistPermissions net_whitelist_permissions; + ConnectionDirection connection_direction; bilingual_str error_net_whitelist_permissions; - if (NetWhitelistPermissions::TryParse(s, net_whitelist_permissions, error_net_whitelist_permissions)) { + if (NetWhitelistPermissions::TryParse(s, net_whitelist_permissions, connection_direction, error_net_whitelist_permissions)) { (void)NetPermissions::ToStrings(net_whitelist_permissions.m_flags); (void)NetPermissions::AddFlag(net_whitelist_permissions.m_flags, net_permission_flags); assert(NetPermissions::HasFlag(net_whitelist_permissions.m_flags, net_permission_flags)); diff --git a/src/test/fuzz/netaddress.cpp b/src/test/fuzz/netaddress.cpp index 5141d3362d..4803cdccad 100644 --- a/src/test/fuzz/netaddress.cpp +++ b/src/test/fuzz/netaddress.cpp @@ -26,6 +26,12 @@ FUZZ_TARGET(netaddress) if (net_addr.GetNetwork() == Network::NET_ONION) { assert(net_addr.IsTor()); } + if (net_addr.GetNetwork() == Network::NET_I2P) { + assert(net_addr.IsI2P()); + } + if (net_addr.GetNetwork() == Network::NET_CJDNS) { + assert(net_addr.IsCJDNS()); + } if (net_addr.GetNetwork() == Network::NET_INTERNAL) { assert(net_addr.IsInternal()); } @@ -69,6 +75,12 @@ FUZZ_TARGET(netaddress) if (net_addr.IsTor()) { assert(net_addr.GetNetwork() == Network::NET_ONION); } + if (net_addr.IsI2P()) { + assert(net_addr.GetNetwork() == Network::NET_I2P); + } + if (net_addr.IsCJDNS()) { + assert(net_addr.GetNetwork() == Network::NET_CJDNS); + } (void)net_addr.IsValid(); (void)net_addr.ToStringAddr(); diff --git a/src/test/fuzz/p2p_transport_serialization.cpp b/src/test/fuzz/p2p_transport_serialization.cpp index a205ce19f4..f6d82c3001 100644 --- a/src/test/fuzz/p2p_transport_serialization.cpp +++ b/src/test/fuzz/p2p_transport_serialization.cpp @@ -25,7 +25,7 @@ std::vector<std::string> g_all_messages; void initialize_p2p_transport_serialization() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::REGTEST); g_all_messages = getAllNetMessageTypes(); std::sort(g_all_messages.begin(), g_all_messages.end()); @@ -354,6 +354,7 @@ std::unique_ptr<Transport> MakeV2Transport(NodeId nodeid, bool initiator, RNG& r } else { // If it's longer, generate it from the RNG. This avoids having large amounts of // (hopefully) irrelevant data needing to be stored in the fuzzer data. + garb.resize(garb_len); for (auto& v : garb) v = uint8_t(rng()); } // Retrieve entropy diff --git a/src/test/fuzz/package_eval.cpp b/src/test/fuzz/package_eval.cpp index 5a08d0ff44..c201118bce 100644 --- a/src/test/fuzz/package_eval.cpp +++ b/src/test/fuzz/package_eval.cpp @@ -6,6 +6,7 @@ #include <node/context.h> #include <node/mempool_args.h> #include <node/miner.h> +#include <policy/v3_policy.h> #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> @@ -46,7 +47,7 @@ void initialize_tx_pool() g_outpoints_coinbase_init_mature.push_back(prevout); } } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); } struct OutpointsUpdater final : public CValidationInterface { @@ -119,7 +120,8 @@ CTxMemPool MakeMempool(FuzzedDataProvider& fuzzed_data_provider, const NodeConte mempool_opts.limits.descendant_size_vbytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 202) * 1'000; mempool_opts.max_size_bytes = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 200) * 1'000'000; mempool_opts.expiry = std::chrono::hours{fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 999)}; - nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(1, 999); + // Only interested in 2 cases: sigop cost 0 or when single legacy sigop cost is >> 1KvB + nBytesPerSigOp = fuzzed_data_provider.ConsumeIntegralInRange<unsigned>(0, 1) * 10'000; mempool_opts.check_ratio = 1; mempool_opts.require_standard = fuzzed_data_provider.ConsumeBool(); @@ -145,7 +147,7 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool) } auto outpoints_updater = std::make_shared<OutpointsUpdater>(mempool_outpoints); - RegisterSharedValidationInterface(outpoints_updater); + node.validation_signals->RegisterSharedValidationInterface(outpoints_updater); CTxMemPool tx_pool_{MakeMempool(fuzzed_data_provider, node)}; MockedTxPool& tx_pool = *static_cast<MockedTxPool*>(&tx_pool_); @@ -171,11 +173,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool) // Create transaction to add to the mempool const CTransactionRef tx = [&] { CMutableTransaction tx_mut; - tx_mut.nVersion = CTransaction::CURRENT_VERSION; + tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION; tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>(); // Last tx will sweep all outpoints in package const auto num_in = last_tx ? package_outpoints.size() : fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size()); - const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2); + auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, mempool_outpoints.size() * 2); auto& outpoints = last_tx ? package_outpoints : mempool_outpoints; @@ -211,17 +213,24 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool) tx_mut.vin.push_back(tx_mut.vin.back()); } - // Refer to a non-existant input + // Refer to a non-existent input if (fuzzed_data_provider.ConsumeBool()) { tx_mut.vin.emplace_back(); } + // Make a p2pk output to make sigops adjusted vsize to violate v3, potentially, which is never spent + if (last_tx && amount_in > 1000 && fuzzed_data_provider.ConsumeBool()) { + tx_mut.vout.emplace_back(1000, CScript() << std::vector<unsigned char>(33, 0x02) << OP_CHECKSIG); + // Don't add any other outputs. + num_out = 1; + amount_in -= 1000; + } + const auto amount_fee = fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(0, amount_in); const auto amount_out = (amount_in - amount_fee) / num_out; for (int i = 0; i < num_out; ++i) { tx_mut.vout.emplace_back(amount_out, P2WSH_EMPTY); } - // TODO vary transaction sizes to catch size-related issues auto tx = MakeTransactionRef(tx_mut); // Restore previously removed outpoints, except in-package outpoints if (!last_tx) { @@ -260,28 +269,36 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool) // Remember all added transactions std::set<CTransactionRef> added; auto txr = std::make_shared<TransactionsDelta>(added); - RegisterSharedValidationInterface(txr); - const bool bypass_limits = fuzzed_data_provider.ConsumeBool(); + node.validation_signals->RegisterSharedValidationInterface(txr); // When there are multiple transactions in the package, we call ProcessNewPackage(txs, test_accept=false) // and AcceptToMemoryPool(txs.back(), test_accept=true). When there is only 1 transaction, we might flip it // (the package is a test accept and ATMP is a submission). auto single_submit = txs.size() == 1 && fuzzed_data_provider.ConsumeBool(); + // Exercise client_maxfeerate logic + std::optional<CFeeRate> client_maxfeerate{}; + if (fuzzed_data_provider.ConsumeBool()) { + client_maxfeerate = CFeeRate(fuzzed_data_provider.ConsumeIntegralInRange<CAmount>(-1, 50 * COIN), 100); + } + const auto result_package = WITH_LOCK(::cs_main, - return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit)); + return ProcessNewPackage(chainstate, tx_pool, txs, /*test_accept=*/single_submit, client_maxfeerate)); - const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(), bypass_limits, /*test_accept=*/!single_submit)); - const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID; + // Always set bypass_limits to false because it is not supported in ProcessNewPackage and + // can be a source of divergence. + const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, txs.back(), GetTime(), + /*bypass_limits=*/false, /*test_accept=*/!single_submit)); + const bool passed = res.m_result_type == MempoolAcceptResult::ResultType::VALID; - SyncWithValidationInterfaceQueue(); - UnregisterSharedValidationInterface(txr); + node.validation_signals->SyncWithValidationInterfaceQueue(); + node.validation_signals->UnregisterSharedValidationInterface(txr); // There is only 1 transaction in the package. We did a test-package-accept and a ATMP if (single_submit) { - Assert(accepted != added.empty()); - Assert(accepted == res.m_state.IsValid()); - if (accepted) { + Assert(passed != added.empty()); + Assert(passed == res.m_state.IsValid()); + if (passed) { Assert(added.size() == 1); Assert(txs.back() == *added.begin()); } @@ -295,9 +312,11 @@ FUZZ_TARGET(tx_package_eval, .init = initialize_tx_pool) // This is empty if it fails early checks, or "full" if transactions are looked at deeper Assert(result_package.m_tx_results.size() == txs.size() || result_package.m_tx_results.empty()); } + + CheckMempoolV3Invariants(tx_pool); } - UnregisterSharedValidationInterface(outpoints_updater); + node.validation_signals->UnregisterSharedValidationInterface(outpoints_updater); WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1)); } diff --git a/src/test/fuzz/partially_downloaded_block.cpp b/src/test/fuzz/partially_downloaded_block.cpp index 4a4b46da60..2bf47930f4 100644 --- a/src/test/fuzz/partially_downloaded_block.cpp +++ b/src/test/fuzz/partially_downloaded_block.cpp @@ -60,7 +60,7 @@ FUZZ_TARGET(partially_downloaded_block, .init = initialize_pdb) // The coinbase is always available available.insert(0); - std::vector<std::pair<uint256, CTransactionRef>> extra_txn; + std::vector<CTransactionRef> extra_txn; for (size_t i = 1; i < block->vtx.size(); ++i) { auto tx{block->vtx[i]}; @@ -68,11 +68,11 @@ FUZZ_TARGET(partially_downloaded_block, .init = initialize_pdb) bool add_to_mempool{fuzzed_data_provider.ConsumeBool()}; if (add_to_extra_txn) { - extra_txn.emplace_back(tx->GetWitnessHash(), tx); + extra_txn.emplace_back(tx); available.insert(i); } - if (add_to_mempool) { + if (add_to_mempool && !pool.exists(GenTxid::Txid(tx->GetHash()))) { LOCK2(cs_main, pool.cs); pool.addUnchecked(ConsumeTxMemPoolEntry(fuzzed_data_provider, *tx)); available.insert(i); diff --git a/src/test/fuzz/poolresource.cpp b/src/test/fuzz/poolresource.cpp index ce64ef6472..f764d9f8db 100644 --- a/src/test/fuzz/poolresource.cpp +++ b/src/test/fuzz/poolresource.cpp @@ -63,9 +63,9 @@ public: { if (m_total_allocated > 0x1000000) return; size_t alignment_bits = m_provider.ConsumeIntegralInRange<size_t>(0, 7); - size_t alignment = 1 << alignment_bits; + size_t alignment = size_t{1} << alignment_bits; size_t size_bits = m_provider.ConsumeIntegralInRange<size_t>(0, 16 - alignment_bits); - size_t size = m_provider.ConsumeIntegralInRange<size_t>(1U << size_bits, (1U << (size_bits + 1)) - 1U) << alignment_bits; + size_t size = m_provider.ConsumeIntegralInRange<size_t>(size_t{1} << size_bits, (size_t{1} << (size_bits + 1)) - 1U) << alignment_bits; Allocate(size, alignment); } diff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp index 56b391ed5c..a467fd5382 100644 --- a/src/test/fuzz/process_message.cpp +++ b/src/test/fuzz/process_message.cpp @@ -47,7 +47,7 @@ void initialize_process_message() for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { MineBlock(g_setup->m_node, CScript() << OP_TRUE); } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); } FUZZ_TARGET(process_message, .init = initialize_process_message) @@ -89,6 +89,6 @@ FUZZ_TARGET(process_message, .init = initialize_process_message) } g_setup->m_node.peerman->SendMessages(&p2p_node); } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); g_setup->m_node.connman->StopNodes(); } diff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp index 6b264907b5..38acd432fa 100644 --- a/src/test/fuzz/process_messages.cpp +++ b/src/test/fuzz/process_messages.cpp @@ -37,7 +37,7 @@ void initialize_process_messages() for (int i = 0; i < 2 * COINBASE_MATURITY; i++) { MineBlock(g_setup->m_node, CScript() << OP_TRUE); } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); } FUZZ_TARGET(process_messages, .init = initialize_process_messages) @@ -89,6 +89,6 @@ FUZZ_TARGET(process_messages, .init = initialize_process_messages) g_setup->m_node.peerman->SendMessages(&random_node); } } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); g_setup->m_node.connman->StopNodes(); } diff --git a/src/test/fuzz/rbf.cpp b/src/test/fuzz/rbf.cpp index aa6385d12d..64785948f6 100644 --- a/src/test/fuzz/rbf.cpp +++ b/src/test/fuzz/rbf.cpp @@ -23,12 +23,30 @@ namespace { const BasicTestingSetup* g_setup; } // namespace +const int NUM_ITERS = 10000; + +std::vector<COutPoint> g_outpoints; + void initialize_rbf() { static const auto testing_setup = MakeNoLogFileContext<>(); g_setup = testing_setup.get(); } +void initialize_package_rbf() +{ + static const auto testing_setup = MakeNoLogFileContext<>(); + g_setup = testing_setup.get(); + + // Create a fixed set of unique "UTXOs" to source parents from + // to avoid fuzzer giving circular references + for (int i = 0; i < NUM_ITERS; ++i) { + g_outpoints.emplace_back(); + g_outpoints.back().n = i; + } + +} + FUZZ_TARGET(rbf, .init = initialize_rbf) { FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); @@ -40,7 +58,7 @@ FUZZ_TARGET(rbf, .init = initialize_rbf) CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node)}; - LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), NUM_ITERS) { const std::optional<CMutableTransaction> another_mtx = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); if (!another_mtx) { @@ -63,3 +81,124 @@ FUZZ_TARGET(rbf, .init = initialize_rbf) (void)IsRBFOptIn(tx, pool); } } + +FUZZ_TARGET(package_rbf, .init = initialize_package_rbf) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + SetMockTime(ConsumeTime(fuzzed_data_provider)); + + std::optional<CMutableTransaction> child = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); + if (!child) return; + + CTxMemPool pool{MemPoolOptionsForTest(g_setup->m_node)}; + + // Add a bunch of parent-child pairs to the mempool, and remember them. + std::vector<CTransaction> mempool_txs; + size_t iter{0}; + + int32_t replacement_vsize = fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(1, 1000000); + + // Keep track of the total vsize of CTxMemPoolEntry's being added to the mempool to avoid overflow + // Add replacement_vsize since this is added to new diagram during RBF check + int64_t running_vsize_total{replacement_vsize}; + + LOCK2(cs_main, pool.cs); + + LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), NUM_ITERS) + { + // Make sure txns only have one input, and that a unique input is given to avoid circular references + std::optional<CMutableTransaction> parent = ConsumeDeserializable<CMutableTransaction>(fuzzed_data_provider, TX_WITH_WITNESS); + if (!parent) { + return; + } + assert(iter <= g_outpoints.size()); + parent->vin.resize(1); + parent->vin[0].prevout = g_outpoints[iter++]; + + mempool_txs.emplace_back(*parent); + const auto parent_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back()); + running_vsize_total += parent_entry.GetTxSize(); + if (running_vsize_total > std::numeric_limits<int32_t>::max()) { + // We aren't adding this final tx to mempool, so we don't want to conflict with it + mempool_txs.pop_back(); + break; + } + pool.addUnchecked(parent_entry); + if (fuzzed_data_provider.ConsumeBool() && !child->vin.empty()) { + child->vin[0].prevout = COutPoint{mempool_txs.back().GetHash(), 0}; + } + mempool_txs.emplace_back(*child); + const auto child_entry = ConsumeTxMemPoolEntry(fuzzed_data_provider, mempool_txs.back()); + running_vsize_total += child_entry.GetTxSize(); + if (running_vsize_total > std::numeric_limits<int32_t>::max()) { + // We aren't adding this final tx to mempool, so we don't want to conflict with it + mempool_txs.pop_back(); + break; + } + pool.addUnchecked(child_entry); + + if (fuzzed_data_provider.ConsumeBool()) { + pool.PrioritiseTransaction(mempool_txs.back().GetHash().ToUint256(), fuzzed_data_provider.ConsumeIntegralInRange<int32_t>(-100000, 100000)); + } + } + + // Pick some transactions at random to be the direct conflicts + CTxMemPool::setEntries direct_conflicts; + for (auto& tx : mempool_txs) { + if (fuzzed_data_provider.ConsumeBool()) { + direct_conflicts.insert(*pool.GetIter(tx.GetHash())); + } + } + + // Calculate all conflicts: + CTxMemPool::setEntries all_conflicts; + for (auto& txiter : direct_conflicts) { + pool.CalculateDescendants(txiter, all_conflicts); + } + + // Calculate the chunks for a replacement. + CAmount replacement_fees = ConsumeMoney(fuzzed_data_provider); + auto calc_results{pool.CalculateChunksForRBF(replacement_fees, replacement_vsize, direct_conflicts, all_conflicts)}; + + if (calc_results.has_value()) { + // Sanity checks on the chunks. + + // Feerates are monotonically decreasing. + FeeFrac first_sum; + for (size_t i = 0; i < calc_results->first.size(); ++i) { + first_sum += calc_results->first[i]; + if (i) assert(!(calc_results->first[i - 1] << calc_results->first[i])); + } + FeeFrac second_sum; + for (size_t i = 0; i < calc_results->second.size(); ++i) { + second_sum += calc_results->second[i]; + if (i) assert(!(calc_results->second[i - 1] << calc_results->second[i])); + } + + FeeFrac replaced; + for (auto txiter : all_conflicts) { + replaced.fee += txiter->GetModifiedFee(); + replaced.size += txiter->GetTxSize(); + } + // The total fee & size of the new diagram minus replaced fee & size should be the total + // fee & size of the old diagram minus replacement fee & size. + assert((first_sum - replaced) == (second_sum - FeeFrac{replacement_fees, replacement_vsize})); + } + + // If internals report error, wrapper should too + auto err_tuple{ImprovesFeerateDiagram(pool, direct_conflicts, all_conflicts, replacement_fees, replacement_vsize)}; + if (!calc_results.has_value()) { + assert(err_tuple.value().first == DiagramCheckError::UNCALCULABLE); + } else { + // Diagram check succeeded + auto old_sum = std::accumulate(calc_results->first.begin(), calc_results->first.end(), FeeFrac{}); + auto new_sum = std::accumulate(calc_results->second.begin(), calc_results->second.end(), FeeFrac{}); + if (!err_tuple.has_value()) { + // New diagram's final fee should always match or exceed old diagram's + assert(old_sum.fee <= new_sum.fee); + } else if (old_sum.fee > new_sum.fee) { + // Or it failed, and if old diagram had higher fees, it should be a failure + assert(err_tuple.value().first == DiagramCheckError::FAILURE); + } + } +} diff --git a/src/test/fuzz/script_bitcoin_consensus.cpp b/src/test/fuzz/script_bitcoin_consensus.cpp deleted file mode 100644 index 846389863d..0000000000 --- a/src/test/fuzz/script_bitcoin_consensus.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2020 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 <script/bitcoinconsensus.h> -#include <script/interpreter.h> -#include <test/fuzz/FuzzedDataProvider.h> -#include <test/fuzz/fuzz.h> -#include <test/fuzz/util.h> - -#include <cstdint> -#include <string> -#include <vector> - -FUZZ_TARGET(script_bitcoin_consensus) -{ - FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); - const std::vector<uint8_t> random_bytes_1 = ConsumeRandomLengthByteVector(fuzzed_data_provider); - const std::vector<uint8_t> random_bytes_2 = ConsumeRandomLengthByteVector(fuzzed_data_provider); - const CAmount money = ConsumeMoney(fuzzed_data_provider); - bitcoinconsensus_error err; - bitcoinconsensus_error* err_p = fuzzed_data_provider.ConsumeBool() ? &err : nullptr; - const unsigned int n_in = fuzzed_data_provider.ConsumeIntegral<unsigned int>(); - const unsigned int flags = fuzzed_data_provider.ConsumeIntegral<unsigned int>(); - assert(bitcoinconsensus_version() == BITCOINCONSENSUS_API_VER); - if ((flags & SCRIPT_VERIFY_WITNESS) != 0 && (flags & SCRIPT_VERIFY_P2SH) == 0) { - return; - } - (void)bitcoinconsensus_verify_script(random_bytes_1.data(), random_bytes_1.size(), random_bytes_2.data(), random_bytes_2.size(), n_in, flags, err_p); - (void)bitcoinconsensus_verify_script_with_amount(random_bytes_1.data(), random_bytes_1.size(), money, random_bytes_2.data(), random_bytes_2.size(), n_in, flags, err_p); - - std::vector<UTXO> spent_outputs; - std::vector<std::vector<unsigned char>> spent_spks; - if (n_in <= 24386) { - spent_outputs.reserve(n_in); - spent_spks.reserve(n_in); - for (size_t i = 0; i < n_in; ++i) { - spent_spks.push_back(ConsumeRandomLengthByteVector(fuzzed_data_provider)); - const CAmount value{ConsumeMoney(fuzzed_data_provider)}; - const auto spk_size{static_cast<unsigned>(spent_spks.back().size())}; - spent_outputs.push_back({.scriptPubKey = spent_spks.back().data(), .scriptPubKeySize = spk_size, .value = value}); - } - } - - const auto spent_outs_size{static_cast<unsigned>(spent_outputs.size())}; - - (void)bitcoinconsensus_verify_script_with_spent_outputs( - random_bytes_1.data(), random_bytes_1.size(), money, random_bytes_2.data(), random_bytes_2.size(), - spent_outputs.data(), spent_outs_size, n_in, flags, err_p); -} diff --git a/src/test/fuzz/script_sign.cpp b/src/test/fuzz/script_sign.cpp index 9ae150e553..4695bc611b 100644 --- a/src/test/fuzz/script_sign.cpp +++ b/src/test/fuzz/script_sign.cpp @@ -26,7 +26,7 @@ void initialize_script_sign() { - ECC_Start(); + static ECC_Context ecc_context{}; SelectParams(ChainType::REGTEST); } diff --git a/src/test/fuzz/secp256k1_ecdsa_signature_parse_der_lax.cpp b/src/test/fuzz/secp256k1_ecdsa_signature_parse_der_lax.cpp index 74ef6bfd4e..ae0c8479cb 100644 --- a/src/test/fuzz/secp256k1_ecdsa_signature_parse_der_lax.cpp +++ b/src/test/fuzz/secp256k1_ecdsa_signature_parse_der_lax.cpp @@ -24,8 +24,7 @@ FUZZ_TARGET(secp256k1_ecdsa_signature_parse_der_lax) secp256k1_ecdsa_signature sig_der_lax; const bool parsed_der_lax = ecdsa_signature_parse_der_lax(&sig_der_lax, signature_bytes.data(), signature_bytes.size()) == 1; if (parsed_der_lax) { - ECC_Start(); + ECC_Context ecc_context{}; (void)SigHasLowR(&sig_der_lax); - ECC_Stop(); } } diff --git a/src/test/fuzz/string.cpp b/src/test/fuzz/string.cpp index e81efac6e0..631da13803 100644 --- a/src/test/fuzz/string.cpp +++ b/src/test/fuzz/string.cpp @@ -90,7 +90,7 @@ FUZZ_TARGET(string) (void)ToUpper(random_string_1); (void)TrimString(random_string_1); (void)TrimString(random_string_1, random_string_2); - (void)urlDecode(random_string_1); + (void)UrlDecode(random_string_1); (void)ContainsNoNUL(random_string_1); (void)_(random_string_1.c_str()); try { diff --git a/src/test/fuzz/timedata.cpp b/src/test/fuzz/timedata.cpp deleted file mode 100644 index f5d005296b..0000000000 --- a/src/test/fuzz/timedata.cpp +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2020-2021 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/FuzzedDataProvider.h> -#include <test/fuzz/fuzz.h> -#include <test/fuzz/util.h> -#include <timedata.h> - -#include <cstdint> -#include <string> -#include <vector> - -FUZZ_TARGET(timedata) -{ - FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); - const unsigned int max_size = fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(0, 1000); - // A max_size of 0 implies no limit, so cap the max number of insertions to avoid timeouts - auto max_to_insert = fuzzed_data_provider.ConsumeIntegralInRange<int>(0, 4000); - // Divide by 2 to avoid signed integer overflow in .median() - const int64_t initial_value = fuzzed_data_provider.ConsumeIntegral<int64_t>() / 2; - CMedianFilter<int64_t> median_filter{max_size, initial_value}; - while (fuzzed_data_provider.remaining_bytes() > 0 && --max_to_insert >= 0) { - (void)median_filter.median(); - assert(median_filter.size() > 0); - assert(static_cast<size_t>(median_filter.size()) == median_filter.sorted().size()); - assert(static_cast<unsigned int>(median_filter.size()) <= max_size || max_size == 0); - // Divide by 2 to avoid signed integer overflow in .median() - median_filter.input(fuzzed_data_provider.ConsumeIntegral<int64_t>() / 2); - } -} diff --git a/src/test/fuzz/timeoffsets.cpp b/src/test/fuzz/timeoffsets.cpp new file mode 100644 index 0000000000..019337a94a --- /dev/null +++ b/src/test/fuzz/timeoffsets.cpp @@ -0,0 +1,28 @@ +// Copyright (c) 2024-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 <node/timeoffsets.h> +#include <test/fuzz/FuzzedDataProvider.h> +#include <test/fuzz/fuzz.h> +#include <test/util/setup_common.h> + +#include <chrono> +#include <cstdint> +#include <functional> + +void initialize_timeoffsets() +{ + static const auto testing_setup = MakeNoLogFileContext<>(ChainType::MAIN); +} + +FUZZ_TARGET(timeoffsets, .init = initialize_timeoffsets) +{ + FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size()); + TimeOffsets offsets{}; + LIMITED_WHILE(fuzzed_data_provider.remaining_bytes() > 0, 4'000) { + (void)offsets.Median(); + offsets.Add(std::chrono::seconds{fuzzed_data_provider.ConsumeIntegral<std::chrono::seconds::rep>()}); + offsets.WarnIfOutOfSync(); + } +} diff --git a/src/test/fuzz/tx_pool.cpp b/src/test/fuzz/tx_pool.cpp index 4ad0956201..9f0aedf29b 100644 --- a/src/test/fuzz/tx_pool.cpp +++ b/src/test/fuzz/tx_pool.cpp @@ -6,6 +6,7 @@ #include <node/context.h> #include <node/mempool_args.h> #include <node/miner.h> +#include <policy/v3_policy.h> #include <test/fuzz/FuzzedDataProvider.h> #include <test/fuzz/fuzz.h> #include <test/fuzz/util.h> @@ -49,7 +50,7 @@ void initialize_tx_pool() g_outpoints_coinbase_init_immature; outpoints.push_back(prevout); } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); } struct TransactionsDelta final : public CValidationInterface { @@ -101,12 +102,10 @@ void Finish(FuzzedDataProvider& fuzzed_data_provider, MockedTxPool& tx_pool, Cha if (!info_all.empty()) { const auto& tx_to_remove = *PickValue(fuzzed_data_provider, info_all).tx; WITH_LOCK(tx_pool.cs, tx_pool.removeRecursive(tx_to_remove, MemPoolRemovalReason::BLOCK /* dummy */)); - std::vector<uint256> all_txids; - tx_pool.queryHashes(all_txids); - assert(all_txids.size() < info_all.size()); + assert(tx_pool.size() < info_all.size()); WITH_LOCK(::cs_main, tx_pool.check(chainstate.CoinsTip(), chainstate.m_chain.Height() + 1)); } - SyncWithValidationInterfaceQueue(); + g_setup->m_node.validation_signals->SyncWithValidationInterfaceQueue(); } void MockTime(FuzzedDataProvider& fuzzed_data_provider, const Chainstate& chainstate) @@ -140,7 +139,6 @@ void CheckATMPInvariants(const MempoolAcceptResult& res, bool txid_in_mempool, b Assert(wtxid_in_mempool); Assert(res.m_state.IsValid()); Assert(!res.m_state.IsInvalid()); - Assert(res.m_replaced_transactions); Assert(res.m_vsize); Assert(res.m_base_fees); Assert(res.m_effective_feerate); @@ -155,7 +153,6 @@ void CheckATMPInvariants(const MempoolAcceptResult& res, bool txid_in_mempool, b Assert(res.m_state.IsInvalid()); const bool is_reconsiderable{res.m_state.GetResult() == TxValidationResult::TX_RECONSIDERABLE}; - Assert(!res.m_replaced_transactions); Assert(!res.m_vsize); Assert(!res.m_base_fees); // Fee information is provided if the failure is TX_RECONSIDERABLE. @@ -229,7 +226,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) // Create transaction to add to the mempool const CTransactionRef tx = [&] { CMutableTransaction tx_mut; - tx_mut.nVersion = CTransaction::CURRENT_VERSION; + tx_mut.nVersion = fuzzed_data_provider.ConsumeBool() ? 3 : CTransaction::CURRENT_VERSION; tx_mut.nLockTime = fuzzed_data_provider.ConsumeBool() ? 0 : fuzzed_data_provider.ConsumeIntegral<uint32_t>(); const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size()); const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<int>(1, outpoints_rbf.size() * 2); @@ -286,13 +283,13 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) std::set<CTransactionRef> removed; std::set<CTransactionRef> added; auto txr = std::make_shared<TransactionsDelta>(removed, added); - RegisterSharedValidationInterface(txr); + node.validation_signals->RegisterSharedValidationInterface(txr); const bool bypass_limits = fuzzed_data_provider.ConsumeBool(); // Make sure ProcessNewPackage on one transaction works. // The result is not guaranteed to be the same as what is returned by ATMP. const auto result_package = WITH_LOCK(::cs_main, - return ProcessNewPackage(chainstate, tx_pool, {tx}, true)); + return ProcessNewPackage(chainstate, tx_pool, {tx}, true, /*client_maxfeerate=*/{})); // If something went wrong due to a package-specific policy, it might not return a // validation result for the transaction. if (result_package.m_state.GetResult() != PackageValidationResult::PCKG_POLICY) { @@ -304,8 +301,8 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) const auto res = WITH_LOCK(::cs_main, return AcceptToMemoryPool(chainstate, tx, GetTime(), bypass_limits, /*test_accept=*/false)); const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID; - SyncWithValidationInterfaceQueue(); - UnregisterSharedValidationInterface(txr); + node.validation_signals->SyncWithValidationInterfaceQueue(); + node.validation_signals->UnregisterSharedValidationInterface(txr); bool txid_in_mempool = tx_pool.exists(GenTxid::Txid(tx->GetHash())); bool wtxid_in_mempool = tx_pool.exists(GenTxid::Wtxid(tx->GetWitnessHash())); @@ -315,6 +312,7 @@ FUZZ_TARGET(tx_pool_standard, .init = initialize_tx_pool) if (accepted) { Assert(added.size() == 1); // For now, no package acceptance Assert(tx == *added.begin()); + CheckMempoolV3Invariants(tx_pool); } else { // Do not consider rejected transaction removed removed.erase(tx); @@ -407,6 +405,7 @@ FUZZ_TARGET(tx_pool, .init = initialize_tx_pool) const bool accepted = res.m_result_type == MempoolAcceptResult::ResultType::VALID; if (accepted) { txids.push_back(tx->GetHash()); + CheckMempoolV3Invariants(tx_pool); } } Finish(fuzzed_data_provider, tx_pool, chainstate); diff --git a/src/test/fuzz/txorphan.cpp b/src/test/fuzz/txorphan.cpp index 5423ba8920..7bebb987b1 100644 --- a/src/test/fuzz/txorphan.cpp +++ b/src/test/fuzz/txorphan.cpp @@ -37,13 +37,14 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage) SetMockTime(ConsumeTime(fuzzed_data_provider)); TxOrphanage orphanage; - std::vector<COutPoint> outpoints; + std::set<COutPoint> outpoints; + // initial outpoints used to construct transactions later for (uint8_t i = 0; i < 4; i++) { - outpoints.emplace_back(Txid::FromUint256(uint256{i}), 0); + outpoints.emplace(Txid::FromUint256(uint256{i}), 0); } - // if true, allow duplicate input when constructing tx - const bool duplicate_input = fuzzed_data_provider.ConsumeBool(); + + CTransactionRef ptx_potential_parent = nullptr; LIMITED_WHILE(outpoints.size() < 200'000 && fuzzed_data_provider.ConsumeBool(), 10 * DEFAULT_MAX_ORPHAN_TRANSACTIONS) { @@ -51,33 +52,47 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage) const CTransactionRef tx = [&] { CMutableTransaction tx_mut; const auto num_in = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, outpoints.size()); - const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, outpoints.size()); - // pick unique outpoints from outpoints as input + const auto num_out = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(1, 256); + // pick outpoints from outpoints as input. We allow input duplicates on purpose, given we are not + // running any transaction validation logic before adding transactions to the orphanage for (uint32_t i = 0; i < num_in; i++) { auto& prevout = PickValue(fuzzed_data_provider, outpoints); - tx_mut.vin.emplace_back(prevout); - // pop the picked outpoint if duplicate input is not allowed - if (!duplicate_input) { - std::swap(prevout, outpoints.back()); - outpoints.pop_back(); - } + // try making transactions unique by setting a random nSequence, but allow duplicate transactions if they happen + tx_mut.vin.emplace_back(prevout, CScript{}, fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, CTxIn::SEQUENCE_FINAL)); } // output amount will not affect txorphanage for (uint32_t i = 0; i < num_out; i++) { tx_mut.vout.emplace_back(CAmount{0}, CScript{}); } - // restore previously popped outpoints - for (auto& in : tx_mut.vin) { - outpoints.push_back(in.prevout); - } auto new_tx = MakeTransactionRef(tx_mut); - // add newly constructed transaction to outpoints + // add newly constructed outpoints to the coin pool for (uint32_t i = 0; i < num_out; i++) { - outpoints.emplace_back(new_tx->GetHash(), i); + outpoints.emplace(new_tx->GetHash(), i); } return new_tx; }(); + // Trigger orphanage functions that are called using parents. ptx_potential_parent is a tx we constructed in a + // previous loop and potentially the parent of this tx. + if (ptx_potential_parent) { + // Set up future GetTxToReconsider call. + orphanage.AddChildrenToWorkSet(*ptx_potential_parent); + + // Check that all txns returned from GetChildrenFrom* are indeed a direct child of this tx. + NodeId peer_id = fuzzed_data_provider.ConsumeIntegral<NodeId>(); + for (const auto& child : orphanage.GetChildrenFromSamePeer(ptx_potential_parent, peer_id)) { + assert(std::any_of(child->vin.cbegin(), child->vin.cend(), [&](const auto& input) { + return input.prevout.hash == ptx_potential_parent->GetHash(); + })); + } + for (const auto& [child, peer] : orphanage.GetChildrenFromDifferentPeer(ptx_potential_parent, peer_id)) { + assert(std::any_of(child->vin.cbegin(), child->vin.cend(), [&](const auto& input) { + return input.prevout.hash == ptx_potential_parent->GetHash(); + })); + assert(peer != peer_id); + } + } + // trigger orphanage functions LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10 * DEFAULT_MAX_ORPHAN_TRANSACTIONS) { @@ -86,19 +101,15 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage) CallOneOf( fuzzed_data_provider, [&] { - orphanage.AddChildrenToWorkSet(*tx); - }, - [&] { { CTransactionRef ref = orphanage.GetTxToReconsider(peer_id); if (ref) { - bool have_tx = orphanage.HaveTx(GenTxid::Txid(ref->GetHash())) || orphanage.HaveTx(GenTxid::Wtxid(ref->GetWitnessHash())); - Assert(have_tx); + Assert(orphanage.HaveTx(ref->GetWitnessHash())); } } }, [&] { - bool have_tx = orphanage.HaveTx(GenTxid::Txid(tx->GetHash())) || orphanage.HaveTx(GenTxid::Wtxid(tx->GetWitnessHash())); + bool have_tx = orphanage.HaveTx(tx->GetWitnessHash()); // AddTx should return false if tx is too big or already have it // tx weight is unknown, we only check when tx is already in orphanage { @@ -106,7 +117,7 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage) // have_tx == true -> add_tx == false Assert(!have_tx || !add_tx); } - have_tx = orphanage.HaveTx(GenTxid::Txid(tx->GetHash())) || orphanage.HaveTx(GenTxid::Wtxid(tx->GetWitnessHash())); + have_tx = orphanage.HaveTx(tx->GetWitnessHash()); { bool add_tx = orphanage.AddTx(tx, peer_id); // if have_tx is still false, it must be too big @@ -115,15 +126,15 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage) } }, [&] { - bool have_tx = orphanage.HaveTx(GenTxid::Txid(tx->GetHash())) || orphanage.HaveTx(GenTxid::Wtxid(tx->GetWitnessHash())); + bool have_tx = orphanage.HaveTx(tx->GetWitnessHash()); // EraseTx should return 0 if m_orphans doesn't have the tx { - Assert(have_tx == orphanage.EraseTx(tx->GetHash())); + Assert(have_tx == orphanage.EraseTx(tx->GetWitnessHash())); } - have_tx = orphanage.HaveTx(GenTxid::Txid(tx->GetHash())) || orphanage.HaveTx(GenTxid::Wtxid(tx->GetWitnessHash())); + have_tx = orphanage.HaveTx(tx->GetWitnessHash()); // have_tx should be false and EraseTx should fail { - Assert(!have_tx && !orphanage.EraseTx(tx->GetHash())); + Assert(!have_tx && !orphanage.EraseTx(tx->GetWitnessHash())); } }, [&] { @@ -136,6 +147,12 @@ FUZZ_TARGET(txorphan, .init = initialize_orphanage) orphanage.LimitOrphans(limit, limit_orphans_rng); Assert(orphanage.Size() <= limit); }); + + } + // Set tx as potential parent to be used for future GetChildren() calls. + if (!ptx_potential_parent || fuzzed_data_provider.ConsumeBool()) { + ptx_potential_parent = tx; } + } } diff --git a/src/test/fuzz/util.cpp b/src/test/fuzz/util.cpp index 23b0761355..259b00fcae 100644 --- a/src/test/fuzz/util.cpp +++ b/src/test/fuzz/util.cpp @@ -272,7 +272,7 @@ FILE* FuzzedFileProvider::open() [&] { mode = "a+"; }); -#if defined _GNU_SOURCE && !defined __ANDROID__ +#if defined _GNU_SOURCE && (defined(__linux__) || defined(__FreeBSD__)) const cookie_io_functions_t io_hooks = { FuzzedFileProvider::read, FuzzedFileProvider::write, diff --git a/src/test/fuzz/util/descriptor.cpp b/src/test/fuzz/util/descriptor.cpp index df78bdf314..0fed2bc5e1 100644 --- a/src/test/fuzz/util/descriptor.cpp +++ b/src/test/fuzz/util/descriptor.cpp @@ -15,7 +15,7 @@ void MockedDescriptorConverter::Init() { // an extended one. if (IdIsCompPubKey(i) || IdIsUnCompPubKey(i) || IdIsXOnlyPubKey(i) || IdIsConstPrivKey(i)) { CKey privkey; - privkey.Set(UCharCast(key_data.begin()), UCharCast(key_data.end()), !IdIsUnCompPubKey(i)); + privkey.Set(key_data.begin(), key_data.end(), !IdIsUnCompPubKey(i)); if (IdIsCompPubKey(i) || IdIsUnCompPubKey(i)) { CPubKey pubkey{privkey.GetPubKey()}; keys_str[i] = HexStr(pubkey); diff --git a/src/test/fuzz/util/net.cpp b/src/test/fuzz/util/net.cpp index eb0f14ede0..99151bb84d 100644 --- a/src/test/fuzz/util/net.cpp +++ b/src/test/fuzz/util/net.cpp @@ -25,33 +25,63 @@ class CNode; -CNetAddr ConsumeNetAddr(FuzzedDataProvider& fuzzed_data_provider) noexcept +CNetAddr ConsumeNetAddr(FuzzedDataProvider& fuzzed_data_provider, FastRandomContext* rand) noexcept { - const Network network = fuzzed_data_provider.PickValueInArray({Network::NET_IPV4, Network::NET_IPV6, Network::NET_INTERNAL, Network::NET_ONION}); - CNetAddr net_addr; - if (network == Network::NET_IPV4) { - in_addr v4_addr = {}; - v4_addr.s_addr = fuzzed_data_provider.ConsumeIntegral<uint32_t>(); - net_addr = CNetAddr{v4_addr}; - } else if (network == Network::NET_IPV6) { - if (fuzzed_data_provider.remaining_bytes() >= 16) { - in6_addr v6_addr = {}; - auto addr_bytes = fuzzed_data_provider.ConsumeBytes<uint8_t>(16); - if (addr_bytes[0] == CJDNS_PREFIX) { // Avoid generating IPv6 addresses that look like CJDNS. - addr_bytes[0] = 0x55; // Just an arbitrary number, anything != CJDNS_PREFIX would do. - } - memcpy(v6_addr.s6_addr, addr_bytes.data(), 16); - net_addr = CNetAddr{v6_addr, fuzzed_data_provider.ConsumeIntegral<uint32_t>()}; + struct NetAux { + Network net; + CNetAddr::BIP155Network bip155; + size_t len; + }; + + static constexpr std::array<NetAux, 6> nets{ + NetAux{.net = Network::NET_IPV4, .bip155 = CNetAddr::BIP155Network::IPV4, .len = ADDR_IPV4_SIZE}, + NetAux{.net = Network::NET_IPV6, .bip155 = CNetAddr::BIP155Network::IPV6, .len = ADDR_IPV6_SIZE}, + NetAux{.net = Network::NET_ONION, .bip155 = CNetAddr::BIP155Network::TORV3, .len = ADDR_TORV3_SIZE}, + NetAux{.net = Network::NET_I2P, .bip155 = CNetAddr::BIP155Network::I2P, .len = ADDR_I2P_SIZE}, + NetAux{.net = Network::NET_CJDNS, .bip155 = CNetAddr::BIP155Network::CJDNS, .len = ADDR_CJDNS_SIZE}, + NetAux{.net = Network::NET_INTERNAL, .bip155 = CNetAddr::BIP155Network{0}, .len = 0}, + }; + + const size_t nets_index{rand == nullptr + ? fuzzed_data_provider.ConsumeIntegralInRange<size_t>(0, nets.size() - 1) + : static_cast<size_t>(rand->randrange(nets.size()))}; + + const auto& aux = nets[nets_index]; + + CNetAddr addr; + + if (aux.net == Network::NET_INTERNAL) { + if (rand == nullptr) { + addr.SetInternal(fuzzed_data_provider.ConsumeBytesAsString(32)); + } else { + const auto v = rand->randbytes(32); + addr.SetInternal(std::string{v.begin(), v.end()}); } - } else if (network == Network::NET_INTERNAL) { - net_addr.SetInternal(fuzzed_data_provider.ConsumeBytesAsString(32)); - } else if (network == Network::NET_ONION) { - auto pub_key{fuzzed_data_provider.ConsumeBytes<uint8_t>(ADDR_TORV3_SIZE)}; - pub_key.resize(ADDR_TORV3_SIZE); - const bool ok{net_addr.SetSpecial(OnionToString(pub_key))}; - assert(ok); + return addr; + } + + DataStream s; + + s << static_cast<uint8_t>(aux.bip155); + + std::vector<uint8_t> addr_bytes; + if (rand == nullptr) { + addr_bytes = fuzzed_data_provider.ConsumeBytes<uint8_t>(aux.len); + addr_bytes.resize(aux.len); + } else { + addr_bytes = rand->randbytes(aux.len); } - return net_addr; + if (aux.net == NET_IPV6 && addr_bytes[0] == CJDNS_PREFIX) { // Avoid generating IPv6 addresses that look like CJDNS. + addr_bytes[0] = 0x55; // Just an arbitrary number, anything != CJDNS_PREFIX would do. + } + if (aux.net == NET_CJDNS) { // Avoid generating CJDNS addresses that don't start with CJDNS_PREFIX because those are !IsValid(). + addr_bytes[0] = CJDNS_PREFIX; + } + s << addr_bytes; + + s >> CAddress::V2_NETWORK(addr); + + return addr; } CAddress ConsumeAddress(FuzzedDataProvider& fuzzed_data_provider) noexcept diff --git a/src/test/fuzz/util/net.h b/src/test/fuzz/util/net.h index 47e4a2fac0..a6c9e23f2e 100644 --- a/src/test/fuzz/util/net.h +++ b/src/test/fuzz/util/net.h @@ -24,7 +24,15 @@ #include <optional> #include <string> -CNetAddr ConsumeNetAddr(FuzzedDataProvider& fuzzed_data_provider) noexcept; +/** + * Create a CNetAddr. It may have `addr.IsValid() == false`. + * @param[in,out] fuzzed_data_provider Take data for the address from this, if `rand` is `nullptr`. + * @param[in,out] rand If not nullptr, take data from it instead of from `fuzzed_data_provider`. + * Prefer generating addresses using `fuzzed_data_provider` because it is not uniform. Only use + * `rand` if `fuzzed_data_provider` is exhausted or its data is needed for other things. + * @return a "random" network address. + */ +CNetAddr ConsumeNetAddr(FuzzedDataProvider& fuzzed_data_provider, FastRandomContext* rand = nullptr) noexcept; class FuzzedSock : public Sock { diff --git a/src/test/i2p_tests.cpp b/src/test/i2p_tests.cpp index f80f07d190..d7249d88f4 100644 --- a/src/test/i2p_tests.cpp +++ b/src/test/i2p_tests.cpp @@ -6,6 +6,7 @@ #include <i2p.h> #include <logging.h> #include <netaddress.h> +#include <netbase.h> #include <test/util/logging.h> #include <test/util/net.h> #include <test/util/setup_common.h> @@ -38,7 +39,7 @@ public: private: const BCLog::Level m_prev_log_level; - const std::function<std::unique_ptr<Sock>(const CService&)> m_create_sock_orig; + const std::function<std::unique_ptr<Sock>(const sa_family_t&)> m_create_sock_orig; }; BOOST_FIXTURE_TEST_SUITE(i2p_tests, EnvTestingSetup) @@ -46,12 +47,14 @@ BOOST_FIXTURE_TEST_SUITE(i2p_tests, EnvTestingSetup) BOOST_AUTO_TEST_CASE(unlimited_recv) { // Mock CreateSock() to create MockSock. - CreateSock = [](const CService&) { + CreateSock = [](const sa_family_t&) { return std::make_unique<StaticContentsSock>(std::string(i2p::sam::MAX_MSG_SIZE + 1, 'a')); }; CThreadInterrupt interrupt; - i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", CService{}, &interrupt); + const std::optional<CService> addr{Lookup("127.0.0.1", 9000, false)}; + const Proxy sam_proxy(addr.value(), false); + i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", sam_proxy, &interrupt); { ASSERT_DEBUG_LOG("Creating persistent SAM session"); @@ -66,7 +69,7 @@ BOOST_AUTO_TEST_CASE(unlimited_recv) BOOST_AUTO_TEST_CASE(listen_ok_accept_fail) { size_t num_sockets{0}; - CreateSock = [&num_sockets](const CService&) { + CreateSock = [&num_sockets](const sa_family_t&) { // clang-format off ++num_sockets; // First socket is the control socket for creating the session. @@ -111,8 +114,10 @@ BOOST_AUTO_TEST_CASE(listen_ok_accept_fail) }; CThreadInterrupt interrupt; + const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}; + const Proxy sam_proxy(addr, false); i2p::sam::Session session(gArgs.GetDataDirNet() / "test_i2p_private_key", - CService{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}, + sam_proxy, &interrupt); i2p::Connection conn; @@ -130,7 +135,7 @@ BOOST_AUTO_TEST_CASE(damaged_private_key) { const auto CreateSockOrig = CreateSock; - CreateSock = [](const CService&) { + CreateSock = [](const sa_family_t&) { return std::make_unique<StaticContentsSock>("HELLO REPLY RESULT=OK VERSION=3.1\n" "SESSION STATUS RESULT=OK DESTINATION=\n"); }; @@ -154,7 +159,9 @@ BOOST_AUTO_TEST_CASE(damaged_private_key) BOOST_REQUIRE(WriteBinaryFile(i2p_private_key_file, file_contents)); CThreadInterrupt interrupt; - i2p::sam::Session session(i2p_private_key_file, CService{}, &interrupt); + const CService addr{in6_addr(IN6ADDR_LOOPBACK_INIT), /*port=*/7656}; + const Proxy sam_proxy{addr, false}; + i2p::sam::Session session(i2p_private_key_file, sam_proxy, &interrupt); { ASSERT_DEBUG_LOG("Creating persistent SAM session"); diff --git a/src/test/ipc_test.capnp b/src/test/ipc_test.capnp new file mode 100644 index 0000000000..55a3dc2683 --- /dev/null +++ b/src/test/ipc_test.capnp @@ -0,0 +1,18 @@ +# Copyright (c) 2023 The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +@0xd71b0fc8727fdf83; + +using Cxx = import "/capnp/c++.capnp"; +$Cxx.namespace("gen"); + +using Proxy = import "/mp/proxy.capnp"; +$Proxy.include("test/ipc_test.h"); +$Proxy.includeTypes("ipc/capnp/common-types.h"); + +interface FooInterface $Proxy.wrap("FooImplementation") { + add @0 (a :Int32, b :Int32) -> (result :Int32); + passOutPoint @1 (arg :Data) -> (result :Data); + passUniValue @2 (arg :Text) -> (result :Text); +} diff --git a/src/test/ipc_test.cpp b/src/test/ipc_test.cpp new file mode 100644 index 0000000000..ce4edaceb0 --- /dev/null +++ b/src/test/ipc_test.cpp @@ -0,0 +1,67 @@ +// Copyright (c) 2023 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 <logging.h> +#include <mp/proxy-types.h> +#include <test/ipc_test.capnp.h> +#include <test/ipc_test.capnp.proxy.h> +#include <test/ipc_test.h> + +#include <future> +#include <kj/common.h> +#include <kj/memory.h> +#include <kj/test.h> + +#include <boost/test/unit_test.hpp> + +//! Unit test that tests execution of IPC calls without actually creating a +//! separate process. This test is primarily intended to verify behavior of type +//! conversion code that converts C++ objects to Cap'n Proto messages and vice +//! versa. +//! +//! The test creates a thread which creates a FooImplementation object (defined +//! in ipc_test.h) and a two-way pipe accepting IPC requests which call methods +//! on the object through FooInterface (defined in ipc_test.capnp). +void IpcTest() +{ + // Setup: create FooImplemention object and listen for FooInterface requests + std::promise<std::unique_ptr<mp::ProxyClient<gen::FooInterface>>> foo_promise; + std::function<void()> disconnect_client; + std::thread thread([&]() { + mp::EventLoop loop("IpcTest", [](bool raise, const std::string& log) { LogPrintf("LOG%i: %s\n", raise, log); }); + auto pipe = loop.m_io_context.provider->newTwoWayPipe(); + + auto connection_client = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[0])); + auto foo_client = std::make_unique<mp::ProxyClient<gen::FooInterface>>( + connection_client->m_rpc_system.bootstrap(mp::ServerVatId().vat_id).castAs<gen::FooInterface>(), + connection_client.get(), /* destroy_connection= */ false); + foo_promise.set_value(std::move(foo_client)); + disconnect_client = [&] { loop.sync([&] { connection_client.reset(); }); }; + + auto connection_server = std::make_unique<mp::Connection>(loop, kj::mv(pipe.ends[1]), [&](mp::Connection& connection) { + auto foo_server = kj::heap<mp::ProxyServer<gen::FooInterface>>(std::make_shared<FooImplementation>(), connection); + return capnp::Capability::Client(kj::mv(foo_server)); + }); + connection_server->onDisconnect([&] { connection_server.reset(); }); + loop.loop(); + }); + std::unique_ptr<mp::ProxyClient<gen::FooInterface>> foo{foo_promise.get_future().get()}; + + // Test: make sure arguments were sent and return value is received + BOOST_CHECK_EQUAL(foo->add(1, 2), 3); + + COutPoint txout1{Txid::FromUint256(uint256{100}), 200}; + COutPoint txout2{foo->passOutPoint(txout1)}; + BOOST_CHECK(txout1 == txout2); + + UniValue uni1{UniValue::VOBJ}; + uni1.pushKV("i", 1); + uni1.pushKV("s", "two"); + UniValue uni2{foo->passUniValue(uni1)}; + BOOST_CHECK_EQUAL(uni1.write(), uni2.write()); + + // Test cleanup: disconnect pipe and join thread + disconnect_client(); + thread.join(); +} diff --git a/src/test/ipc_test.h b/src/test/ipc_test.h new file mode 100644 index 0000000000..bcfcc2125c --- /dev/null +++ b/src/test/ipc_test.h @@ -0,0 +1,21 @@ +// Copyright (c) 2023 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_TEST_IPC_TEST_H +#define BITCOIN_TEST_IPC_TEST_H + +#include <primitives/transaction.h> +#include <univalue.h> + +class FooImplementation +{ +public: + int add(int a, int b) { return a + b; } + COutPoint passOutPoint(COutPoint o) { return o; } + UniValue passUniValue(UniValue v) { return v; } +}; + +void IpcTest(); + +#endif // BITCOIN_TEST_IPC_TEST_H diff --git a/src/test/ipc_tests.cpp b/src/test/ipc_tests.cpp new file mode 100644 index 0000000000..6e144b0f41 --- /dev/null +++ b/src/test/ipc_tests.cpp @@ -0,0 +1,13 @@ +// Copyright (c) 2023 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/ipc_test.h> +#include <boost/test/unit_test.hpp> + +BOOST_AUTO_TEST_SUITE(ipc_tests) +BOOST_AUTO_TEST_CASE(ipc_tests) +{ + IpcTest(); +} +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/main.cpp b/src/test/main.cpp index 0809f83c93..67740ece93 100644 --- a/src/test/main.cpp +++ b/src/test/main.cpp @@ -39,3 +39,10 @@ const std::function<std::vector<const char*>()> G_TEST_COMMAND_LINE_ARGUMENTS = } return args; }; + +/** + * Retrieve the boost unit test name. + */ +const std::function<std::string()> G_TEST_GET_FULL_NAME = []() { + return boost::unit_test::framework::current_test_case().full_name(); +}; diff --git a/src/test/mempool_tests.cpp b/src/test/mempool_tests.cpp index 217e4a6d22..9f8d434213 100644 --- a/src/test/mempool_tests.cpp +++ b/src/test/mempool_tests.cpp @@ -202,9 +202,11 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest) tx7.vout[1].scriptPubKey = CScript() << OP_11 << OP_EQUAL; tx7.vout[1].nValue = 1 * COIN; - auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(2000000LL).FromTx(tx7), CTxMemPool::Limits::NoLimits())}; - BOOST_REQUIRE(ancestors_calculated.has_value()); - BOOST_CHECK(*ancestors_calculated == setAncestors); + { + auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(2000000LL).FromTx(tx7), CTxMemPool::Limits::NoLimits())}; + BOOST_REQUIRE(ancestors_calculated.has_value()); + BOOST_CHECK(*ancestors_calculated == setAncestors); + } pool.addUnchecked(entry.FromTx(tx7), setAncestors); BOOST_CHECK_EQUAL(pool.size(), 7U); @@ -260,9 +262,11 @@ BOOST_AUTO_TEST_CASE(MempoolIndexingTest) tx10.vout[0].scriptPubKey = CScript() << OP_11 << OP_EQUAL; tx10.vout[0].nValue = 10 * COIN; - ancestors_calculated = pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits()); - BOOST_REQUIRE(ancestors_calculated); - BOOST_CHECK(*ancestors_calculated == setAncestors); + { + auto ancestors_calculated{pool.CalculateMemPoolAncestors(entry.Fee(200000LL).Time(NodeSeconds{4s}).FromTx(tx10), CTxMemPool::Limits::NoLimits())}; + BOOST_REQUIRE(ancestors_calculated); + BOOST_CHECK(*ancestors_calculated == setAncestors); + } pool.addUnchecked(entry.FromTx(tx10), setAncestors); diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 342d2514ed..d50af4c175 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -12,7 +12,6 @@ #include <policy/policy.h> #include <test/util/random.h> #include <test/util/txmempool.h> -#include <timedata.h> #include <txmempool.h> #include <uint256.h> #include <util/strencodings.h> diff --git a/src/test/miniscript_tests.cpp b/src/test/miniscript_tests.cpp index 1f28e61bc9..a3699f84b6 100644 --- a/src/test/miniscript_tests.cpp +++ b/src/test/miniscript_tests.cpp @@ -297,6 +297,7 @@ using miniscript::operator"" _mst; using Node = miniscript::Node<CPubKey>; /** Compute all challenges (pubkeys, hashes, timelocks) that occur in a given Miniscript. */ +// NOLINTNEXTLINE(misc-no-recursion) std::set<Challenge> FindChallenges(const NodeRef& ref) { std::set<Challenge> chal; for (const auto& key : ref->keys) { diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 70a8279f04..b9dff96610 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -19,7 +19,6 @@ #include <test/util/random.h> #include <test/util/setup_common.h> #include <test/util/validation.h> -#include <timedata.h> #include <util/strencodings.h> #include <util/string.h> #include <validation.h> @@ -902,10 +901,6 @@ BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message) chainman.ResetIbd(); m_node.args->ForceSetArg("-capturemessages", "0"); m_node.args->ForceSetArg("-bind", ""); - // PeerManager::ProcessMessage() calls AddTimeData() which changes the internal state - // in timedata.cpp and later confuses the test "timedata_tests/addtimedata". Thus reset - // that state as it was before our test was run. - TestOnlyResetTimeData(); } diff --git a/src/test/netbase_tests.cpp b/src/test/netbase_tests.cpp index fa70f62eb4..3422cb8023 100644 --- a/src/test/netbase_tests.cpp +++ b/src/test/netbase_tests.cpp @@ -366,6 +366,7 @@ BOOST_AUTO_TEST_CASE(netpermissions_test) bilingual_str error; NetWhitebindPermissions whitebindPermissions; NetWhitelistPermissions whitelistPermissions; + ConnectionDirection connection_direction; // Detect invalid white bind BOOST_CHECK(!NetWhitebindPermissions::TryParse("", whitebindPermissions, error)); @@ -435,24 +436,33 @@ BOOST_AUTO_TEST_CASE(netpermissions_test) BOOST_CHECK(NetWhitebindPermissions::TryParse(",,@1.2.3.4:32", whitebindPermissions, error)); BOOST_CHECK_EQUAL(whitebindPermissions.m_flags, NetPermissionFlags::None); + BOOST_CHECK(!NetWhitebindPermissions::TryParse("out,forcerelay@1.2.3.4:32", whitebindPermissions, error)); + BOOST_CHECK(error.original.find("whitebind may only be used for incoming connections (\"out\" was passed)") != std::string::npos); + // Detect invalid flag BOOST_CHECK(!NetWhitebindPermissions::TryParse("bloom,forcerelay,oopsie@1.2.3.4:32", whitebindPermissions, error)); BOOST_CHECK(error.original.find("Invalid P2P permission") != std::string::npos); // Check netmask error - BOOST_CHECK(!NetWhitelistPermissions::TryParse("bloom,forcerelay,noban@1.2.3.4:32", whitelistPermissions, error)); + BOOST_CHECK(!NetWhitelistPermissions::TryParse("bloom,forcerelay,noban@1.2.3.4:32", whitelistPermissions, connection_direction, error)); BOOST_CHECK(error.original.find("Invalid netmask specified in -whitelist") != std::string::npos); // Happy path for whitelist parsing - BOOST_CHECK(NetWhitelistPermissions::TryParse("noban@1.2.3.4", whitelistPermissions, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("noban@1.2.3.4", whitelistPermissions, connection_direction, error)); BOOST_CHECK_EQUAL(whitelistPermissions.m_flags, NetPermissionFlags::NoBan); BOOST_CHECK(NetPermissions::HasFlag(whitelistPermissions.m_flags, NetPermissionFlags::NoBan)); - BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay@1.2.3.4/32", whitelistPermissions, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay@1.2.3.4/32", whitelistPermissions, connection_direction, error)); BOOST_CHECK_EQUAL(whitelistPermissions.m_flags, NetPermissionFlags::BloomFilter | NetPermissionFlags::ForceRelay | NetPermissionFlags::NoBan | NetPermissionFlags::Relay); BOOST_CHECK(error.empty()); BOOST_CHECK_EQUAL(whitelistPermissions.m_subnet.ToString(), "1.2.3.4/32"); - BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay,mempool@1.2.3.4/32", whitelistPermissions, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("bloom,forcerelay,noban,relay,mempool@1.2.3.4/32", whitelistPermissions, connection_direction, error)); + BOOST_CHECK(NetWhitelistPermissions::TryParse("in,relay@1.2.3.4", whitelistPermissions, connection_direction, error)); + BOOST_CHECK_EQUAL(connection_direction, ConnectionDirection::In); + BOOST_CHECK(NetWhitelistPermissions::TryParse("out,bloom@1.2.3.4", whitelistPermissions, connection_direction, error)); + BOOST_CHECK_EQUAL(connection_direction, ConnectionDirection::Out); + BOOST_CHECK(NetWhitelistPermissions::TryParse("in,out,bloom@1.2.3.4", whitelistPermissions, connection_direction, error)); + BOOST_CHECK_EQUAL(connection_direction, ConnectionDirection::Both); const auto strings = NetPermissions::ToStrings(NetPermissionFlags::All); BOOST_CHECK_EQUAL(strings.size(), 7U); diff --git a/src/test/orphanage_tests.cpp b/src/test/orphanage_tests.cpp index 4231fcc909..450bf6a4fc 100644 --- a/src/test/orphanage_tests.cpp +++ b/src/test/orphanage_tests.cpp @@ -30,22 +30,74 @@ public: CTransactionRef RandomOrphan() EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) { LOCK(m_mutex); - std::map<Txid, OrphanTx>::iterator it; - it = m_orphans.lower_bound(Txid::FromUint256(InsecureRand256())); + std::map<Wtxid, OrphanTx>::iterator it; + it = m_orphans.lower_bound(Wtxid::FromUint256(InsecureRand256())); if (it == m_orphans.end()) it = m_orphans.begin(); return it->second.tx; } }; -static void MakeNewKeyWithFastRandomContext(CKey& key) +static void MakeNewKeyWithFastRandomContext(CKey& key, FastRandomContext& rand_ctx = g_insecure_rand_ctx) { std::vector<unsigned char> keydata; - keydata = g_insecure_rand_ctx.randbytes(32); + keydata = rand_ctx.randbytes(32); key.Set(keydata.data(), keydata.data() + keydata.size(), /*fCompressedIn=*/true); assert(key.IsValid()); } +// Creates a transaction with 2 outputs. Spends all outpoints. If outpoints is empty, spends a random one. +static CTransactionRef MakeTransactionSpending(const std::vector<COutPoint>& outpoints, FastRandomContext& det_rand) +{ + CKey key; + MakeNewKeyWithFastRandomContext(key, det_rand); + CMutableTransaction tx; + // If no outpoints are given, create a random one. + if (outpoints.empty()) { + tx.vin.emplace_back(Txid::FromUint256(det_rand.rand256()), 0); + } else { + for (const auto& outpoint : outpoints) { + tx.vin.emplace_back(outpoint); + } + } + // Ensure txid != wtxid + tx.vin[0].scriptWitness.stack.push_back({1}); + tx.vout.resize(2); + tx.vout[0].nValue = CENT; + tx.vout[0].scriptPubKey = GetScriptForDestination(PKHash(key.GetPubKey())); + tx.vout[1].nValue = 3 * CENT; + tx.vout[1].scriptPubKey = GetScriptForDestination(WitnessV0KeyHash(key.GetPubKey())); + return MakeTransactionRef(tx); +} + +// Make another (not necessarily valid) tx with the same txid but different wtxid. +static CTransactionRef MakeMutation(const CTransactionRef& ptx) +{ + CMutableTransaction tx(*ptx); + tx.vin[0].scriptWitness.stack.push_back({5}); + auto mutated_tx = MakeTransactionRef(tx); + assert(ptx->GetHash() == mutated_tx->GetHash()); + return mutated_tx; +} + +static bool EqualTxns(const std::set<CTransactionRef>& set_txns, const std::vector<CTransactionRef>& vec_txns) +{ + if (vec_txns.size() != set_txns.size()) return false; + for (const auto& tx : vec_txns) { + if (!set_txns.contains(tx)) return false; + } + return true; +} +static bool EqualTxns(const std::set<CTransactionRef>& set_txns, + const std::vector<std::pair<CTransactionRef, NodeId>>& vec_txns) +{ + if (vec_txns.size() != set_txns.size()) return false; + for (const auto& [tx, nodeid] : vec_txns) { + if (!set_txns.contains(tx)) return false; + } + return true; +} + BOOST_AUTO_TEST_CASE(DoS_mapOrphans) { // This test had non-deterministic coverage due to @@ -138,4 +190,148 @@ BOOST_AUTO_TEST_CASE(DoS_mapOrphans) BOOST_CHECK(orphanage.CountOrphans() == 0); } +BOOST_AUTO_TEST_CASE(same_txid_diff_witness) +{ + FastRandomContext det_rand{true}; + TxOrphanage orphanage; + NodeId peer{0}; + + std::vector<COutPoint> empty_outpoints; + auto parent = MakeTransactionSpending(empty_outpoints, det_rand); + + // Create children to go into orphanage. + auto child_normal = MakeTransactionSpending({{parent->GetHash(), 0}}, det_rand); + auto child_mutated = MakeMutation(child_normal); + + const auto& normal_wtxid = child_normal->GetWitnessHash(); + const auto& mutated_wtxid = child_mutated->GetWitnessHash(); + BOOST_CHECK(normal_wtxid != mutated_wtxid); + + BOOST_CHECK(orphanage.AddTx(child_normal, peer)); + // EraseTx fails as transaction by this wtxid doesn't exist. + BOOST_CHECK_EQUAL(orphanage.EraseTx(mutated_wtxid), 0); + BOOST_CHECK(orphanage.HaveTx(normal_wtxid)); + BOOST_CHECK(!orphanage.HaveTx(mutated_wtxid)); + + // Must succeed. Both transactions should be present in orphanage. + BOOST_CHECK(orphanage.AddTx(child_mutated, peer)); + BOOST_CHECK(orphanage.HaveTx(normal_wtxid)); + BOOST_CHECK(orphanage.HaveTx(mutated_wtxid)); + + // Outpoints map should track all entries: check that both are returned as children of the parent. + std::set<CTransactionRef> expected_children{child_normal, child_mutated}; + BOOST_CHECK(EqualTxns(expected_children, orphanage.GetChildrenFromSamePeer(parent, peer))); + + // Erase by wtxid: mutated first + BOOST_CHECK_EQUAL(orphanage.EraseTx(mutated_wtxid), 1); + BOOST_CHECK(orphanage.HaveTx(normal_wtxid)); + BOOST_CHECK(!orphanage.HaveTx(mutated_wtxid)); + + BOOST_CHECK_EQUAL(orphanage.EraseTx(normal_wtxid), 1); + BOOST_CHECK(!orphanage.HaveTx(normal_wtxid)); + BOOST_CHECK(!orphanage.HaveTx(mutated_wtxid)); +} + + +BOOST_AUTO_TEST_CASE(get_children) +{ + FastRandomContext det_rand{true}; + std::vector<COutPoint> empty_outpoints; + + auto parent1 = MakeTransactionSpending(empty_outpoints, det_rand); + auto parent2 = MakeTransactionSpending(empty_outpoints, det_rand); + + // Make sure these parents have different txids otherwise this test won't make sense. + while (parent1->GetHash() == parent2->GetHash()) { + parent2 = MakeTransactionSpending(empty_outpoints, det_rand); + } + + // Create children to go into orphanage. + auto child_p1n0 = MakeTransactionSpending({{parent1->GetHash(), 0}}, det_rand); + auto child_p2n1 = MakeTransactionSpending({{parent2->GetHash(), 1}}, det_rand); + // Spends the same tx twice. Should not cause duplicates. + auto child_p1n0_p1n1 = MakeTransactionSpending({{parent1->GetHash(), 0}, {parent1->GetHash(), 1}}, det_rand); + // Spends the same outpoint as previous tx. Should still be returned; don't assume outpoints are unique. + auto child_p1n0_p2n0 = MakeTransactionSpending({{parent1->GetHash(), 0}, {parent2->GetHash(), 0}}, det_rand); + + const NodeId node1{1}; + const NodeId node2{2}; + + // All orphans provided by node1 + { + TxOrphanage orphanage; + BOOST_CHECK(orphanage.AddTx(child_p1n0, node1)); + BOOST_CHECK(orphanage.AddTx(child_p2n1, node1)); + BOOST_CHECK(orphanage.AddTx(child_p1n0_p1n1, node1)); + BOOST_CHECK(orphanage.AddTx(child_p1n0_p2n0, node1)); + + std::set<CTransactionRef> expected_parent1_children{child_p1n0, child_p1n0_p2n0, child_p1n0_p1n1}; + std::set<CTransactionRef> expected_parent2_children{child_p2n1, child_p1n0_p2n0}; + + BOOST_CHECK(EqualTxns(expected_parent1_children, orphanage.GetChildrenFromSamePeer(parent1, node1))); + BOOST_CHECK(EqualTxns(expected_parent2_children, orphanage.GetChildrenFromSamePeer(parent2, node1))); + + BOOST_CHECK(EqualTxns(expected_parent1_children, orphanage.GetChildrenFromDifferentPeer(parent1, node2))); + BOOST_CHECK(EqualTxns(expected_parent2_children, orphanage.GetChildrenFromDifferentPeer(parent2, node2))); + + // The peer must match + BOOST_CHECK(orphanage.GetChildrenFromSamePeer(parent1, node2).empty()); + BOOST_CHECK(orphanage.GetChildrenFromSamePeer(parent2, node2).empty()); + + // There shouldn't be any children of this tx in the orphanage + BOOST_CHECK(orphanage.GetChildrenFromSamePeer(child_p1n0_p2n0, node1).empty()); + BOOST_CHECK(orphanage.GetChildrenFromSamePeer(child_p1n0_p2n0, node2).empty()); + BOOST_CHECK(orphanage.GetChildrenFromDifferentPeer(child_p1n0_p2n0, node1).empty()); + BOOST_CHECK(orphanage.GetChildrenFromDifferentPeer(child_p1n0_p2n0, node2).empty()); + } + + // Orphans provided by node1 and node2 + { + TxOrphanage orphanage; + BOOST_CHECK(orphanage.AddTx(child_p1n0, node1)); + BOOST_CHECK(orphanage.AddTx(child_p2n1, node1)); + BOOST_CHECK(orphanage.AddTx(child_p1n0_p1n1, node2)); + BOOST_CHECK(orphanage.AddTx(child_p1n0_p2n0, node2)); + + // +----------------+---------------+----------------------------------+ + // | | sender=node1 | sender=node2 | + // +----------------+---------------+----------------------------------+ + // | spends parent1 | child_p1n0 | child_p1n0_p1n1, child_p1n0_p2n0 | + // | spends parent2 | child_p2n1 | child_p1n0_p2n0 | + // +----------------+---------------+----------------------------------+ + + // Children of parent1 from node1: + { + std::set<CTransactionRef> expected_parent1_node1{child_p1n0}; + + BOOST_CHECK(EqualTxns(expected_parent1_node1, orphanage.GetChildrenFromSamePeer(parent1, node1))); + BOOST_CHECK(EqualTxns(expected_parent1_node1, orphanage.GetChildrenFromDifferentPeer(parent1, node2))); + } + + // Children of parent2 from node1: + { + std::set<CTransactionRef> expected_parent2_node1{child_p2n1}; + + BOOST_CHECK(EqualTxns(expected_parent2_node1, orphanage.GetChildrenFromSamePeer(parent2, node1))); + BOOST_CHECK(EqualTxns(expected_parent2_node1, orphanage.GetChildrenFromDifferentPeer(parent2, node2))); + } + + // Children of parent1 from node2: + { + std::set<CTransactionRef> expected_parent1_node2{child_p1n0_p1n1, child_p1n0_p2n0}; + + BOOST_CHECK(EqualTxns(expected_parent1_node2, orphanage.GetChildrenFromSamePeer(parent1, node2))); + BOOST_CHECK(EqualTxns(expected_parent1_node2, orphanage.GetChildrenFromDifferentPeer(parent1, node1))); + } + + // Children of parent2 from node2: + { + std::set<CTransactionRef> expected_parent2_node2{child_p1n0_p2n0}; + + BOOST_CHECK(EqualTxns(expected_parent2_node2, orphanage.GetChildrenFromSamePeer(parent2, node2))); + BOOST_CHECK(EqualTxns(expected_parent2_node2, orphanage.GetChildrenFromDifferentPeer(parent2, node1))); + } + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp new file mode 100644 index 0000000000..28866695bc --- /dev/null +++ b/src/test/peerman_tests.cpp @@ -0,0 +1,76 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://www.opensource.org/licenses/mit-license.php. + +#include <chainparams.h> +#include <node/miner.h> +#include <net_processing.h> +#include <pow.h> +#include <test/util/setup_common.h> +#include <validation.h> + +#include <boost/test/unit_test.hpp> + +BOOST_FIXTURE_TEST_SUITE(peerman_tests, RegTestingSetup) + +/** Window, in blocks, for connecting to NODE_NETWORK_LIMITED peers */ +static constexpr int64_t NODE_NETWORK_LIMITED_ALLOW_CONN_BLOCKS = 144; + +static void mineBlock(const node::NodeContext& node, std::chrono::seconds block_time) +{ + auto curr_time = GetTime<std::chrono::seconds>(); + SetMockTime(block_time); // update time so the block is created with it + CBlock block = node::BlockAssembler{node.chainman->ActiveChainstate(), nullptr}.CreateNewBlock(CScript() << OP_TRUE)->block; + while (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) ++block.nNonce; + block.fChecked = true; // little speedup + SetMockTime(curr_time); // process block at current time + Assert(node.chainman->ProcessNewBlock(std::make_shared<const CBlock>(block), /*force_processing=*/true, /*min_pow_checked=*/true, nullptr)); + node.validation_signals->SyncWithValidationInterfaceQueue(); // drain events queue +} + +// Verifying when network-limited peer connections are desirable based on the node's proximity to the tip +BOOST_AUTO_TEST_CASE(connections_desirable_service_flags) +{ + std::unique_ptr<PeerManager> peerman = PeerManager::make(*m_node.connman, *m_node.addrman, nullptr, *m_node.chainman, *m_node.mempool, {}); + auto consensus = m_node.chainman->GetParams().GetConsensus(); + + // Check we start connecting to full nodes + ServiceFlags peer_flags{NODE_WITNESS | NODE_NETWORK_LIMITED}; + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK | NODE_WITNESS)); + + // Make peerman aware of the initial best block and verify we accept limited peers when we start close to the tip time. + auto tip = WITH_LOCK(::cs_main, return m_node.chainman->ActiveChain().Tip()); + uint64_t tip_block_time = tip->GetBlockTime(); + int tip_block_height = tip->nHeight; + peerman->SetBestBlock(tip_block_height, std::chrono::seconds{tip_block_time}); + + SetMockTime(tip_block_time + 1); // Set node time to tip time + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK_LIMITED | NODE_WITNESS)); + + // Check we don't disallow limited peers connections when we are behind but still recoverable (below the connection safety window) + SetMockTime(GetTime<std::chrono::seconds>() + std::chrono::seconds{consensus.nPowTargetSpacing * (NODE_NETWORK_LIMITED_ALLOW_CONN_BLOCKS - 1)}); + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK_LIMITED | NODE_WITNESS)); + + // Check we disallow limited peers connections when we are further than the limited peers safety window + SetMockTime(GetTime<std::chrono::seconds>() + std::chrono::seconds{consensus.nPowTargetSpacing * 2}); + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK | NODE_WITNESS)); + + // By now, we tested that the connections desirable services flags change based on the node's time proximity to the tip. + // Now, perform the same tests for when the node receives a block. + m_node.validation_signals->RegisterValidationInterface(peerman.get()); + + // First, verify a block in the past doesn't enable limited peers connections + // At this point, our time is (NODE_NETWORK_LIMITED_ALLOW_CONN_BLOCKS + 1) * 10 minutes ahead the tip's time. + mineBlock(m_node, /*block_time=*/std::chrono::seconds{tip_block_time + 1}); + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK | NODE_WITNESS)); + + // Verify a block close to the tip enables limited peers connections + mineBlock(m_node, /*block_time=*/GetTime<std::chrono::seconds>()); + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK_LIMITED | NODE_WITNESS)); + + // Lastly, verify the stale tip checks can disallow limited peers connections after not receiving blocks for a prolonged period. + SetMockTime(GetTime<std::chrono::seconds>() + std::chrono::seconds{consensus.nPowTargetSpacing * NODE_NETWORK_LIMITED_ALLOW_CONN_BLOCKS + 1}); + BOOST_CHECK(peerman->GetDesirableServiceFlags(peer_flags) == ServiceFlags(NODE_NETWORK | NODE_WITNESS)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/policyestimator_tests.cpp b/src/test/policyestimator_tests.cpp index ede73c6895..6cadc3290a 100644 --- a/src/test/policyestimator_tests.cpp +++ b/src/test/policyestimator_tests.cpp @@ -20,7 +20,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) { CBlockPolicyEstimator& feeEst = *Assert(m_node.fee_estimator); CTxMemPool& mpool = *Assert(m_node.mempool); - RegisterValidationInterface(&feeEst); + m_node.validation_signals->RegisterValidationInterface(&feeEst); TestMemPoolEntryHelper entry; CAmount basefee(2000); CAmount deltaFee(100); @@ -74,7 +74,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) /*submitted_in_package=*/false, /*chainstate_is_current=*/true, /*has_no_mempool_parents=*/true)}; - GetMainSignals().TransactionAddedToMempool(tx_info, mpool.GetAndIncrementSequence()); + m_node.validation_signals->TransactionAddedToMempool(tx_info, mpool.GetAndIncrementSequence()); } uint256 hash = tx.GetHash(); txHashes[j].push_back(hash); @@ -102,7 +102,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) // Check after just a few txs that combining buckets works as expected if (blocknum == 3) { // Wait for fee estimator to catch up - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); // At this point we should need to combine 3 buckets to get enough data points // So estimateFee(1) should fail and estimateFee(2) should return somewhere around // 9*baserate. estimateFee(2) %'s are 100,100,90 = average 97% @@ -113,7 +113,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) } // Wait for fee estimator to catch up - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); std::vector<CAmount> origFeeEst; // Highest feerate is 10*baseRate and gets in all blocks, @@ -146,7 +146,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) } // Wait for fee estimator to catch up - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0)); for (int i = 2; i < 10;i++) { @@ -175,7 +175,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) /*submitted_in_package=*/false, /*chainstate_is_current=*/true, /*has_no_mempool_parents=*/true)}; - GetMainSignals().TransactionAddedToMempool(tx_info, mpool.GetAndIncrementSequence()); + m_node.validation_signals->TransactionAddedToMempool(tx_info, mpool.GetAndIncrementSequence()); } uint256 hash = tx.GetHash(); txHashes[j].push_back(hash); @@ -188,7 +188,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) } // Wait for fee estimator to catch up - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); for (int i = 1; i < 10;i++) { BOOST_CHECK(feeEst.estimateFee(i) == CFeeRate(0) || feeEst.estimateFee(i).GetFeePerK() > origFeeEst[i-1] - deltaFee); @@ -212,7 +212,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) block.clear(); // Wait for fee estimator to catch up - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0)); for (int i = 2; i < 10;i++) { @@ -239,7 +239,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) /*submitted_in_package=*/false, /*chainstate_is_current=*/true, /*has_no_mempool_parents=*/true)}; - GetMainSignals().TransactionAddedToMempool(tx_info, mpool.GetAndIncrementSequence()); + m_node.validation_signals->TransactionAddedToMempool(tx_info, mpool.GetAndIncrementSequence()); } uint256 hash = tx.GetHash(); CTransactionRef ptx = mpool.get(hash); @@ -257,7 +257,7 @@ BOOST_AUTO_TEST_CASE(BlockPolicyEstimates) block.clear(); } // Wait for fee estimator to catch up - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); BOOST_CHECK(feeEst.estimateFee(1) == CFeeRate(0)); for (int i = 2; i < 9; i++) { // At 9, the original estimate was already at the bottom (b/c scale = 2) BOOST_CHECK(feeEst.estimateFee(i).GetFeePerK() < origFeeEst[i-1] - deltaFee); diff --git a/src/test/rbf_tests.cpp b/src/test/rbf_tests.cpp index fb6a3614c0..19e45c550a 100644 --- a/src/test/rbf_tests.cpp +++ b/src/test/rbf_tests.cpp @@ -37,7 +37,38 @@ static inline CTransactionRef make_tx(const std::vector<CTransactionRef>& inputs return MakeTransactionRef(tx); } -static void add_descendants(const CTransactionRef& tx, int32_t num_descendants, CTxMemPool& pool) +// Make two child transactions from parent (which must have at least 2 outputs). +// Each tx will have the same outputs, using the amounts specified in output_values. +static inline std::pair<CTransactionRef, CTransactionRef> make_two_siblings(const CTransactionRef parent, + const std::vector<CAmount>& output_values) +{ + assert(parent->vout.size() >= 2); + + // First tx takes first parent output + CMutableTransaction tx1 = CMutableTransaction(); + tx1.vin.resize(1); + tx1.vout.resize(output_values.size()); + + tx1.vin[0].prevout.hash = parent->GetHash(); + tx1.vin[0].prevout.n = 0; + // Add a witness so wtxid != txid + CScriptWitness witness; + witness.stack.emplace_back(10); + tx1.vin[0].scriptWitness = witness; + + for (size_t i = 0; i < output_values.size(); ++i) { + tx1.vout[i].scriptPubKey = CScript() << OP_11 << OP_EQUAL; + tx1.vout[i].nValue = output_values[i]; + } + + // Second tx takes second parent output + CMutableTransaction tx2 = tx1; + tx2.vin[0].prevout.n = 1; + + return std::make_pair(MakeTransactionRef(tx1), MakeTransactionRef(tx2)); +} + +static CTransactionRef add_descendants(const CTransactionRef& tx, int32_t num_descendants, CTxMemPool& pool) EXCLUSIVE_LOCKS_REQUIRED(::cs_main, pool.cs) { AssertLockHeld(::cs_main); @@ -50,6 +81,35 @@ static void add_descendants(const CTransactionRef& tx, int32_t num_descendants, pool.addUnchecked(entry.FromTx(next_tx)); tx_to_spend = next_tx; } + // Return last created tx + return tx_to_spend; +} + +static CTransactionRef add_descendant_to_parents(const std::vector<CTransactionRef>& parents, CTxMemPool& pool) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, pool.cs) +{ + AssertLockHeld(::cs_main); + AssertLockHeld(pool.cs); + TestMemPoolEntryHelper entry; + // Assumes this isn't already spent in mempool + auto child_tx = make_tx(/*inputs=*/parents, /*output_values=*/{50 * CENT}); + pool.addUnchecked(entry.FromTx(child_tx)); + // Return last created tx + return child_tx; +} + +// Makes two children for a single parent +static std::pair<CTransactionRef, CTransactionRef> add_children_to_parent(const CTransactionRef parent, CTxMemPool& pool) + EXCLUSIVE_LOCKS_REQUIRED(::cs_main, pool.cs) +{ + AssertLockHeld(::cs_main); + AssertLockHeld(pool.cs); + TestMemPoolEntryHelper entry; + // Assumes this isn't already spent in mempool + auto children_tx = make_two_siblings(/*parent=*/parent, /*output_values=*/{50 * CENT}); + pool.addUnchecked(entry.FromTx(children_tx.first)); + pool.addUnchecked(entry.FromTx(children_tx.second)); + return children_tx; } BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup) @@ -89,28 +149,51 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup) const auto tx8 = make_tx(/*inputs=*/ {m_coinbase_txns[4]}, /*output_values=*/ {999 * CENT}); pool.addUnchecked(entry.Fee(high_fee).FromTx(tx8)); - const auto entry1 = pool.GetIter(tx1->GetHash()).value(); - const auto entry2 = pool.GetIter(tx2->GetHash()).value(); - const auto entry3 = pool.GetIter(tx3->GetHash()).value(); - const auto entry4 = pool.GetIter(tx4->GetHash()).value(); - const auto entry5 = pool.GetIter(tx5->GetHash()).value(); - const auto entry6 = pool.GetIter(tx6->GetHash()).value(); - const auto entry7 = pool.GetIter(tx7->GetHash()).value(); - const auto entry8 = pool.GetIter(tx8->GetHash()).value(); - - BOOST_CHECK_EQUAL(entry1->GetFee(), normal_fee); - BOOST_CHECK_EQUAL(entry2->GetFee(), normal_fee); - BOOST_CHECK_EQUAL(entry3->GetFee(), low_fee); - BOOST_CHECK_EQUAL(entry4->GetFee(), high_fee); - BOOST_CHECK_EQUAL(entry5->GetFee(), low_fee); - BOOST_CHECK_EQUAL(entry6->GetFee(), low_fee); - BOOST_CHECK_EQUAL(entry7->GetFee(), high_fee); - BOOST_CHECK_EQUAL(entry8->GetFee(), high_fee); - - CTxMemPool::setEntries set_12_normal{entry1, entry2}; - CTxMemPool::setEntries set_34_cpfp{entry3, entry4}; - CTxMemPool::setEntries set_56_low{entry5, entry6}; - CTxMemPool::setEntries all_entries{entry1, entry2, entry3, entry4, entry5, entry6, entry7, entry8}; + // Normal txs, will chain txns right before CheckConflictTopology test + const auto tx9 = make_tx(/*inputs=*/ {m_coinbase_txns[5]}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx9)); + const auto tx10 = make_tx(/*inputs=*/ {m_coinbase_txns[6]}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx10)); + + // Will make these two parents of single child + const auto tx11 = make_tx(/*inputs=*/ {m_coinbase_txns[7]}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx11)); + const auto tx12 = make_tx(/*inputs=*/ {m_coinbase_txns[8]}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx12)); + + // Will make two children of this single parent + const auto tx13 = make_tx(/*inputs=*/ {m_coinbase_txns[9]}, /*output_values=*/ {995 * CENT, 995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx13)); + + const auto entry1_normal = pool.GetIter(tx1->GetHash()).value(); + const auto entry2_normal = pool.GetIter(tx2->GetHash()).value(); + const auto entry3_low = pool.GetIter(tx3->GetHash()).value(); + const auto entry4_high = pool.GetIter(tx4->GetHash()).value(); + const auto entry5_low = pool.GetIter(tx5->GetHash()).value(); + const auto entry6_low_prioritised = pool.GetIter(tx6->GetHash()).value(); + const auto entry7_high = pool.GetIter(tx7->GetHash()).value(); + const auto entry8_high = pool.GetIter(tx8->GetHash()).value(); + const auto entry9_unchained = pool.GetIter(tx9->GetHash()).value(); + const auto entry10_unchained = pool.GetIter(tx10->GetHash()).value(); + const auto entry11_unchained = pool.GetIter(tx11->GetHash()).value(); + const auto entry12_unchained = pool.GetIter(tx12->GetHash()).value(); + const auto entry13_unchained = pool.GetIter(tx13->GetHash()).value(); + + BOOST_CHECK_EQUAL(entry1_normal->GetFee(), normal_fee); + BOOST_CHECK_EQUAL(entry2_normal->GetFee(), normal_fee); + BOOST_CHECK_EQUAL(entry3_low->GetFee(), low_fee); + BOOST_CHECK_EQUAL(entry4_high->GetFee(), high_fee); + BOOST_CHECK_EQUAL(entry5_low->GetFee(), low_fee); + BOOST_CHECK_EQUAL(entry6_low_prioritised->GetFee(), low_fee); + BOOST_CHECK_EQUAL(entry7_high->GetFee(), high_fee); + BOOST_CHECK_EQUAL(entry8_high->GetFee(), high_fee); + + CTxMemPool::setEntries set_12_normal{entry1_normal, entry2_normal}; + CTxMemPool::setEntries set_34_cpfp{entry3_low, entry4_high}; + CTxMemPool::setEntries set_56_low{entry5_low, entry6_low_prioritised}; + CTxMemPool::setEntries set_78_high{entry7_high, entry8_high}; + CTxMemPool::setEntries all_entries{entry1_normal, entry2_normal, entry3_low, entry4_high, + entry5_low, entry6_low_prioritised, entry7_high, entry8_high}; CTxMemPool::setEntries empty_set; const auto unused_txid{GetRandHash()}; @@ -118,31 +201,29 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup) // Tests for PaysMoreThanConflicts // These tests use feerate, not absolute fee. BOOST_CHECK(PaysMoreThanConflicts(/*iters_conflicting=*/set_12_normal, - /*replacement_feerate=*/CFeeRate(entry1->GetModifiedFee() + 1, entry1->GetTxSize() + 2), + /*replacement_feerate=*/CFeeRate(entry1_normal->GetModifiedFee() + 1, entry1_normal->GetTxSize() + 2), /*txid=*/unused_txid).has_value()); // Replacement must be strictly greater than the originals. - BOOST_CHECK(PaysMoreThanConflicts(set_12_normal, CFeeRate(entry1->GetModifiedFee(), entry1->GetTxSize()), unused_txid).has_value()); - BOOST_CHECK(PaysMoreThanConflicts(set_12_normal, CFeeRate(entry1->GetModifiedFee() + 1, entry1->GetTxSize()), unused_txid) == std::nullopt); + BOOST_CHECK(PaysMoreThanConflicts(set_12_normal, CFeeRate(entry1_normal->GetModifiedFee(), entry1_normal->GetTxSize()), unused_txid).has_value()); + BOOST_CHECK(PaysMoreThanConflicts(set_12_normal, CFeeRate(entry1_normal->GetModifiedFee() + 1, entry1_normal->GetTxSize()), unused_txid) == std::nullopt); // These tests use modified fees (including prioritisation), not base fees. - BOOST_CHECK(PaysMoreThanConflicts({entry5}, CFeeRate(entry5->GetModifiedFee() + 1, entry5->GetTxSize()), unused_txid) == std::nullopt); - BOOST_CHECK(PaysMoreThanConflicts({entry6}, CFeeRate(entry6->GetFee() + 1, entry6->GetTxSize()), unused_txid).has_value()); - BOOST_CHECK(PaysMoreThanConflicts({entry6}, CFeeRate(entry6->GetModifiedFee() + 1, entry6->GetTxSize()), unused_txid) == std::nullopt); + BOOST_CHECK(PaysMoreThanConflicts({entry5_low}, CFeeRate(entry5_low->GetModifiedFee() + 1, entry5_low->GetTxSize()), unused_txid) == std::nullopt); + BOOST_CHECK(PaysMoreThanConflicts({entry6_low_prioritised}, CFeeRate(entry6_low_prioritised->GetFee() + 1, entry6_low_prioritised->GetTxSize()), unused_txid).has_value()); + BOOST_CHECK(PaysMoreThanConflicts({entry6_low_prioritised}, CFeeRate(entry6_low_prioritised->GetModifiedFee() + 1, entry6_low_prioritised->GetTxSize()), unused_txid) == std::nullopt); // PaysMoreThanConflicts checks individual feerate, not ancestor feerate. This test compares - // replacement_feerate and entry4's feerate, which are the same. The replacement_feerate is - // considered too low even though entry4 has a low ancestor feerate. - BOOST_CHECK(PaysMoreThanConflicts(set_34_cpfp, CFeeRate(entry4->GetModifiedFee(), entry4->GetTxSize()), unused_txid).has_value()); + // replacement_feerate and entry4_high's feerate, which are the same. The replacement_feerate is + // considered too low even though entry4_high has a low ancestor feerate. + BOOST_CHECK(PaysMoreThanConflicts(set_34_cpfp, CFeeRate(entry4_high->GetModifiedFee(), entry4_high->GetTxSize()), unused_txid).has_value()); // Tests for EntriesAndTxidsDisjoint BOOST_CHECK(EntriesAndTxidsDisjoint(empty_set, {tx1->GetHash()}, unused_txid) == std::nullopt); BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx3->GetHash()}, unused_txid) == std::nullopt); - // EntriesAndTxidsDisjoint uses txids, not wtxids. - BOOST_CHECK(EntriesAndTxidsDisjoint({entry2}, {tx2->GetWitnessHash()}, unused_txid) == std::nullopt); - BOOST_CHECK(EntriesAndTxidsDisjoint({entry2}, {tx2->GetHash()}, unused_txid).has_value()); + BOOST_CHECK(EntriesAndTxidsDisjoint({entry2_normal}, {tx2->GetHash()}, unused_txid).has_value()); BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx1->GetHash()}, unused_txid).has_value()); BOOST_CHECK(EntriesAndTxidsDisjoint(set_12_normal, {tx2->GetHash()}, unused_txid).has_value()); // EntriesAndTxidsDisjoint does not calculate descendants of iters_conflicting; it uses whatever - // the caller passed in. As such, no error is returned even though entry2 is a descendant of tx1. - BOOST_CHECK(EntriesAndTxidsDisjoint({entry2}, {tx1->GetHash()}, unused_txid) == std::nullopt); + // the caller passed in. As such, no error is returned even though entry2_normal is a descendant of tx1. + BOOST_CHECK(EntriesAndTxidsDisjoint({entry2_normal}, {tx1->GetHash()}, unused_txid) == std::nullopt); // Tests for PaysForRBF const CFeeRate incremental_relay_feerate{DEFAULT_INCREMENTAL_RELAY_FEE}; @@ -165,8 +246,8 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup) BOOST_CHECK(PaysForRBF(low_fee, high_fee + 99999999, 99999999, incremental_relay_feerate, unused_txid) == std::nullopt); // Tests for GetEntriesForConflicts - CTxMemPool::setEntries all_parents{entry1, entry3, entry5, entry7, entry8}; - CTxMemPool::setEntries all_children{entry2, entry4, entry6}; + CTxMemPool::setEntries all_parents{entry1_normal, entry3_low, entry5_low, entry7_high, entry8_high}; + CTxMemPool::setEntries all_children{entry2_normal, entry4_high, entry6_low_prioritised}; const std::vector<CTransactionRef> parent_inputs({m_coinbase_txns[0], m_coinbase_txns[1], m_coinbase_txns[2], m_coinbase_txns[3], m_coinbase_txns[4]}); const auto conflicts_with_parents = make_tx(parent_inputs, {50 * CENT}); @@ -217,15 +298,328 @@ BOOST_FIXTURE_TEST_CASE(rbf_helper_functions, TestChain100Setup) BOOST_CHECK(HasNoNewUnconfirmed(/*tx=*/ *spends_unconfirmed.get(), /*pool=*/ pool, /*iters_conflicting=*/ all_entries) == std::nullopt); - BOOST_CHECK(HasNoNewUnconfirmed(*spends_unconfirmed.get(), pool, {entry2}) == std::nullopt); + BOOST_CHECK(HasNoNewUnconfirmed(*spends_unconfirmed.get(), pool, {entry2_normal}) == std::nullopt); BOOST_CHECK(HasNoNewUnconfirmed(*spends_unconfirmed.get(), pool, empty_set).has_value()); const auto spends_new_unconfirmed = make_tx({tx1, tx8}, {36 * CENT}); - BOOST_CHECK(HasNoNewUnconfirmed(*spends_new_unconfirmed.get(), pool, {entry2}).has_value()); + BOOST_CHECK(HasNoNewUnconfirmed(*spends_new_unconfirmed.get(), pool, {entry2_normal}).has_value()); BOOST_CHECK(HasNoNewUnconfirmed(*spends_new_unconfirmed.get(), pool, all_entries).has_value()); const auto spends_conflicting_confirmed = make_tx({m_coinbase_txns[0], m_coinbase_txns[1]}, {45 * CENT}); - BOOST_CHECK(HasNoNewUnconfirmed(*spends_conflicting_confirmed.get(), pool, {entry1, entry3}) == std::nullopt); + BOOST_CHECK(HasNoNewUnconfirmed(*spends_conflicting_confirmed.get(), pool, {entry1_normal, entry3_low}) == std::nullopt); + + // Tests for CheckConflictTopology + + // Tx4 has 23 descendants + BOOST_CHECK_EQUAL(pool.CheckConflictTopology(set_34_cpfp).value(), strprintf("%s has 23 descendants, max 1 allowed", entry4_high->GetSharedTx()->GetHash().ToString())); + + // No descendants yet + BOOST_CHECK(pool.CheckConflictTopology({entry9_unchained}) == std::nullopt); + + // Add 1 descendant, still ok + add_descendants(tx9, 1, pool); + BOOST_CHECK(pool.CheckConflictTopology({entry9_unchained}) == std::nullopt); + + // N direct conflicts; ok + BOOST_CHECK(pool.CheckConflictTopology({entry9_unchained, entry10_unchained, entry11_unchained}) == std::nullopt); + + // Add 1 descendant, still ok, even if it's considered a direct conflict as well + const auto child_tx = add_descendants(tx10, 1, pool); + const auto entry10_child = pool.GetIter(child_tx->GetHash()).value(); + BOOST_CHECK(pool.CheckConflictTopology({entry9_unchained, entry10_unchained, entry11_unchained}) == std::nullopt); + BOOST_CHECK(pool.CheckConflictTopology({entry9_unchained, entry10_unchained, entry11_unchained, entry10_child}) == std::nullopt); + + // One more, size 3 cluster too much + const auto grand_child_tx = add_descendants(child_tx, 1, pool); + const auto entry10_grand_child = pool.GetIter(grand_child_tx->GetHash()).value(); + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry9_unchained, entry10_unchained, entry11_unchained}).value(), strprintf("%s has 2 descendants, max 1 allowed", entry10_unchained->GetSharedTx()->GetHash().ToString())); + // even if direct conflict is descendent itself + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry9_unchained, entry10_grand_child, entry11_unchained}).value(), strprintf("%s has 2 ancestors, max 1 allowed", entry10_grand_child->GetSharedTx()->GetHash().ToString())); + + // Make a single child from two singleton parents + const auto two_parent_child_tx = add_descendant_to_parents({tx11, tx12}, pool); + const auto entry_two_parent_child = pool.GetIter(two_parent_child_tx->GetHash()).value(); + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry11_unchained}).value(), strprintf("%s is not the only parent of child %s", entry11_unchained->GetSharedTx()->GetHash().ToString(), entry_two_parent_child->GetSharedTx()->GetHash().ToString())); + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry12_unchained}).value(), strprintf("%s is not the only parent of child %s", entry12_unchained->GetSharedTx()->GetHash().ToString(), entry_two_parent_child->GetSharedTx()->GetHash().ToString())); + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry_two_parent_child}).value(), strprintf("%s has 2 ancestors, max 1 allowed", entry_two_parent_child->GetSharedTx()->GetHash().ToString())); + + // Single parent with two children, we will conflict with the siblings directly only + const auto two_siblings = add_children_to_parent(tx13, pool); + const auto entry_sibling_1 = pool.GetIter(two_siblings.first->GetHash()).value(); + const auto entry_sibling_2 = pool.GetIter(two_siblings.second->GetHash()).value(); + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry_sibling_1}).value(), strprintf("%s is not the only child of parent %s", entry_sibling_1->GetSharedTx()->GetHash().ToString(), entry13_unchained->GetSharedTx()->GetHash().ToString())); + BOOST_CHECK_EQUAL(pool.CheckConflictTopology({entry_sibling_2}).value(), strprintf("%s is not the only child of parent %s", entry_sibling_2->GetSharedTx()->GetHash().ToString(), entry13_unchained->GetSharedTx()->GetHash().ToString())); + +} + +BOOST_FIXTURE_TEST_CASE(improves_feerate, TestChain100Setup) +{ + CTxMemPool& pool = *Assert(m_node.mempool); + LOCK2(::cs_main, pool.cs); + TestMemPoolEntryHelper entry; + + const CAmount low_fee{CENT/100}; + const CAmount normal_fee{CENT/10}; + + // low feerate parent with normal feerate child + const auto tx1 = make_tx(/*inputs=*/ {m_coinbase_txns[0]}, /*output_values=*/ {10 * COIN}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(tx1)); + const auto tx2 = make_tx(/*inputs=*/ {tx1}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx2)); + + const auto entry1 = pool.GetIter(tx1->GetHash()).value(); + const auto tx1_fee = entry1->GetModifiedFee(); + const auto tx1_size = entry1->GetTxSize(); + const auto entry2 = pool.GetIter(tx2->GetHash()).value(); + const auto tx2_fee = entry2->GetModifiedFee(); + const auto tx2_size = entry2->GetTxSize(); + + // Now test ImprovesFeerateDiagram with various levels of "package rbf" feerates + + // It doesn't improve itself + const auto res1 = ImprovesFeerateDiagram(pool, {entry1}, {entry1, entry2}, tx1_fee + tx2_fee, tx1_size + tx2_size); + BOOST_CHECK(res1.has_value()); + BOOST_CHECK(res1.value().first == DiagramCheckError::FAILURE); + BOOST_CHECK(res1.value().second == "insufficient feerate: does not improve feerate diagram"); + + // With one more satoshi it does + BOOST_CHECK(ImprovesFeerateDiagram(pool, {entry1}, {entry1, entry2}, tx1_fee + tx2_fee + 1, tx1_size + tx2_size) == std::nullopt); + + // With prioritisation of in-mempool conflicts, it affects the results of the comparison using the same args as just above + pool.PrioritiseTransaction(entry1->GetSharedTx()->GetHash(), /*nFeeDelta=*/1); + const auto res2 = ImprovesFeerateDiagram(pool, {entry1}, {entry1, entry2}, tx1_fee + tx2_fee + 1, tx1_size + tx2_size); + BOOST_CHECK(res2.has_value()); + BOOST_CHECK(res2.value().first == DiagramCheckError::FAILURE); + BOOST_CHECK(res2.value().second == "insufficient feerate: does not improve feerate diagram"); + pool.PrioritiseTransaction(entry1->GetSharedTx()->GetHash(), /*nFeeDelta=*/-1); + + // With one less vB it does + BOOST_CHECK(ImprovesFeerateDiagram(pool, {entry1}, {entry1, entry2}, tx1_fee + tx2_fee, tx1_size + tx2_size - 1) == std::nullopt); + + // Adding a grandchild makes the cluster size 3, which is uncalculable + const auto tx3 = make_tx(/*inputs=*/ {tx2}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(tx3)); + const auto res3 = ImprovesFeerateDiagram(pool, {entry1}, {entry1, entry2}, tx1_fee + tx2_fee + 1, tx1_size + tx2_size); + BOOST_CHECK(res3.has_value()); + BOOST_CHECK(res3.value().first == DiagramCheckError::UNCALCULABLE); + BOOST_CHECK(res3.value().second == strprintf("%s has 2 descendants, max 1 allowed", tx1->GetHash().GetHex())); + +} + +BOOST_FIXTURE_TEST_CASE(calc_feerate_diagram_rbf, TestChain100Setup) +{ + CTxMemPool& pool = *Assert(m_node.mempool); + LOCK2(::cs_main, pool.cs); + TestMemPoolEntryHelper entry; + + const CAmount low_fee{CENT/100}; + const CAmount normal_fee{CENT/10}; + const CAmount high_fee{CENT}; + + // low -> high -> medium fee transactions that would result in two chunks together since they + // are all same size + const auto low_tx = make_tx(/*inputs=*/ {m_coinbase_txns[0]}, /*output_values=*/ {10 * COIN}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(low_tx)); + + const auto entry_low = pool.GetIter(low_tx->GetHash()).value(); + const auto low_size = entry_low->GetTxSize(); + + // Replacement of size 1 + { + const auto replace_one{pool.CalculateChunksForRBF(/*replacement_fees=*/0, /*replacement_vsize=*/1, {entry_low}, {entry_low})}; + BOOST_CHECK(replace_one.has_value()); + std::vector<FeeFrac> expected_old_chunks{{low_fee, low_size}}; + BOOST_CHECK(replace_one->first == expected_old_chunks); + std::vector<FeeFrac> expected_new_chunks{{0, 1}}; + BOOST_CHECK(replace_one->second == expected_new_chunks); + } + + // Non-zero replacement fee/size + { + const auto replace_one_fee{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_low}, {entry_low})}; + BOOST_CHECK(replace_one_fee.has_value()); + std::vector<FeeFrac> expected_old_diagram{{low_fee, low_size}}; + BOOST_CHECK(replace_one_fee->first == expected_old_diagram); + std::vector<FeeFrac> expected_new_diagram{{high_fee, low_size}}; + BOOST_CHECK(replace_one_fee->second == expected_new_diagram); + } + + // Add a second transaction to the cluster that will make a single chunk, to be evicted in the RBF + const auto high_tx = make_tx(/*inputs=*/ {low_tx}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(high_fee).FromTx(high_tx)); + const auto entry_high = pool.GetIter(high_tx->GetHash()).value(); + const auto high_size = entry_high->GetTxSize(); + + { + const auto replace_single_chunk{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_low}, {entry_low, entry_high})}; + BOOST_CHECK(replace_single_chunk.has_value()); + std::vector<FeeFrac> expected_old_chunks{{low_fee + high_fee, low_size + high_size}}; + BOOST_CHECK(replace_single_chunk->first == expected_old_chunks); + std::vector<FeeFrac> expected_new_chunks{{high_fee, low_size}}; + BOOST_CHECK(replace_single_chunk->second == expected_new_chunks); + } + + // Conflict with the 2nd tx, resulting in new diagram with three entries + { + const auto replace_cpfp_child{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_high}, {entry_high})}; + BOOST_CHECK(replace_cpfp_child.has_value()); + std::vector<FeeFrac> expected_old_chunks{{low_fee + high_fee, low_size + high_size}}; + BOOST_CHECK(replace_cpfp_child->first == expected_old_chunks); + std::vector<FeeFrac> expected_new_chunks{{high_fee, low_size}, {low_fee, low_size}}; + BOOST_CHECK(replace_cpfp_child->second == expected_new_chunks); + } + + // third transaction causes the topology check to fail + const auto normal_tx = make_tx(/*inputs=*/ {high_tx}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(normal_fee).FromTx(normal_tx)); + const auto entry_normal = pool.GetIter(normal_tx->GetHash()).value(); + const auto normal_size = entry_normal->GetTxSize(); + + { + const auto replace_too_large{pool.CalculateChunksForRBF(/*replacement_fees=*/normal_fee, /*replacement_vsize=*/normal_size, {entry_low}, {entry_low, entry_high, entry_normal})}; + BOOST_CHECK(!replace_too_large.has_value()); + BOOST_CHECK_EQUAL(util::ErrorString(replace_too_large).original, strprintf("%s has 2 descendants, max 1 allowed", low_tx->GetHash().GetHex())); + } + + // Make a size 2 cluster that is itself two chunks; evict both txns + const auto high_tx_2 = make_tx(/*inputs=*/ {m_coinbase_txns[1]}, /*output_values=*/ {10 * COIN}); + pool.addUnchecked(entry.Fee(high_fee).FromTx(high_tx_2)); + const auto entry_high_2 = pool.GetIter(high_tx_2->GetHash()).value(); + const auto high_size_2 = entry_high_2->GetTxSize(); + + const auto low_tx_2 = make_tx(/*inputs=*/ {high_tx_2}, /*output_values=*/ {9 * COIN}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(low_tx_2)); + const auto entry_low_2 = pool.GetIter(low_tx_2->GetHash()).value(); + const auto low_size_2 = entry_low_2->GetTxSize(); + + { + const auto replace_two_chunks_single_cluster{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {entry_high_2}, {entry_high_2, entry_low_2})}; + BOOST_CHECK(replace_two_chunks_single_cluster.has_value()); + std::vector<FeeFrac> expected_old_chunks{{high_fee, high_size_2}, {low_fee, low_size_2}}; + BOOST_CHECK(replace_two_chunks_single_cluster->first == expected_old_chunks); + std::vector<FeeFrac> expected_new_chunks{{high_fee, low_size_2}}; + BOOST_CHECK(replace_two_chunks_single_cluster->second == expected_new_chunks); + } + + // You can have more than two direct conflicts if the there are multiple affected clusters, all of size 2 or less + const auto conflict_1 = make_tx(/*inputs=*/ {m_coinbase_txns[2]}, /*output_values=*/ {10 * COIN}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(conflict_1)); + const auto conflict_1_entry = pool.GetIter(conflict_1->GetHash()).value(); + + const auto conflict_2 = make_tx(/*inputs=*/ {m_coinbase_txns[3]}, /*output_values=*/ {10 * COIN}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(conflict_2)); + const auto conflict_2_entry = pool.GetIter(conflict_2->GetHash()).value(); + + const auto conflict_3 = make_tx(/*inputs=*/ {m_coinbase_txns[4]}, /*output_values=*/ {10 * COIN}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(conflict_3)); + const auto conflict_3_entry = pool.GetIter(conflict_3->GetHash()).value(); + + { + const auto replace_multiple_clusters{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry})}; + BOOST_CHECK(replace_multiple_clusters.has_value()); + BOOST_CHECK(replace_multiple_clusters->first.size() == 3); + BOOST_CHECK(replace_multiple_clusters->second.size() == 1); + } + + // Add a child transaction to conflict_1 and make it cluster size 2, two chunks due to same feerate + const auto conflict_1_child = make_tx(/*inputs=*/{conflict_1}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(low_fee).FromTx(conflict_1_child)); + const auto conflict_1_child_entry = pool.GetIter(conflict_1_child->GetHash()).value(); + + { + const auto replace_multiple_clusters_2{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry, conflict_1_child_entry})}; + + BOOST_CHECK(replace_multiple_clusters_2.has_value()); + BOOST_CHECK(replace_multiple_clusters_2->first.size() == 4); + BOOST_CHECK(replace_multiple_clusters_2->second.size() == 1); + } + + // Add another descendant to conflict_1, making the cluster size > 2 should fail at this point. + const auto conflict_1_grand_child = make_tx(/*inputs=*/{conflict_1_child}, /*output_values=*/ {995 * CENT}); + pool.addUnchecked(entry.Fee(high_fee).FromTx(conflict_1_grand_child)); + const auto conflict_1_grand_child_entry = pool.GetIter(conflict_1_child->GetHash()).value(); + + { + const auto replace_cluster_size_3{pool.CalculateChunksForRBF(/*replacement_fees=*/high_fee, /*replacement_vsize=*/low_size, {conflict_1_entry, conflict_2_entry, conflict_3_entry}, {conflict_1_entry, conflict_2_entry, conflict_3_entry, conflict_1_child_entry, conflict_1_grand_child_entry})}; + + BOOST_CHECK(!replace_cluster_size_3.has_value()); + BOOST_CHECK_EQUAL(util::ErrorString(replace_cluster_size_3).original, strprintf("%s has 2 descendants, max 1 allowed", conflict_1->GetHash().GetHex())); + } +} + +BOOST_AUTO_TEST_CASE(feerate_chunks_utilities) +{ + // Sanity check the correctness of the feerate chunks comparison. + + // A strictly better case. + std::vector<FeeFrac> old_chunks{{{950, 300}, {100, 100}}}; + std::vector<FeeFrac> new_chunks{{{1000, 300}, {50, 100}}}; + + BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks))); + + // Incomparable diagrams + old_chunks = {{950, 300}, {100, 100}}; + new_chunks = {{1000, 300}, {0, 100}}; + + BOOST_CHECK(CompareChunks(old_chunks, new_chunks) == std::partial_ordering::unordered); + BOOST_CHECK(CompareChunks(new_chunks, old_chunks) == std::partial_ordering::unordered); + + // Strictly better but smaller size. + old_chunks = {{950, 300}, {100, 100}}; + new_chunks = {{1100, 300}}; + + BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks))); + + // New diagram is strictly better due to the first chunk, even though + // second chunk contributes no fees + old_chunks = {{950, 300}, {100, 100}}; + new_chunks = {{1100, 100}, {0, 100}}; + + BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks))); + + // Feerate of first new chunk is better with, but second chunk is worse + old_chunks = {{950, 300}, {100, 100}}; + new_chunks = {{750, 100}, {249, 250}, {151, 650}}; + + BOOST_CHECK(CompareChunks(old_chunks, new_chunks) == std::partial_ordering::unordered); + BOOST_CHECK(CompareChunks(new_chunks, old_chunks) == std::partial_ordering::unordered); + + // If we make the second chunk slightly better, the new diagram now wins. + old_chunks = {{950, 300}, {100, 100}}; + new_chunks = {{750, 100}, {250, 250}, {150, 150}}; + + BOOST_CHECK(std::is_lt(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_gt(CompareChunks(new_chunks, old_chunks))); + + // Identical diagrams, cannot be strictly better + old_chunks = {{950, 300}, {100, 100}}; + new_chunks = {{950, 300}, {100, 100}}; + + BOOST_CHECK(std::is_eq(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_eq(CompareChunks(new_chunks, old_chunks))); + + // Same aggregate fee, but different total size (trigger single tail fee check step) + old_chunks = {{950, 300}, {100, 99}}; + new_chunks = {{950, 300}, {100, 100}}; + + // No change in evaluation when tail check needed. + BOOST_CHECK(std::is_gt(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_lt(CompareChunks(new_chunks, old_chunks))); + + // Trigger multiple tail fee check steps + old_chunks = {{950, 300}, {100, 99}}; + new_chunks = {{950, 300}, {100, 100}, {0, 1}, {0, 1}}; + + BOOST_CHECK(std::is_gt(CompareChunks(old_chunks, new_chunks))); + BOOST_CHECK(std::is_lt(CompareChunks(new_chunks, old_chunks))); + + // Multiple tail fee check steps, unordered result + new_chunks = {{950, 300}, {100, 100}, {0, 1}, {0, 1}, {1, 1}}; + BOOST_CHECK(CompareChunks(old_chunks, new_chunks) == std::partial_ordering::unordered); + BOOST_CHECK(CompareChunks(new_chunks, old_chunks) == std::partial_ordering::unordered); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/rpc_tests.cpp b/src/test/rpc_tests.cpp index 0d2460c606..acacb6257d 100644 --- a/src/test/rpc_tests.cpp +++ b/src/test/rpc_tests.cpp @@ -291,6 +291,7 @@ BOOST_AUTO_TEST_CASE(rpc_parse_monetary_values) BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("1e-8")), COIN/100000000); BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("0.1e-7")), COIN/100000000); BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("0.01e-6")), COIN/100000000); + BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("0.00000000000000000000000000000000000001e+30")), 1); BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("0.0000000000000000000000000000000000000000000000000000000000000000000000000001e+68")), COIN/100000000); BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("10000000000000000000000000000000000000000000000000000000000000000e-64")), COIN); BOOST_CHECK_EQUAL(AmountFromValue(ValueFromString("0.000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000e64")), COIN); @@ -581,4 +582,72 @@ BOOST_AUTO_TEST_CASE(help_example) BOOST_CHECK_NE(HelpExampleRpcNamed("foo", {{"arg", true}}), HelpExampleRpcNamed("foo", {{"arg", "true"}})); } +static void CheckRpc(const std::vector<RPCArg>& params, const UniValue& args, RPCHelpMan::RPCMethodImpl test_impl) +{ + auto null_result{RPCResult{RPCResult::Type::NONE, "", "None"}}; + const RPCHelpMan rpc{"dummy", "dummy description", params, null_result, RPCExamples{""}, test_impl}; + JSONRPCRequest req; + req.params = args; + + rpc.HandleRequest(req); +} + +BOOST_AUTO_TEST_CASE(rpc_arg_helper) +{ + constexpr bool DEFAULT_BOOL = true; + constexpr auto DEFAULT_STRING = "default"; + constexpr uint64_t DEFAULT_UINT64_T = 3; + + //! Parameters with which the RPCHelpMan is instantiated + const std::vector<RPCArg> params{ + // Required arg + {"req_int", RPCArg::Type::NUM, RPCArg::Optional::NO, ""}, + {"req_str", RPCArg::Type::STR, RPCArg::Optional::NO, ""}, + // Default arg + {"def_uint64_t", RPCArg::Type::NUM, RPCArg::Default{DEFAULT_UINT64_T}, ""}, + {"def_string", RPCArg::Type::STR, RPCArg::Default{DEFAULT_STRING}, ""}, + {"def_bool", RPCArg::Type::BOOL, RPCArg::Default{DEFAULT_BOOL}, ""}, + // Optional arg without default + {"opt_double", RPCArg::Type::NUM, RPCArg::Optional::OMITTED, ""}, + {"opt_string", RPCArg::Type::STR, RPCArg::Optional::OMITTED, ""} + }; + + //! Check that `self.Arg` returns the same value as the `request.params` accessors + RPCHelpMan::RPCMethodImpl check_positional = [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + BOOST_CHECK_EQUAL(self.Arg<int>(0), request.params[0].getInt<int>()); + BOOST_CHECK_EQUAL(self.Arg<std::string>(1), request.params[1].get_str()); + BOOST_CHECK_EQUAL(self.Arg<uint64_t>(2), request.params[2].isNull() ? DEFAULT_UINT64_T : request.params[2].getInt<uint64_t>()); + BOOST_CHECK_EQUAL(self.Arg<std::string>(3), request.params[3].isNull() ? DEFAULT_STRING : request.params[3].get_str()); + BOOST_CHECK_EQUAL(self.Arg<bool>(4), request.params[4].isNull() ? DEFAULT_BOOL : request.params[4].get_bool()); + if (!request.params[5].isNull()) { + BOOST_CHECK_EQUAL(self.MaybeArg<double>(5).value(), request.params[5].get_real()); + } else { + BOOST_CHECK(!self.MaybeArg<double>(5)); + } + if (!request.params[6].isNull()) { + BOOST_CHECK(self.MaybeArg<std::string>(6)); + BOOST_CHECK_EQUAL(*self.MaybeArg<std::string>(6), request.params[6].get_str()); + } else { + BOOST_CHECK(!self.MaybeArg<std::string>(6)); + } + return UniValue{}; + }; + CheckRpc(params, UniValue{JSON(R"([5, "hello", null, null, null, null, null])")}, check_positional); + CheckRpc(params, UniValue{JSON(R"([5, "hello", 4, "test", true, 1.23, "world"])")}, check_positional); + + //! Check that `self.Arg` returns the same value when using index and key + RPCHelpMan::RPCMethodImpl check_named = [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { + BOOST_CHECK_EQUAL(self.Arg<int>(0), self.Arg<int>("req_int")); + BOOST_CHECK_EQUAL(self.Arg<std::string>(1), self.Arg<std::string>("req_str")); + BOOST_CHECK_EQUAL(self.Arg<uint64_t>(2), self.Arg<uint64_t>("def_uint64_t")); + BOOST_CHECK_EQUAL(self.Arg<std::string>(3), self.Arg<std::string>("def_string")); + BOOST_CHECK_EQUAL(self.Arg<bool>(4), self.Arg<bool>("def_bool")); + BOOST_CHECK(self.MaybeArg<double>(5) == self.MaybeArg<double>("opt_double")); + BOOST_CHECK(self.MaybeArg<std::string>(6) == self.MaybeArg<std::string>("opt_string")); + return UniValue{}; + }; + CheckRpc(params, UniValue{JSON(R"([5, "hello", null, null, null, null, null])")}, check_named); + CheckRpc(params, UniValue{JSON(R"([5, "hello", 4, "test", true, 1.23, "world"])")}, check_named); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/sanity_tests.cpp b/src/test/sanity_tests.cpp index 451bc99d44..68f82b760c 100644 --- a/src/test/sanity_tests.cpp +++ b/src/test/sanity_tests.cpp @@ -4,7 +4,6 @@ #include <key.h> #include <test/util/setup_common.h> -#include <util/time.h> #include <boost/test/unit_test.hpp> @@ -13,7 +12,6 @@ BOOST_FIXTURE_TEST_SUITE(sanity_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(basic_sanity) { BOOST_CHECK_MESSAGE(ECC_InitSanityCheck() == true, "secp256k1 sanity test"); - BOOST_CHECK_MESSAGE(ChronoSanityCheck() == true, "chrono epoch test"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/scheduler_tests.cpp b/src/test/scheduler_tests.cpp index 3fb3378598..95e725de46 100644 --- a/src/test/scheduler_tests.cpp +++ b/src/test/scheduler_tests.cpp @@ -129,8 +129,8 @@ BOOST_AUTO_TEST_CASE(singlethreadedscheduler_ordered) CScheduler scheduler; // each queue should be well ordered with respect to itself but not other queues - SingleThreadedSchedulerClient queue1(scheduler); - SingleThreadedSchedulerClient queue2(scheduler); + SerialTaskRunner queue1(scheduler); + SerialTaskRunner queue2(scheduler); // create more threads than queues // if the queues only permit execution of one task at once then @@ -142,7 +142,7 @@ BOOST_AUTO_TEST_CASE(singlethreadedscheduler_ordered) threads.emplace_back([&] { scheduler.serviceQueue(); }); } - // these are not atomic, if SinglethreadedSchedulerClient prevents + // these are not atomic, if SerialTaskRunner prevents // parallel execution at the queue level no synchronization should be required here int counter1 = 0; int counter2 = 0; @@ -150,12 +150,12 @@ BOOST_AUTO_TEST_CASE(singlethreadedscheduler_ordered) // just simply count up on each queue - if execution is properly ordered then // the callbacks should run in exactly the order in which they were enqueued for (int i = 0; i < 100; ++i) { - queue1.AddToProcessQueue([i, &counter1]() { + queue1.insert([i, &counter1]() { bool expectation = i == counter1++; assert(expectation); }); - queue2.AddToProcessQueue([i, &counter2]() { + queue2.insert([i, &counter2]() { bool expectation = i == counter2++; assert(expectation); }); diff --git a/src/test/script_tests.cpp b/src/test/script_tests.cpp index 1f674408b2..e4142e203c 100644 --- a/src/test/script_tests.cpp +++ b/src/test/script_tests.cpp @@ -23,10 +23,6 @@ #include <util/fs.h> #include <util/strencodings.h> -#if defined(HAVE_CONSENSUS_LIB) -#include <script/bitcoinconsensus.h> -#endif - #include <cstdint> #include <fstream> #include <string> @@ -139,21 +135,6 @@ void DoTest(const CScript& scriptPubKey, const CScript& scriptSig, const CScript if (combined_flags & SCRIPT_VERIFY_WITNESS && ~combined_flags & SCRIPT_VERIFY_P2SH) continue; BOOST_CHECK_MESSAGE(VerifyScript(scriptSig, scriptPubKey, &scriptWitness, combined_flags, MutableTransactionSignatureChecker(&tx, 0, txCredit.vout[0].nValue, MissingDataBehavior::ASSERT_FAIL), &err) == expect, message + strprintf(" (with flags %x)", combined_flags)); } - -#if defined(HAVE_CONSENSUS_LIB) - DataStream stream; - stream << TX_WITH_WITNESS(tx2); - uint32_t libconsensus_flags{flags & bitcoinconsensus_SCRIPT_FLAGS_VERIFY_ALL}; - if (libconsensus_flags == flags) { - int expectedSuccessCode = expect ? 1 : 0; - if (flags & bitcoinconsensus_SCRIPT_FLAGS_VERIFY_WITNESS) { - BOOST_CHECK_MESSAGE(bitcoinconsensus_verify_script_with_amount(scriptPubKey.data(), scriptPubKey.size(), txCredit.vout[0].nValue, UCharCast(stream.data()), stream.size(), 0, libconsensus_flags, nullptr) == expectedSuccessCode, message); - } else { - BOOST_CHECK_MESSAGE(bitcoinconsensus_verify_script_with_amount(scriptPubKey.data(), scriptPubKey.size(), 0, UCharCast(stream.data()), stream.size(), 0, libconsensus_flags, nullptr) == expectedSuccessCode, message); - BOOST_CHECK_MESSAGE(bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), 0, libconsensus_flags, nullptr) == expectedSuccessCode, message); - } - } -#endif } void static NegateSignatureS(std::vector<unsigned char>& vchSig) { @@ -1273,6 +1254,30 @@ BOOST_AUTO_TEST_CASE(script_combineSigs) BOOST_CHECK(combined.scriptSig == partial3c); } +/** + * Reproduction of an exception incorrectly raised when parsing a public key inside a TapMiniscript. + */ +BOOST_AUTO_TEST_CASE(sign_invalid_miniscript) +{ + FillableSigningProvider keystore; + SignatureData sig_data; + CMutableTransaction prev, curr; + + // Create a Taproot output which contains a leaf in which a non-32 bytes push is used where a public key is expected + // by the Miniscript parser. This offending Script was found by the RPC fuzzer. + const auto invalid_pubkey{ParseHex("173d36c8c9c9c9ffffffffffff0200000000021e1e37373721361818181818181e1e1e1e19000000000000000000b19292929292926b006c9b9b9292")}; + TaprootBuilder builder; + builder.Add(0, {invalid_pubkey}, 0xc0); + XOnlyPubKey nums{ParseHex("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0")}; + builder.Finalize(nums); + prev.vout.emplace_back(0, GetScriptForDestination(builder.GetOutput())); + curr.vin.emplace_back(COutPoint{prev.GetHash(), 0}); + sig_data.tr_spenddata = builder.GetSpendData(); + + // SignSignature can fail but it shouldn't raise an exception (nor crash). + BOOST_CHECK(!SignSignature(keystore, CTransaction(prev), curr, 0, SIGHASH_ALL, sig_data)); +} + BOOST_AUTO_TEST_CASE(script_standard_push) { ScriptError err; @@ -1494,179 +1499,6 @@ static CScriptWitness ScriptWitnessFromJSON(const UniValue& univalue) return scriptwitness; } -#if defined(HAVE_CONSENSUS_LIB) - -/* Test simple (successful) usage of bitcoinconsensus_verify_script */ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_returns_true) -{ - unsigned int libconsensus_flags = 0; - int nIn = 0; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_1; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << TX_WITH_WITNESS(spendTx); - - bitcoinconsensus_error err; - int result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 1); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_OK); -} - -/* Test bitcoinconsensus_verify_script returns invalid tx index err*/ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_tx_index_err) -{ - unsigned int libconsensus_flags = 0; - int nIn = 3; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_EQUAL; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << TX_WITH_WITNESS(spendTx); - - bitcoinconsensus_error err; - int result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_TX_INDEX); -} - -/* Test bitcoinconsensus_verify_script returns tx size mismatch err*/ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_tx_size) -{ - unsigned int libconsensus_flags = 0; - int nIn = 0; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_EQUAL; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << TX_WITH_WITNESS(spendTx); - - bitcoinconsensus_error err; - int result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size() * 2, nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_TX_SIZE_MISMATCH); -} - -/* Test bitcoinconsensus_verify_script returns invalid tx serialization error */ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_tx_serialization) -{ - unsigned int libconsensus_flags = 0; - int nIn = 0; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_EQUAL; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << 0xffffffff; - - bitcoinconsensus_error err; - int result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_TX_DESERIALIZE); -} - -/* Test bitcoinconsensus_verify_script returns amount required error */ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_amount_required_err) -{ - unsigned int libconsensus_flags = bitcoinconsensus_SCRIPT_FLAGS_VERIFY_WITNESS; - int nIn = 0; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_EQUAL; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << TX_WITH_WITNESS(spendTx); - - bitcoinconsensus_error err; - int result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_AMOUNT_REQUIRED); -} - -/* Test bitcoinconsensus_verify_script returns invalid flags err */ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_invalid_flags) -{ - unsigned int libconsensus_flags = 1 << 3; - int nIn = 0; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_EQUAL; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << TX_WITH_WITNESS(spendTx); - - bitcoinconsensus_error err; - int result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_INVALID_FLAGS); -} - -/* Test bitcoinconsensus_verify_script returns spent outputs required err */ -BOOST_AUTO_TEST_CASE(bitcoinconsensus_verify_script_spent_outputs_required_err) -{ - unsigned int libconsensus_flags{bitcoinconsensus_SCRIPT_FLAGS_VERIFY_TAPROOT}; - const int nIn{0}; - - CScript scriptPubKey; - CScript scriptSig; - CScriptWitness wit; - - scriptPubKey << OP_EQUAL; - CTransaction creditTx{BuildCreditingTransaction(scriptPubKey, 1)}; - CTransaction spendTx{BuildSpendingTransaction(scriptSig, wit, creditTx)}; - - DataStream stream; - stream << TX_WITH_WITNESS(spendTx); - - bitcoinconsensus_error err; - int result{bitcoinconsensus_verify_script_with_spent_outputs(scriptPubKey.data(), scriptPubKey.size(), creditTx.vout[0].nValue, UCharCast(stream.data()), stream.size(), nullptr, 0, nIn, libconsensus_flags, &err)}; - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_SPENT_OUTPUTS_REQUIRED); - - result = bitcoinconsensus_verify_script_with_amount(scriptPubKey.data(), scriptPubKey.size(), creditTx.vout[0].nValue, UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_SPENT_OUTPUTS_REQUIRED); - - result = bitcoinconsensus_verify_script(scriptPubKey.data(), scriptPubKey.size(), UCharCast(stream.data()), stream.size(), nIn, libconsensus_flags, &err); - BOOST_CHECK_EQUAL(result, 0); - BOOST_CHECK_EQUAL(err, bitcoinconsensus_ERR_SPENT_OUTPUTS_REQUIRED); -} - -#endif // defined(HAVE_CONSENSUS_LIB) - static std::vector<unsigned int> AllConsensusFlags() { std::vector<unsigned int> ret; @@ -1714,28 +1546,12 @@ static void AssetTest(const UniValue& test) txdata.Init(tx, std::vector<CTxOut>(prevouts)); CachingTransactionSignatureChecker txcheck(&tx, idx, prevouts[idx].nValue, true, txdata); -#if defined(HAVE_CONSENSUS_LIB) - DataStream stream; - stream << TX_WITH_WITNESS(tx); - std::vector<UTXO> utxos; - utxos.resize(prevouts.size()); - for (size_t i = 0; i < prevouts.size(); i++) { - utxos[i].scriptPubKey = prevouts[i].scriptPubKey.data(); - utxos[i].scriptPubKeySize = prevouts[i].scriptPubKey.size(); - utxos[i].value = prevouts[i].nValue; - } -#endif - for (const auto flags : ALL_CONSENSUS_FLAGS) { // "final": true tests are valid for all flags. Others are only valid with flags that are // a subset of test_flags. if (fin || ((flags & test_flags) == flags)) { bool ret = VerifyScript(tx.vin[idx].scriptSig, prevouts[idx].scriptPubKey, &tx.vin[idx].scriptWitness, flags, txcheck, nullptr); BOOST_CHECK(ret); -#if defined(HAVE_CONSENSUS_LIB) - int lib_ret = bitcoinconsensus_verify_script_with_spent_outputs(prevouts[idx].scriptPubKey.data(), prevouts[idx].scriptPubKey.size(), prevouts[idx].nValue, UCharCast(stream.data()), stream.size(), utxos.data(), utxos.size(), idx, flags, nullptr); - BOOST_CHECK(lib_ret == 1); -#endif } } } @@ -1748,27 +1564,11 @@ static void AssetTest(const UniValue& test) txdata.Init(tx, std::vector<CTxOut>(prevouts)); CachingTransactionSignatureChecker txcheck(&tx, idx, prevouts[idx].nValue, true, txdata); -#if defined(HAVE_CONSENSUS_LIB) - DataStream stream; - stream << TX_WITH_WITNESS(tx); - std::vector<UTXO> utxos; - utxos.resize(prevouts.size()); - for (size_t i = 0; i < prevouts.size(); i++) { - utxos[i].scriptPubKey = prevouts[i].scriptPubKey.data(); - utxos[i].scriptPubKeySize = prevouts[i].scriptPubKey.size(); - utxos[i].value = prevouts[i].nValue; - } -#endif - for (const auto flags : ALL_CONSENSUS_FLAGS) { // If a test is supposed to fail with test_flags, it should also fail with any superset thereof. if ((flags & test_flags) == test_flags) { bool ret = VerifyScript(tx.vin[idx].scriptSig, prevouts[idx].scriptPubKey, &tx.vin[idx].scriptWitness, flags, txcheck, nullptr); BOOST_CHECK(!ret); -#if defined(HAVE_CONSENSUS_LIB) - int lib_ret = bitcoinconsensus_verify_script_with_spent_outputs(prevouts[idx].scriptPubKey.data(), prevouts[idx].scriptPubKey.size(), prevouts[idx].nValue, UCharCast(stream.data()), stream.size(), utxos.data(), utxos.size(), idx, flags, nullptr); - BOOST_CHECK(lib_ret == 0); -#endif } } } diff --git a/src/test/serfloat_tests.cpp b/src/test/serfloat_tests.cpp index b36bdc02ca..304541074f 100644 --- a/src/test/serfloat_tests.cpp +++ b/src/test/serfloat_tests.cpp @@ -37,6 +37,7 @@ uint64_t TestDouble(double f) { } // namespace BOOST_AUTO_TEST_CASE(double_serfloat_tests) { + // Test specific values against their expected encoding. BOOST_CHECK_EQUAL(TestDouble(0.0), 0U); BOOST_CHECK_EQUAL(TestDouble(-0.0), 0x8000000000000000); BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::infinity()), 0x7ff0000000000000U); @@ -46,55 +47,76 @@ BOOST_AUTO_TEST_CASE(double_serfloat_tests) { BOOST_CHECK_EQUAL(TestDouble(2.0), 0x4000000000000000ULL); BOOST_CHECK_EQUAL(TestDouble(4.0), 0x4010000000000000ULL); BOOST_CHECK_EQUAL(TestDouble(785.066650390625), 0x4088888880000000ULL); + BOOST_CHECK_EQUAL(TestDouble(3.7243058682384174), 0x400dcb60e0031440); + BOOST_CHECK_EQUAL(TestDouble(91.64070592566159), 0x4056e901536d447a); + BOOST_CHECK_EQUAL(TestDouble(-98.63087668642575), 0xc058a860489c007a); + BOOST_CHECK_EQUAL(TestDouble(4.908737756962054), 0x4013a28c268b2b70); + BOOST_CHECK_EQUAL(TestDouble(77.9247330021754), 0x40537b2ed3547804); + BOOST_CHECK_EQUAL(TestDouble(40.24732825357566), 0x40441fa873c43dfc); + BOOST_CHECK_EQUAL(TestDouble(71.39395607929222), 0x4051d936938f27b6); + BOOST_CHECK_EQUAL(TestDouble(58.80100710817612), 0x404d668766a2bd70); + BOOST_CHECK_EQUAL(TestDouble(-30.10665786964975), 0xc03e1b4dee1e01b8); + BOOST_CHECK_EQUAL(TestDouble(60.15231509068704), 0x404e137f0f969814); + BOOST_CHECK_EQUAL(TestDouble(-48.15848711335961), 0xc04814494e445bc6); + BOOST_CHECK_EQUAL(TestDouble(26.68450101125353), 0x403aaf3b755169b0); + BOOST_CHECK_EQUAL(TestDouble(-65.72071986604303), 0xc0506e2046378ede); + BOOST_CHECK_EQUAL(TestDouble(17.95575825512381), 0x4031f4ac92b0a388); + BOOST_CHECK_EQUAL(TestDouble(-35.27171863226279), 0xc041a2c7ad17a42a); + BOOST_CHECK_EQUAL(TestDouble(-8.58810329425124), 0xc0212d1bdffef538); + BOOST_CHECK_EQUAL(TestDouble(88.51393044338977), 0x405620e43c83b1c8); + BOOST_CHECK_EQUAL(TestDouble(48.07224932612732), 0x4048093f77466ffc); + BOOST_CHECK_EQUAL(TestDouble(9.867348871395659e+117), 0x586f4daeb2459b9f); + BOOST_CHECK_EQUAL(TestDouble(-1.5166424385129721e+206), 0xeabe3bbc484bd458); + BOOST_CHECK_EQUAL(TestDouble(-8.585156555624594e-275), 0x8707c76eee012429); + BOOST_CHECK_EQUAL(TestDouble(2.2794371091628822e+113), 0x5777b2184458f4ee); + BOOST_CHECK_EQUAL(TestDouble(-1.1290476594131867e+163), 0xe1c91893d3488bb0); + BOOST_CHECK_EQUAL(TestDouble(9.143848423979275e-246), 0x0d0ff76e5f2620a3); + BOOST_CHECK_EQUAL(TestDouble(-2.8366718125941117e+81), 0xd0d7ec7e754b394a); + BOOST_CHECK_EQUAL(TestDouble(-1.2754409481684012e+229), 0xef80d32f8ec55342); + BOOST_CHECK_EQUAL(TestDouble(6.000577060053642e-186), 0x197a1be7c8209b6a); + BOOST_CHECK_EQUAL(TestDouble(2.0839423284378986e-302), 0x014c94f8689cb0a5); + BOOST_CHECK_EQUAL(TestDouble(-1.422140051483753e+259), 0xf5bd99271d04bb35); + BOOST_CHECK_EQUAL(TestDouble(-1.0593973991188853e+46), 0xc97db0cdb72d1046); + BOOST_CHECK_EQUAL(TestDouble(2.62945125875249e+190), 0x67779b36366c993b); + BOOST_CHECK_EQUAL(TestDouble(-2.920377657275094e+115), 0xd7e7b7b45908e23b); + BOOST_CHECK_EQUAL(TestDouble(9.790289014855851e-118), 0x27a3c031cc428bcc); + BOOST_CHECK_EQUAL(TestDouble(-4.629317182034961e-114), 0xa866ccf0b753705a); + BOOST_CHECK_EQUAL(TestDouble(-1.7674605603846528e+279), 0xf9e8ed383ffc3e25); + BOOST_CHECK_EQUAL(TestDouble(2.5308171727712605e+120), 0x58ef5cd55f0ec997); + BOOST_CHECK_EQUAL(TestDouble(-1.05034156412799e+54), 0xcb25eea1b9350fa0); - // Roundtrip test on IEC559-compatible systems - if (std::numeric_limits<double>::is_iec559) { - BOOST_CHECK_EQUAL(sizeof(double), 8U); - BOOST_CHECK_EQUAL(sizeof(uint64_t), 8U); - // Test extreme values - TestDouble(std::numeric_limits<double>::min()); - TestDouble(-std::numeric_limits<double>::min()); - TestDouble(std::numeric_limits<double>::max()); - TestDouble(-std::numeric_limits<double>::max()); - TestDouble(std::numeric_limits<double>::lowest()); - TestDouble(-std::numeric_limits<double>::lowest()); - TestDouble(std::numeric_limits<double>::quiet_NaN()); - TestDouble(-std::numeric_limits<double>::quiet_NaN()); - TestDouble(std::numeric_limits<double>::signaling_NaN()); - TestDouble(-std::numeric_limits<double>::signaling_NaN()); - TestDouble(std::numeric_limits<double>::denorm_min()); - TestDouble(-std::numeric_limits<double>::denorm_min()); - // Test exact encoding: on currently supported platforms, EncodeDouble - // should produce exactly the same as the in-memory representation for non-NaN. - for (int j = 0; j < 1000; ++j) { - // Iterate over 9 specific bits exhaustively; the others are chosen randomly. - // These specific bits are the sign bit, and the 2 top and bottom bits of - // exponent and mantissa in the IEEE754 binary64 format. - for (int x = 0; x < 512; ++x) { - uint64_t v = InsecureRandBits(64); - v &= ~(uint64_t{1} << 0); - if (x & 1) v |= (uint64_t{1} << 0); - v &= ~(uint64_t{1} << 1); - if (x & 2) v |= (uint64_t{1} << 1); - v &= ~(uint64_t{1} << 50); - if (x & 4) v |= (uint64_t{1} << 50); - v &= ~(uint64_t{1} << 51); - if (x & 8) v |= (uint64_t{1} << 51); - v &= ~(uint64_t{1} << 52); - if (x & 16) v |= (uint64_t{1} << 52); - v &= ~(uint64_t{1} << 53); - if (x & 32) v |= (uint64_t{1} << 53); - v &= ~(uint64_t{1} << 61); - if (x & 64) v |= (uint64_t{1} << 61); - v &= ~(uint64_t{1} << 62); - if (x & 128) v |= (uint64_t{1} << 62); - v &= ~(uint64_t{1} << 63); - if (x & 256) v |= (uint64_t{1} << 63); - double f; - memcpy(&f, &v, 8); - uint64_t v2 = TestDouble(f); - if (!std::isnan(f)) BOOST_CHECK_EQUAL(v, v2); + // Test extreme values + BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::min()), 0x10000000000000); + BOOST_CHECK_EQUAL(TestDouble(-std::numeric_limits<double>::min()), 0x8010000000000000); + BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::max()), 0x7fefffffffffffff); + BOOST_CHECK_EQUAL(TestDouble(-std::numeric_limits<double>::max()), 0xffefffffffffffff); + BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::lowest()), 0xffefffffffffffff); + BOOST_CHECK_EQUAL(TestDouble(-std::numeric_limits<double>::lowest()), 0x7fefffffffffffff); + BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::denorm_min()), 0x1); + BOOST_CHECK_EQUAL(TestDouble(-std::numeric_limits<double>::denorm_min()), 0x8000000000000001); + // Note that all NaNs are encoded the same way. + BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::quiet_NaN()), 0x7ff8000000000000); + BOOST_CHECK_EQUAL(TestDouble(-std::numeric_limits<double>::quiet_NaN()), 0x7ff8000000000000); + BOOST_CHECK_EQUAL(TestDouble(std::numeric_limits<double>::signaling_NaN()), 0x7ff8000000000000); + BOOST_CHECK_EQUAL(TestDouble(-std::numeric_limits<double>::signaling_NaN()), 0x7ff8000000000000); + + // Construct doubles to test from the encoding. + static_assert(sizeof(double) == 8); + static_assert(sizeof(uint64_t) == 8); + for (int j = 0; j < 1000; ++j) { + // Iterate over 9 specific bits exhaustively; the others are chosen randomly. + // These specific bits are the sign bit, and the 2 top and bottom bits of + // exponent and mantissa in the IEEE754 binary64 format. + for (int x = 0; x < 512; ++x) { + uint64_t v = InsecureRandBits(64); + int x_pos = 0; + for (int v_pos : {0, 1, 50, 51, 52, 53, 61, 62, 63}) { + v &= ~(uint64_t{1} << v_pos); + if ((x >> (x_pos++)) & 1) v |= (uint64_t{1} << v_pos); } + double f; + memcpy(&f, &v, 8); + TestDouble(f); } } } diff --git a/src/test/settings_tests.cpp b/src/test/settings_tests.cpp index fa8ceb8dd6..41190b3579 100644 --- a/src/test/settings_tests.cpp +++ b/src/test/settings_tests.cpp @@ -99,7 +99,9 @@ BOOST_AUTO_TEST_CASE(ReadWrite) // Check invalid json not allowed WriteText(path, R"(invalid json)"); BOOST_CHECK(!common::ReadSettings(path, values, errors)); - std::vector<std::string> fail_parse = {strprintf("Unable to parse settings file %s", fs::PathToString(path))}; + std::vector<std::string> fail_parse = {strprintf("Settings file %s does not contain valid JSON. This is probably caused by disk corruption or a crash, " + "and can be fixed by removing the file, which will reset settings to default values.", + fs::PathToString(path))}; BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end()); } diff --git a/src/test/span_tests.cpp b/src/test/span_tests.cpp index f6cac10b09..aae61990f7 100644 --- a/src/test/span_tests.cpp +++ b/src/test/span_tests.cpp @@ -53,7 +53,7 @@ BOOST_AUTO_TEST_SUITE(span_tests) // aren't compatible with Spans at compile time. // // Previously there was a bug where writing a SFINAE check for vector<bool> was -// not possible, because in libstdc++ vector<bool> has a data() memeber +// not possible, because in libstdc++ vector<bool> has a data() member // returning void*, and the Span template guide ignored the data() return value, // so the template substitution would succeed, but the constructor would fail, // resulting in a fatal compile error, rather than a SFINAE error that could be diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp index 7d1ac5a19a..e666e11758 100644 --- a/src/test/streams_tests.cpp +++ b/src/test/streams_tests.cpp @@ -29,7 +29,14 @@ BOOST_AUTO_TEST_CASE(xor_file) BOOST_CHECK_EXCEPTION(xor_file.ignore(1), std::ios_base::failure, HasReason{"AutoFile::ignore: file handle is nullpt"}); } { - AutoFile xor_file{raw_file("wbx"), xor_pat}; +#ifdef __MINGW64__ + // Our usage of mingw-w64 and the msvcrt runtime does not support + // the x modifier for the _wfopen(). + const char* mode = "wb"; +#else + const char* mode = "wbx"; +#endif + AutoFile xor_file{raw_file(mode), xor_pat}; xor_file << test1 << test2; } { @@ -136,8 +143,8 @@ BOOST_AUTO_TEST_CASE(streams_vector_reader) BOOST_CHECK_EQUAL(reader.size(), 5U); BOOST_CHECK(!reader.empty()); - // Read a single byte as a signed char. - signed char b; + // Read a single byte as a int8_t. + int8_t b; reader >> b; BOOST_CHECK_EQUAL(b, -1); BOOST_CHECK_EQUAL(reader.size(), 4U); diff --git a/src/test/system_tests.cpp b/src/test/system_tests.cpp index 6a96b60db0..baa759e42c 100644 --- a/src/test/system_tests.cpp +++ b/src/test/system_tests.cpp @@ -2,12 +2,14 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. // + +#include <config/bitcoin-config.h> // IWYU pragma: keep #include <test/util/setup_common.h> #include <common/run_command.h> #include <univalue.h> #ifdef ENABLE_EXTERNAL_SIGNER -#include <boost/process.hpp> +#include <util/subprocess.h> #endif // ENABLE_EXTERNAL_SIGNER #include <boost/test/unit_test.hpp> @@ -25,48 +27,25 @@ BOOST_AUTO_TEST_CASE(dummy) BOOST_AUTO_TEST_CASE(run_command) { -#ifdef WIN32 - // https://www.winehq.org/pipermail/wine-devel/2008-September/069387.html - auto hntdll = GetModuleHandleA("ntdll.dll"); - assert(hntdll); - const bool wine_runtime = GetProcAddress(hntdll, "wine_get_version"); -#endif - { const UniValue result = RunCommandParseJSON(""); BOOST_CHECK(result.isNull()); } { -#ifdef WIN32 - const UniValue result = RunCommandParseJSON("cmd.exe /c echo {\"success\": true}"); -#else - const UniValue result = RunCommandParseJSON("echo \"{\"success\": true}\""); -#endif + const UniValue result = RunCommandParseJSON("echo {\"success\": true}"); BOOST_CHECK(result.isObject()); const UniValue& success = result.find_value("success"); BOOST_CHECK(!success.isNull()); BOOST_CHECK_EQUAL(success.get_bool(), true); } { - // An invalid command is handled by Boost -#ifdef WIN32 - const int expected_error{wine_runtime ? 6 : 2}; -#else - const int expected_error{2}; -#endif - BOOST_CHECK_EXCEPTION(RunCommandParseJSON("invalid_command"), boost::process::process_error, [&](const boost::process::process_error& e) { - BOOST_CHECK(std::string(e.what()).find("RunCommandParseJSON error:") == std::string::npos); - BOOST_CHECK_EQUAL(e.code().value(), expected_error); - return true; - }); + // An invalid command is handled by cpp-subprocess + const std::string expected{"execve failed: "}; + BOOST_CHECK_EXCEPTION(RunCommandParseJSON("invalid_command"), subprocess::CalledProcessError, HasReason(expected)); } { // Return non-zero exit code, no output to stderr -#ifdef WIN32 - const std::string command{"cmd.exe /c exit 1"}; -#else const std::string command{"false"}; -#endif BOOST_CHECK_EXCEPTION(RunCommandParseJSON(command), std::runtime_error, [&](const std::runtime_error& e) { const std::string what{e.what()}; BOOST_CHECK(what.find(strprintf("RunCommandParseJSON error: process(%s) returned 1: \n", command)) != std::string::npos); @@ -75,13 +54,8 @@ BOOST_AUTO_TEST_CASE(run_command) } { // Return non-zero exit code, with error message for stderr -#ifdef WIN32 - const std::string command{"cmd.exe /c dir nosuchfile"}; - const std::string expected{wine_runtime ? "File not found." : "File Not Found"}; -#else const std::string command{"ls nosuchfile"}; const std::string expected{"No such file or directory"}; -#endif BOOST_CHECK_EXCEPTION(RunCommandParseJSON(command), std::runtime_error, [&](const std::runtime_error& e) { const std::string what(e.what()); BOOST_CHECK(what.find(strprintf("RunCommandParseJSON error: process(%s) returned", command)) != std::string::npos); @@ -91,15 +65,10 @@ BOOST_AUTO_TEST_CASE(run_command) } { // Unable to parse JSON -#ifdef WIN32 - const std::string command{"cmd.exe /c echo {"}; -#else const std::string command{"echo {"}; -#endif BOOST_CHECK_EXCEPTION(RunCommandParseJSON(command), std::runtime_error, HasReason("Unable to parse JSON: {")); } - // Test std::in, except for Windows -#ifndef WIN32 + // Test std::in { const UniValue result = RunCommandParseJSON("cat", "{\"success\": true}"); BOOST_CHECK(result.isObject()); @@ -107,7 +76,6 @@ BOOST_AUTO_TEST_CASE(run_command) BOOST_CHECK(!success.isNull()); BOOST_CHECK_EQUAL(success.get_bool(), true); } -#endif } #endif // ENABLE_EXTERNAL_SIGNER diff --git a/src/test/timedata_tests.cpp b/src/test/timedata_tests.cpp deleted file mode 100644 index 2814dbf4c0..0000000000 --- a/src/test/timedata_tests.cpp +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2011-2021 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 <netaddress.h> -#include <noui.h> -#include <test/util/logging.h> -#include <test/util/setup_common.h> -#include <timedata.h> -#include <util/string.h> -#include <util/translation.h> -#include <warnings.h> - -#include <string> - -#include <boost/test/unit_test.hpp> - -BOOST_FIXTURE_TEST_SUITE(timedata_tests, BasicTestingSetup) - -BOOST_AUTO_TEST_CASE(util_MedianFilter) -{ - CMedianFilter<int> filter(5, 15); - - BOOST_CHECK_EQUAL(filter.median(), 15); - - filter.input(20); // [15 20] - BOOST_CHECK_EQUAL(filter.median(), 17); - - filter.input(30); // [15 20 30] - BOOST_CHECK_EQUAL(filter.median(), 20); - - filter.input(3); // [3 15 20 30] - BOOST_CHECK_EQUAL(filter.median(), 17); - - filter.input(7); // [3 7 15 20 30] - BOOST_CHECK_EQUAL(filter.median(), 15); - - filter.input(18); // [3 7 18 20 30] - BOOST_CHECK_EQUAL(filter.median(), 18); - - filter.input(0); // [0 3 7 18 30] - BOOST_CHECK_EQUAL(filter.median(), 7); -} - -static void MultiAddTimeData(int n, int64_t offset) -{ - static int cnt = 0; - for (int i = 0; i < n; ++i) { - CNetAddr addr; - addr.SetInternal(ToString(++cnt)); - AddTimeData(addr, offset); - } -} - - -BOOST_AUTO_TEST_CASE(addtimedata) -{ - BOOST_CHECK_EQUAL(GetTimeOffset(), 0); - - //Part 1: Add large offsets to test a warning message that our clock may be wrong. - MultiAddTimeData(3, DEFAULT_MAX_TIME_ADJUSTMENT + 1); - // Filter size is 1 + 3 = 4: It is always initialized with a single element (offset 0) - - { - ASSERT_DEBUG_LOG("Please check that your computer's date and time are correct!"); - MultiAddTimeData(1, DEFAULT_MAX_TIME_ADJUSTMENT + 1); //filter size 5 - } - - BOOST_CHECK(GetWarnings(true).original.find("clock is wrong") != std::string::npos); - - // nTimeOffset is not changed if the median of offsets exceeds DEFAULT_MAX_TIME_ADJUSTMENT - BOOST_CHECK_EQUAL(GetTimeOffset(), 0); - - // Part 2: Test positive and negative medians by adding more offsets - MultiAddTimeData(4, 100); // filter size 9 - BOOST_CHECK_EQUAL(GetTimeOffset(), 100); - MultiAddTimeData(10, -100); //filter size 19 - BOOST_CHECK_EQUAL(GetTimeOffset(), -100); - - // Part 3: Test behaviour when filter has reached maximum number of offsets - const int MAX_SAMPLES = 200; - int nfill = (MAX_SAMPLES - 3 - 19) / 2; //89 - MultiAddTimeData(nfill, 100); - MultiAddTimeData(nfill, -100); //filter size MAX_SAMPLES - 3 - BOOST_CHECK_EQUAL(GetTimeOffset(), -100); - - MultiAddTimeData(2, 100); - //filter size MAX_SAMPLES -1, median is the initial 0 offset - //since we added same number of positive/negative offsets - - BOOST_CHECK_EQUAL(GetTimeOffset(), 0); - - // After the number of offsets has reached MAX_SAMPLES -1 (=199), nTimeOffset will never change - // because it is only updated when the number of elements in the filter becomes odd. It was decided - // not to fix this because it prevents possible attacks. See the comment in AddTimeData() or issue #4521 - // for a more detailed explanation. - MultiAddTimeData(2, 100); // filter median is 100 now, but nTimeOffset will not change - // We want this test to end with nTimeOffset==0, otherwise subsequent tests of the suite will fail. - BOOST_CHECK_EQUAL(GetTimeOffset(), 0); - - TestOnlyResetTimeData(); -} - -BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/timeoffsets_tests.cpp b/src/test/timeoffsets_tests.cpp new file mode 100644 index 0000000000..008f1a9db9 --- /dev/null +++ b/src/test/timeoffsets_tests.cpp @@ -0,0 +1,69 @@ +// Copyright (c) 2024-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 <node/timeoffsets.h> +#include <test/util/setup_common.h> + +#include <boost/test/unit_test.hpp> + +#include <chrono> +#include <vector> + +using namespace std::chrono_literals; + +static void AddMulti(TimeOffsets& offsets, const std::vector<std::chrono::seconds>& to_add) +{ + for (auto offset : to_add) { + offsets.Add(offset); + } +} + +BOOST_FIXTURE_TEST_SUITE(timeoffsets_tests, BasicTestingSetup) + +BOOST_AUTO_TEST_CASE(timeoffsets) +{ + TimeOffsets offsets{}; + BOOST_CHECK(offsets.Median() == 0s); + + AddMulti(offsets, {{0s, -1s, -2s, -3s}}); + // median should be zero for < 5 offsets + BOOST_CHECK(offsets.Median() == 0s); + + offsets.Add(-4s); + // we now have 5 offsets: [-4, -3, -2, -1, 0] + BOOST_CHECK(offsets.Median() == -2s); + + AddMulti(offsets, {4, 5s}); + // we now have 9 offsets: [-4, -3, -2, -1, 0, 5, 5, 5, 5] + BOOST_CHECK(offsets.Median() == 0s); + + AddMulti(offsets, {41, 10s}); + // the TimeOffsets is now at capacity with 50 offsets, oldest offsets is discarded for any additional offset + BOOST_CHECK(offsets.Median() == 10s); + + AddMulti(offsets, {25, 15s}); + // we now have 25 offsets of 10s followed by 25 offsets of 15s + BOOST_CHECK(offsets.Median() == 15s); +} + +static bool IsWarningRaised(const std::vector<std::chrono::seconds>& check_offsets) +{ + TimeOffsets offsets{}; + AddMulti(offsets, check_offsets); + return offsets.WarnIfOutOfSync(); +} + + +BOOST_AUTO_TEST_CASE(timeoffsets_warning) +{ + BOOST_CHECK(IsWarningRaised({{-60min, -40min, -30min, 0min, 10min}})); + BOOST_CHECK(IsWarningRaised({5, 11min})); + + BOOST_CHECK(!IsWarningRaised({4, 60min})); + BOOST_CHECK(!IsWarningRaised({100, 3min})); +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/transaction_tests.cpp b/src/test/transaction_tests.cpp index d1cb2531aa..e6cf64611e 100644 --- a/src/test/transaction_tests.cpp +++ b/src/test/transaction_tests.cpp @@ -786,7 +786,7 @@ BOOST_AUTO_TEST_CASE(test_IsStandard) t.nVersion = 0; CheckIsNotStandard(t, "version"); - t.nVersion = 3; + t.nVersion = TX_MAX_STANDARD_VERSION + 1; CheckIsNotStandard(t, "version"); // Allowed nVersion diff --git a/src/test/txindex_tests.cpp b/src/test/txindex_tests.cpp index e2432a4718..5a32b02ad9 100644 --- a/src/test/txindex_tests.cpp +++ b/src/test/txindex_tests.cpp @@ -71,7 +71,7 @@ BOOST_FIXTURE_TEST_CASE(txindex_initial_sync, TestChain100Setup) // SyncWithValidationInterfaceQueue() call below is also needed to ensure // TSAN always sees the test thread waiting for the notification thread, and // avoid potential false positive reports. - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); // shutdown sequence (c.f. Shutdown() in init.cpp) txindex.Stop(); diff --git a/src/test/txpackage_tests.cpp b/src/test/txpackage_tests.cpp index f6456526bb..55e0c5f285 100644 --- a/src/test/txpackage_tests.cpp +++ b/src/test/txpackage_tests.cpp @@ -8,9 +8,12 @@ #include <policy/policy.h> #include <primitives/transaction.h> #include <script/script.h> +#include <serialize.h> +#include <streams.h> #include <test/util/random.h> #include <test/util/script.h> #include <test/util/setup_common.h> +#include <util/strencodings.h> #include <test/util/txmempool.h> #include <validation.h> @@ -40,6 +43,93 @@ inline CTransactionRef create_placeholder_tx(size_t num_inputs, size_t num_outpu return MakeTransactionRef(mtx); } +// Create a Wtxid from a hex string +inline Wtxid WtxidFromString(std::string_view str) +{ + return Wtxid::FromUint256(uint256S(str.data())); +} + +BOOST_FIXTURE_TEST_CASE(package_hash_tests, TestChain100Setup) +{ + // Random real segwit transaction + DataStream stream_1{ + ParseHex("02000000000101964b8aa63509579ca6086e6012eeaa4c2f4dd1e283da29b67c8eea38b3c6fd220000000000fdffffff0294c618000000000017a9145afbbb42f4e83312666d0697f9e66259912ecde38768fa2c0000000000160014897388a0889390fd0e153a22bb2cf9d8f019faf50247304402200547406380719f84d68cf4e96cc3e4a1688309ef475b150be2b471c70ea562aa02206d255f5acc40fd95981874d77201d2eb07883657ce1c796513f32b6079545cdf0121023ae77335cefcb5ab4c1dc1fb0d2acfece184e593727d7d5906c78e564c7c11d125cf0c00"), + }; + CTransaction tx_1(deserialize, TX_WITH_WITNESS, stream_1); + CTransactionRef ptx_1{MakeTransactionRef(tx_1)}; + + // Random real nonsegwit transaction + DataStream stream_2{ + ParseHex("01000000010b26e9b7735eb6aabdf358bab62f9816a21ba9ebdb719d5299e88607d722c190000000008b4830450220070aca44506c5cef3a16ed519d7c3c39f8aab192c4e1c90d065f37b8a4af6141022100a8e160b856c2d43d27d8fba71e5aef6405b8643ac4cb7cb3c462aced7f14711a0141046d11fee51b0e60666d5049a9101a72741df480b96ee26488a4d3466b95c9a40ac5eeef87e10a5cd336c19a84565f80fa6c547957b7700ff4dfbdefe76036c339ffffffff021bff3d11000000001976a91404943fdd508053c75000106d3bc6e2754dbcff1988ac2f15de00000000001976a914a266436d2965547608b9e15d9032a7b9d64fa43188ac00000000"), + }; + CTransaction tx_2(deserialize, TX_WITH_WITNESS, stream_2); + CTransactionRef ptx_2{MakeTransactionRef(tx_2)}; + + // Random real segwit transaction + DataStream stream_3{ + ParseHex("0200000000010177862801f77c2c068a70372b4c435ef8dd621291c36a64eb4dd491f02218f5324600000000fdffffff014a0100000000000022512035ea312034cfac01e956a269f3bf147f569c2fbb00180677421262da042290d803402be713325ff285e66b0380f53f2fae0d0fb4e16f378a440fed51ce835061437566729d4883bc917632f3cff474d6384bc8b989961a1d730d4a87ed38ad28bd337b20f1d658c6c138b1c312e072b4446f50f01ae0da03a42e6274f8788aae53416a7fac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800357b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a224342414c222c22616d74223a2236393639227d6821c1f1d658c6c138b1c312e072b4446f50f01ae0da03a42e6274f8788aae53416a7f00000000"), + }; + CTransaction tx_3(deserialize, TX_WITH_WITNESS, stream_3); + CTransactionRef ptx_3{MakeTransactionRef(tx_3)}; + + // It's easy to see that wtxids are sorted in lexicographical order: + Wtxid wtxid_1{WtxidFromString("0x85cd1a31eb38f74ed5742ec9cb546712ab5aaf747de28a9168b53e846cbda17f")}; + Wtxid wtxid_2{WtxidFromString("0xb4749f017444b051c44dfd2720e88f314ff94f3dd6d56d40ef65854fcd7fff6b")}; + Wtxid wtxid_3{WtxidFromString("0xe065bac15f62bb4e761d761db928ddee65a47296b2b776785abb912cdec474e3")}; + BOOST_CHECK_EQUAL(tx_1.GetWitnessHash(), wtxid_1); + BOOST_CHECK_EQUAL(tx_2.GetWitnessHash(), wtxid_2); + BOOST_CHECK_EQUAL(tx_3.GetWitnessHash(), wtxid_3); + + BOOST_CHECK(wtxid_1.GetHex() < wtxid_2.GetHex()); + BOOST_CHECK(wtxid_2.GetHex() < wtxid_3.GetHex()); + + // The txids are not (we want to test that sorting and hashing use wtxid, not txid): + Txid txid_1{TxidFromString("0xbd0f71c1d5e50589063e134fad22053cdae5ab2320db5bf5e540198b0b5a4e69")}; + Txid txid_2{TxidFromString("0xb4749f017444b051c44dfd2720e88f314ff94f3dd6d56d40ef65854fcd7fff6b")}; + Txid txid_3{TxidFromString("0xee707be5201160e32c4fc715bec227d1aeea5940fb4295605e7373edce3b1a93")}; + BOOST_CHECK_EQUAL(tx_1.GetHash(), txid_1); + BOOST_CHECK_EQUAL(tx_2.GetHash(), txid_2); + BOOST_CHECK_EQUAL(tx_3.GetHash(), txid_3); + + BOOST_CHECK(txid_2.GetHex() < txid_1.GetHex()); + + BOOST_CHECK(txid_1.ToUint256() != wtxid_1.ToUint256()); + BOOST_CHECK(txid_2.ToUint256() == wtxid_2.ToUint256()); + BOOST_CHECK(txid_3.ToUint256() != wtxid_3.ToUint256()); + + // We are testing that both functions compare using GetHex() and not uint256. + // (in this pair of wtxids, hex string order != uint256 order) + BOOST_CHECK(wtxid_2 < wtxid_1); + // (in this pair of wtxids, hex string order == uint256 order) + BOOST_CHECK(wtxid_2 < wtxid_3); + + // All permutations of the package containing ptx_1, ptx_2, ptx_3 have the same package hash + std::vector<CTransactionRef> package_123{ptx_1, ptx_2, ptx_3}; + std::vector<CTransactionRef> package_132{ptx_1, ptx_3, ptx_2}; + std::vector<CTransactionRef> package_231{ptx_2, ptx_3, ptx_1}; + std::vector<CTransactionRef> package_213{ptx_2, ptx_1, ptx_3}; + std::vector<CTransactionRef> package_312{ptx_3, ptx_1, ptx_2}; + std::vector<CTransactionRef> package_321{ptx_3, ptx_2, ptx_1}; + + uint256 calculated_hash_123 = (HashWriter() << wtxid_1 << wtxid_2 << wtxid_3).GetSHA256(); + + uint256 hash_if_by_txid = (HashWriter() << wtxid_2 << wtxid_1 << wtxid_3).GetSHA256(); + BOOST_CHECK(hash_if_by_txid != calculated_hash_123); + + uint256 hash_if_use_txid = (HashWriter() << txid_2 << txid_1 << txid_3).GetSHA256(); + BOOST_CHECK(hash_if_use_txid != calculated_hash_123); + + uint256 hash_if_use_int_order = (HashWriter() << wtxid_2 << wtxid_1 << wtxid_3).GetSHA256(); + BOOST_CHECK(hash_if_use_int_order != calculated_hash_123); + + BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_123)); + BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_132)); + BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_231)); + BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_213)); + BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_312)); + BOOST_CHECK_EQUAL(calculated_hash_123, GetPackageHash(package_321)); +} + BOOST_FIXTURE_TEST_CASE(package_sanitization_tests, TestChain100Setup) { // Packages can't have more than 25 transactions. @@ -132,7 +222,7 @@ BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup) /*output_amount=*/CAmount(48 * COIN), /*submit=*/false); CTransactionRef tx_child = MakeTransactionRef(mtx_child); Package package_parent_child{tx_parent, tx_child}; - const auto result_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_parent_child, /*test_accept=*/true); + const auto result_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_parent_child, /*test_accept=*/true, /*client_maxfeerate=*/{}); if (auto err_parent_child{CheckPackageMempoolAcceptResult(package_parent_child, result_parent_child, /*expect_valid=*/true, nullptr)}) { BOOST_ERROR(err_parent_child.value()); } else { @@ -151,7 +241,7 @@ BOOST_FIXTURE_TEST_CASE(package_validation_tests, TestChain100Setup) CTransactionRef giant_ptx = create_placeholder_tx(999, 999); BOOST_CHECK(GetVirtualTransactionSize(*giant_ptx) > DEFAULT_ANCESTOR_SIZE_LIMIT_KVB * 1000); Package package_single_giant{giant_ptx}; - auto result_single_large = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_single_giant, /*test_accept=*/true); + auto result_single_large = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_single_giant, /*test_accept=*/true, /*client_maxfeerate=*/{}); if (auto err_single_large{CheckPackageMempoolAcceptResult(package_single_giant, result_single_large, /*expect_valid=*/false, nullptr)}) { BOOST_ERROR(err_single_large.value()); } else { @@ -190,6 +280,9 @@ BOOST_FIXTURE_TEST_CASE(noncontextual_package_tests, TestChain100Setup) BOOST_CHECK_EQUAL(state.GetRejectReason(), "package-not-sorted"); BOOST_CHECK(IsChildWithParents({tx_parent, tx_child})); BOOST_CHECK(IsChildWithParentsTree({tx_parent, tx_child})); + BOOST_CHECK(GetPackageHash({tx_parent}) != GetPackageHash({tx_child})); + BOOST_CHECK(GetPackageHash({tx_child, tx_child}) != GetPackageHash({tx_child})); + BOOST_CHECK(GetPackageHash({tx_child, tx_parent}) != GetPackageHash({tx_child, tx_child})); } // 24 Parents and 1 Child @@ -275,7 +368,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup) package_unrelated.emplace_back(MakeTransactionRef(mtx)); } auto result_unrelated_submit = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_unrelated, /*test_accept=*/false); + package_unrelated, /*test_accept=*/false, /*client_maxfeerate=*/{}); // We don't expect m_tx_results for each transaction when basic sanity checks haven't passed. BOOST_CHECK(result_unrelated_submit.m_state.IsInvalid()); BOOST_CHECK_EQUAL(result_unrelated_submit.m_state.GetResult(), PackageValidationResult::PCKG_POLICY); @@ -315,7 +408,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup) // 3 Generations is not allowed. { auto result_3gen_submit = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_3gen, /*test_accept=*/false); + package_3gen, /*test_accept=*/false, /*client_maxfeerate=*/{}); BOOST_CHECK(result_3gen_submit.m_state.IsInvalid()); BOOST_CHECK_EQUAL(result_3gen_submit.m_state.GetResult(), PackageValidationResult::PCKG_POLICY); BOOST_CHECK_EQUAL(result_3gen_submit.m_state.GetRejectReason(), "package-not-child-with-parents"); @@ -332,7 +425,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup) CTransactionRef tx_parent_invalid = MakeTransactionRef(mtx_parent_invalid); Package package_invalid_parent{tx_parent_invalid, tx_child}; auto result_quit_early = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_invalid_parent, /*test_accept=*/ false); + package_invalid_parent, /*test_accept=*/ false, /*client_maxfeerate=*/{}); if (auto err_parent_invalid{CheckPackageMempoolAcceptResult(package_invalid_parent, result_quit_early, /*expect_valid=*/false, m_node.mempool.get())}) { BOOST_ERROR(err_parent_invalid.value()); } else { @@ -353,7 +446,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup) package_missing_parent.push_back(MakeTransactionRef(mtx_child)); { const auto result_missing_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_missing_parent, /*test_accept=*/false); + package_missing_parent, /*test_accept=*/false, /*client_maxfeerate=*/{}); BOOST_CHECK(result_missing_parent.m_state.IsInvalid()); BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetResult(), PackageValidationResult::PCKG_POLICY); BOOST_CHECK_EQUAL(result_missing_parent.m_state.GetRejectReason(), "package-not-child-with-unconfirmed-parents"); @@ -363,7 +456,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup) // Submit package with parent + child. { const auto submit_parent_child = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_parent_child, /*test_accept=*/false); + package_parent_child, /*test_accept=*/false, /*client_maxfeerate=*/{}); expected_pool_size += 2; BOOST_CHECK_MESSAGE(submit_parent_child.m_state.IsValid(), "Package validation unexpectedly failed: " << submit_parent_child.m_state.GetRejectReason()); @@ -385,7 +478,7 @@ BOOST_FIXTURE_TEST_CASE(package_submission_tests, TestChain100Setup) // Already-in-mempool transactions should be detected and de-duplicated. { const auto submit_deduped = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_parent_child, /*test_accept=*/false); + package_parent_child, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_deduped{CheckPackageMempoolAcceptResult(package_parent_child, submit_deduped, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_deduped.value()); } else { @@ -450,13 +543,15 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup) BOOST_CHECK_EQUAL(ptx_child1->GetHash(), ptx_child2->GetHash()); // child1 and child2 have different wtxids BOOST_CHECK(ptx_child1->GetWitnessHash() != ptx_child2->GetWitnessHash()); + // Check that they have different package hashes + BOOST_CHECK(GetPackageHash({ptx_parent, ptx_child1}) != GetPackageHash({ptx_parent, ptx_child2})); // Try submitting Package1{parent, child1} and Package2{parent, child2} where the children are // same-txid-different-witness. { Package package_parent_child1{ptx_parent, ptx_child1}; const auto submit_witness1 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_parent_child1, /*test_accept=*/false); + package_parent_child1, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_witness1{CheckPackageMempoolAcceptResult(package_parent_child1, submit_witness1, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_witness1.value()); } @@ -464,7 +559,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup) // Child2 would have been validated individually. Package package_parent_child2{ptx_parent, ptx_child2}; const auto submit_witness2 = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_parent_child2, /*test_accept=*/false); + package_parent_child2, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_witness2{CheckPackageMempoolAcceptResult(package_parent_child2, submit_witness2, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_witness2.value()); } else { @@ -478,7 +573,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup) // Deduplication should work when wtxid != txid. Submit package with the already-in-mempool // transactions again, which should not fail. const auto submit_segwit_dedup = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_parent_child1, /*test_accept=*/false); + package_parent_child1, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_segwit_dedup{CheckPackageMempoolAcceptResult(package_parent_child1, submit_segwit_dedup, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_segwit_dedup.value()); } else { @@ -503,12 +598,13 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup) /*output_destination=*/grandchild_locking_script, /*output_amount=*/CAmount(47 * COIN), /*submit=*/false); CTransactionRef ptx_grandchild = MakeTransactionRef(mtx_grandchild); - + // Check that they have different package hashes + BOOST_CHECK(GetPackageHash({ptx_child1, ptx_grandchild}) != GetPackageHash({ptx_child2, ptx_grandchild})); // We already submitted child1 above. { Package package_child2_grandchild{ptx_child2, ptx_grandchild}; const auto submit_spend_ignored = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_child2_grandchild, /*test_accept=*/false); + package_child2_grandchild, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_spend_ignored{CheckPackageMempoolAcceptResult(package_child2_grandchild, submit_spend_ignored, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_spend_ignored.value()); } else { @@ -583,7 +679,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup) CTransactionRef ptx_parent3 = MakeTransactionRef(mtx_parent3); package_mixed.push_back(ptx_parent3); BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(GetVirtualTransactionSize(*ptx_parent3)) > low_fee_amt); - BOOST_CHECK(m_node.mempool->m_min_relay_feerate.GetFee(GetVirtualTransactionSize(*ptx_parent3)) <= low_fee_amt); + BOOST_CHECK(m_node.mempool->m_opts.min_relay_feerate.GetFee(GetVirtualTransactionSize(*ptx_parent3)) <= low_fee_amt); // child spends parent1, parent2, and parent3 CKey mixed_grandchild_key = GenerateRandomKey(); @@ -606,7 +702,7 @@ BOOST_FIXTURE_TEST_CASE(package_witness_swap_tests, TestChain100Setup) // parent3 should be accepted // child should be accepted { - const auto mixed_result = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_mixed, false); + const auto mixed_result = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, package_mixed, false, /*client_maxfeerate=*/{}); if (auto err_mixed{CheckPackageMempoolAcceptResult(package_mixed, mixed_result, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_mixed.value()); } else { @@ -670,7 +766,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup) { BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); const auto submit_cpfp_deprio = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_cpfp, /*test_accept=*/ false); + package_cpfp, /*test_accept=*/ false, /*client_maxfeerate=*/{}); if (auto err_cpfp_deprio{CheckPackageMempoolAcceptResult(package_cpfp, submit_cpfp_deprio, /*expect_valid=*/false, m_node.mempool.get())}) { BOOST_ERROR(err_cpfp_deprio.value()); } else { @@ -692,7 +788,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup) { BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); const auto submit_cpfp = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_cpfp, /*test_accept=*/ false); + package_cpfp, /*test_accept=*/ false, /*client_maxfeerate=*/{}); if (auto err_cpfp{CheckPackageMempoolAcceptResult(package_cpfp, submit_cpfp, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_cpfp.value()); } else { @@ -729,7 +825,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup) CTransactionRef tx_parent_cheap = MakeTransactionRef(mtx_parent_cheap); package_still_too_low.push_back(tx_parent_cheap); BOOST_CHECK(m_node.mempool->GetMinFee().GetFee(GetVirtualTransactionSize(*tx_parent_cheap)) > parent_fee); - BOOST_CHECK(m_node.mempool->m_min_relay_feerate.GetFee(GetVirtualTransactionSize(*tx_parent_cheap)) <= parent_fee); + BOOST_CHECK(m_node.mempool->m_opts.min_relay_feerate.GetFee(GetVirtualTransactionSize(*tx_parent_cheap)) <= parent_fee); auto mtx_child_cheap = CreateValidMempoolTransaction(/*input_transaction=*/tx_parent_cheap, /*input_vout=*/0, /*input_height=*/101, /*input_signing_key=*/child_key, @@ -744,7 +840,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup) // Cheap package should fail for being too low fee. { const auto submit_package_too_low = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_still_too_low, /*test_accept=*/false); + package_still_too_low, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_package_too_low{CheckPackageMempoolAcceptResult(package_still_too_low, submit_package_too_low, /*expect_valid=*/false, m_node.mempool.get())}) { BOOST_ERROR(err_package_too_low.value()); } else { @@ -770,7 +866,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup) // Now that the child's fees have "increased" by 1 BTC, the cheap package should succeed. { const auto submit_prioritised_package = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_still_too_low, /*test_accept=*/false); + package_still_too_low, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_prioritised{CheckPackageMempoolAcceptResult(package_still_too_low, submit_prioritised_package, /*expect_valid=*/true, m_node.mempool.get())}) { BOOST_ERROR(err_prioritised.value()); } else { @@ -818,7 +914,7 @@ BOOST_FIXTURE_TEST_CASE(package_cpfp_tests, TestChain100Setup) { BOOST_CHECK_EQUAL(m_node.mempool->size(), expected_pool_size); const auto submit_rich_parent = ProcessNewPackage(m_node.chainman->ActiveChainstate(), *m_node.mempool, - package_rich_parent, /*test_accept=*/false); + package_rich_parent, /*test_accept=*/false, /*client_maxfeerate=*/{}); if (auto err_rich_parent{CheckPackageMempoolAcceptResult(package_rich_parent, submit_rich_parent, /*expect_valid=*/false, m_node.mempool.get())}) { BOOST_ERROR(err_rich_parent.value()); } else { diff --git a/src/test/txvalidation_tests.cpp b/src/test/txvalidation_tests.cpp index ecf0889711..95583b53bf 100644 --- a/src/test/txvalidation_tests.cpp +++ b/src/test/txvalidation_tests.cpp @@ -4,11 +4,14 @@ #include <consensus/validation.h> #include <key_io.h> +#include <policy/v3_policy.h> #include <policy/packages.h> #include <policy/policy.h> #include <primitives/transaction.h> +#include <random.h> #include <script/script.h> #include <test/util/setup_common.h> +#include <test/util/txmempool.h> #include <validation.h> #include <boost/test/unit_test.hpp> @@ -48,4 +51,340 @@ BOOST_FIXTURE_TEST_CASE(tx_mempool_reject_coinbase, TestChain100Setup) BOOST_CHECK_EQUAL(result.m_state.GetRejectReason(), "coinbase"); BOOST_CHECK(result.m_state.GetResult() == TxValidationResult::TX_CONSENSUS); } + +// Generate a number of random, nonexistent outpoints. +static inline std::vector<COutPoint> random_outpoints(size_t num_outpoints) { + std::vector<COutPoint> outpoints; + for (size_t i{0}; i < num_outpoints; ++i) { + outpoints.emplace_back(Txid::FromUint256(GetRandHash()), 0); + } + return outpoints; +} + +static inline std::vector<CPubKey> random_keys(size_t num_keys) { + std::vector<CPubKey> keys; + keys.reserve(num_keys); + for (size_t i{0}; i < num_keys; ++i) { + CKey key; + key.MakeNewKey(true); + keys.emplace_back(key.GetPubKey()); + } + return keys; +} + +// Creates a placeholder tx (not valid) with 25 outputs. Specify the nVersion and the inputs. +static inline CTransactionRef make_tx(const std::vector<COutPoint>& inputs, int32_t version) +{ + CMutableTransaction mtx = CMutableTransaction{}; + mtx.nVersion = version; + mtx.vin.resize(inputs.size()); + mtx.vout.resize(25); + for (size_t i{0}; i < inputs.size(); ++i) { + mtx.vin[i].prevout = inputs[i]; + } + for (auto i{0}; i < 25; ++i) { + mtx.vout[i].scriptPubKey = CScript() << OP_TRUE; + mtx.vout[i].nValue = 10000; + } + return MakeTransactionRef(mtx); +} + +BOOST_FIXTURE_TEST_CASE(version3_tests, RegTestingSetup) +{ + // Test V3 policy helper functions + CTxMemPool& pool = *Assert(m_node.mempool); + LOCK2(cs_main, pool.cs); + TestMemPoolEntryHelper entry; + std::set<Txid> empty_conflicts_set; + CTxMemPool::setEntries empty_ancestors; + + auto mempool_tx_v3 = make_tx(random_outpoints(1), /*version=*/3); + pool.addUnchecked(entry.FromTx(mempool_tx_v3)); + auto mempool_tx_v2 = make_tx(random_outpoints(1), /*version=*/2); + pool.addUnchecked(entry.FromTx(mempool_tx_v2)); + // Default values. + CTxMemPool::Limits m_limits{}; + + // Cannot spend from an unconfirmed v3 transaction unless this tx is also v3. + { + // mempool_tx_v3 + // ^ + // tx_v2_from_v3 + auto tx_v2_from_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}}, /*version=*/2); + auto ancestors_v2_from_v3{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v2_from_v3), m_limits)}; + const auto expected_error_str{strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)", + tx_v2_from_v3->GetHash().ToString(), tx_v2_from_v3->GetWitnessHash().ToString(), + mempool_tx_v3->GetHash().ToString(), mempool_tx_v3->GetWitnessHash().ToString())}; + auto result_v2_from_v3{SingleV3Checks(tx_v2_from_v3, *ancestors_v2_from_v3, empty_conflicts_set, GetVirtualTransactionSize(*tx_v2_from_v3))}; + BOOST_CHECK_EQUAL(result_v2_from_v3->first, expected_error_str); + BOOST_CHECK_EQUAL(result_v2_from_v3->second, nullptr); + + Package package_v3_v2{mempool_tx_v3, tx_v2_from_v3}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v2_from_v3, GetVirtualTransactionSize(*tx_v2_from_v3), package_v3_v2, empty_ancestors), expected_error_str); + CTxMemPool::setEntries entries_mempool_v3{pool.GetIter(mempool_tx_v3->GetHash().ToUint256()).value()}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v2_from_v3, GetVirtualTransactionSize(*tx_v2_from_v3), {tx_v2_from_v3}, entries_mempool_v3), expected_error_str); + + // mempool_tx_v3 mempool_tx_v2 + // ^ ^ + // tx_v2_from_v2_and_v3 + auto tx_v2_from_v2_and_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}, COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/2); + auto ancestors_v2_from_both{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v2_from_v2_and_v3), m_limits)}; + const auto expected_error_str_2{strprintf("non-v3 tx %s (wtxid=%s) cannot spend from v3 tx %s (wtxid=%s)", + tx_v2_from_v2_and_v3->GetHash().ToString(), tx_v2_from_v2_and_v3->GetWitnessHash().ToString(), + mempool_tx_v3->GetHash().ToString(), mempool_tx_v3->GetWitnessHash().ToString())}; + auto result_v2_from_both{SingleV3Checks(tx_v2_from_v2_and_v3, *ancestors_v2_from_both, empty_conflicts_set, GetVirtualTransactionSize(*tx_v2_from_v2_and_v3))}; + BOOST_CHECK_EQUAL(result_v2_from_both->first, expected_error_str_2); + BOOST_CHECK_EQUAL(result_v2_from_both->second, nullptr); + + Package package_v3_v2_v2{mempool_tx_v3, mempool_tx_v2, tx_v2_from_v2_and_v3}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v2_from_v2_and_v3, GetVirtualTransactionSize(*tx_v2_from_v2_and_v3), package_v3_v2_v2, empty_ancestors), expected_error_str_2); + } + + // V3 cannot spend from an unconfirmed non-v3 transaction. + { + // mempool_tx_v2 + // ^ + // tx_v3_from_v2 + auto tx_v3_from_v2 = make_tx({COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/3); + auto ancestors_v3_from_v2{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_from_v2), m_limits)}; + const auto expected_error_str{strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)", + tx_v3_from_v2->GetHash().ToString(), tx_v3_from_v2->GetWitnessHash().ToString(), + mempool_tx_v2->GetHash().ToString(), mempool_tx_v2->GetWitnessHash().ToString())}; + auto result_v3_from_v2{SingleV3Checks(tx_v3_from_v2, *ancestors_v3_from_v2, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_from_v2))}; + BOOST_CHECK_EQUAL(result_v3_from_v2->first, expected_error_str); + BOOST_CHECK_EQUAL(result_v3_from_v2->second, nullptr); + + Package package_v2_v3{mempool_tx_v2, tx_v3_from_v2}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_from_v2, GetVirtualTransactionSize(*tx_v3_from_v2), package_v2_v3, empty_ancestors), expected_error_str); + CTxMemPool::setEntries entries_mempool_v2{pool.GetIter(mempool_tx_v2->GetHash().ToUint256()).value()}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_from_v2, GetVirtualTransactionSize(*tx_v3_from_v2), {tx_v3_from_v2}, entries_mempool_v2), expected_error_str); + + // mempool_tx_v3 mempool_tx_v2 + // ^ ^ + // tx_v3_from_v2_and_v3 + auto tx_v3_from_v2_and_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}, COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/3); + auto ancestors_v3_from_both{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_from_v2_and_v3), m_limits)}; + const auto expected_error_str_2{strprintf("v3 tx %s (wtxid=%s) cannot spend from non-v3 tx %s (wtxid=%s)", + tx_v3_from_v2_and_v3->GetHash().ToString(), tx_v3_from_v2_and_v3->GetWitnessHash().ToString(), + mempool_tx_v2->GetHash().ToString(), mempool_tx_v2->GetWitnessHash().ToString())}; + auto result_v3_from_both{SingleV3Checks(tx_v3_from_v2_and_v3, *ancestors_v3_from_both, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_from_v2_and_v3))}; + BOOST_CHECK_EQUAL(result_v3_from_both->first, expected_error_str_2); + BOOST_CHECK_EQUAL(result_v3_from_both->second, nullptr); + + // tx_v3_from_v2_and_v3 also violates V3_ANCESTOR_LIMIT. + const auto expected_error_str_3{strprintf("tx %s (wtxid=%s) would have too many ancestors", + tx_v3_from_v2_and_v3->GetHash().ToString(), tx_v3_from_v2_and_v3->GetWitnessHash().ToString())}; + Package package_v3_v2_v3{mempool_tx_v3, mempool_tx_v2, tx_v3_from_v2_and_v3}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_from_v2_and_v3, GetVirtualTransactionSize(*tx_v3_from_v2_and_v3), package_v3_v2_v3, empty_ancestors), expected_error_str_3); + } + // V3 from V3 is ok, and non-V3 from non-V3 is ok. + { + // mempool_tx_v3 + // ^ + // tx_v3_from_v3 + auto tx_v3_from_v3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}}, /*version=*/3); + auto ancestors_v3{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_from_v3), m_limits)}; + BOOST_CHECK(SingleV3Checks(tx_v3_from_v3, *ancestors_v3, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_from_v3)) + == std::nullopt); + + Package package_v3_v3{mempool_tx_v3, tx_v3_from_v3}; + BOOST_CHECK(PackageV3Checks(tx_v3_from_v3, GetVirtualTransactionSize(*tx_v3_from_v3), package_v3_v3, empty_ancestors) == std::nullopt); + + // mempool_tx_v2 + // ^ + // tx_v2_from_v2 + auto tx_v2_from_v2 = make_tx({COutPoint{mempool_tx_v2->GetHash(), 0}}, /*version=*/2); + auto ancestors_v2{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v2_from_v2), m_limits)}; + BOOST_CHECK(SingleV3Checks(tx_v2_from_v2, *ancestors_v2, empty_conflicts_set, GetVirtualTransactionSize(*tx_v2_from_v2)) + == std::nullopt); + + Package package_v2_v2{mempool_tx_v2, tx_v2_from_v2}; + BOOST_CHECK(PackageV3Checks(tx_v2_from_v2, GetVirtualTransactionSize(*tx_v2_from_v2), package_v2_v2, empty_ancestors) == std::nullopt); + } + + // Tx spending v3 cannot have too many mempool ancestors + // Configuration where the tx has multiple direct parents. + { + Package package_multi_parents; + std::vector<COutPoint> mempool_outpoints; + mempool_outpoints.emplace_back(mempool_tx_v3->GetHash(), 0); + package_multi_parents.emplace_back(mempool_tx_v3); + for (size_t i{0}; i < 2; ++i) { + auto mempool_tx = make_tx(random_outpoints(i + 1), /*version=*/3); + pool.addUnchecked(entry.FromTx(mempool_tx)); + mempool_outpoints.emplace_back(mempool_tx->GetHash(), 0); + package_multi_parents.emplace_back(mempool_tx); + } + auto tx_v3_multi_parent = make_tx(mempool_outpoints, /*version=*/3); + package_multi_parents.emplace_back(tx_v3_multi_parent); + auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_multi_parent), m_limits)}; + BOOST_CHECK_EQUAL(ancestors->size(), 3); + const auto expected_error_str{strprintf("tx %s (wtxid=%s) would have too many ancestors", + tx_v3_multi_parent->GetHash().ToString(), tx_v3_multi_parent->GetWitnessHash().ToString())}; + auto result{SingleV3Checks(tx_v3_multi_parent, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_multi_parent))}; + BOOST_CHECK_EQUAL(result->first, expected_error_str); + BOOST_CHECK_EQUAL(result->second, nullptr); + + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_multi_parent, GetVirtualTransactionSize(*tx_v3_multi_parent), package_multi_parents, empty_ancestors), + expected_error_str); + } + + // Configuration where the tx is in a multi-generation chain. + { + Package package_multi_gen; + CTransactionRef middle_tx; + auto last_outpoint{random_outpoints(1)[0]}; + for (size_t i{0}; i < 2; ++i) { + auto mempool_tx = make_tx({last_outpoint}, /*version=*/3); + pool.addUnchecked(entry.FromTx(mempool_tx)); + last_outpoint = COutPoint{mempool_tx->GetHash(), 0}; + package_multi_gen.emplace_back(mempool_tx); + if (i == 1) middle_tx = mempool_tx; + } + auto tx_v3_multi_gen = make_tx({last_outpoint}, /*version=*/3); + package_multi_gen.emplace_back(tx_v3_multi_gen); + auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_multi_gen), m_limits)}; + const auto expected_error_str{strprintf("tx %s (wtxid=%s) would have too many ancestors", + tx_v3_multi_gen->GetHash().ToString(), tx_v3_multi_gen->GetWitnessHash().ToString())}; + auto result{SingleV3Checks(tx_v3_multi_gen, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_multi_gen))}; + BOOST_CHECK_EQUAL(result->first, expected_error_str); + BOOST_CHECK_EQUAL(result->second, nullptr); + + // Middle tx is what triggers a failure for the grandchild: + BOOST_CHECK_EQUAL(*PackageV3Checks(middle_tx, GetVirtualTransactionSize(*middle_tx), package_multi_gen, empty_ancestors), expected_error_str); + BOOST_CHECK(PackageV3Checks(tx_v3_multi_gen, GetVirtualTransactionSize(*tx_v3_multi_gen), package_multi_gen, empty_ancestors) == std::nullopt); + } + + // Tx spending v3 cannot be too large in virtual size. + auto many_inputs{random_outpoints(100)}; + many_inputs.emplace_back(mempool_tx_v3->GetHash(), 0); + { + auto tx_v3_child_big = make_tx(many_inputs, /*version=*/3); + const auto vsize{GetVirtualTransactionSize(*tx_v3_child_big)}; + auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_child_big), m_limits)}; + const auto expected_error_str{strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes", + tx_v3_child_big->GetHash().ToString(), tx_v3_child_big->GetWitnessHash().ToString(), vsize, V3_CHILD_MAX_VSIZE)}; + auto result{SingleV3Checks(tx_v3_child_big, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_child_big))}; + BOOST_CHECK_EQUAL(result->first, expected_error_str); + BOOST_CHECK_EQUAL(result->second, nullptr); + + Package package_child_big{mempool_tx_v3, tx_v3_child_big}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_child_big, GetVirtualTransactionSize(*tx_v3_child_big), package_child_big, empty_ancestors), + expected_error_str); + } + + // Tx spending v3 cannot have too many sigops. + // This child has 10 P2WSH multisig inputs. + auto multisig_outpoints{random_outpoints(10)}; + multisig_outpoints.emplace_back(mempool_tx_v3->GetHash(), 0); + auto keys{random_keys(2)}; + CScript script_multisig; + script_multisig << OP_1; + for (const auto& key : keys) { + script_multisig << ToByteVector(key); + } + script_multisig << OP_2 << OP_CHECKMULTISIG; + { + CMutableTransaction mtx_many_sigops = CMutableTransaction{}; + mtx_many_sigops.nVersion = 3; + for (const auto& outpoint : multisig_outpoints) { + mtx_many_sigops.vin.emplace_back(outpoint); + mtx_many_sigops.vin.back().scriptWitness.stack.emplace_back(script_multisig.begin(), script_multisig.end()); + } + mtx_many_sigops.vout.resize(1); + mtx_many_sigops.vout.back().scriptPubKey = CScript() << OP_TRUE; + mtx_many_sigops.vout.back().nValue = 10000; + auto tx_many_sigops{MakeTransactionRef(mtx_many_sigops)}; + + auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_many_sigops), m_limits)}; + // legacy uses fAccurate = false, and the maximum number of multisig keys is used + const int64_t total_sigops{static_cast<int64_t>(tx_many_sigops->vin.size()) * static_cast<int64_t>(script_multisig.GetSigOpCount(/*fAccurate=*/false))}; + BOOST_CHECK_EQUAL(total_sigops, tx_many_sigops->vin.size() * MAX_PUBKEYS_PER_MULTISIG); + const int64_t bip141_vsize{GetVirtualTransactionSize(*tx_many_sigops)}; + // Weight limit is not reached... + BOOST_CHECK(SingleV3Checks(tx_many_sigops, *ancestors, empty_conflicts_set, bip141_vsize) == std::nullopt); + // ...but sigop limit is. + const auto expected_error_str{strprintf("v3 child tx %s (wtxid=%s) is too big: %u > %u virtual bytes", + tx_many_sigops->GetHash().ToString(), tx_many_sigops->GetWitnessHash().ToString(), + total_sigops * DEFAULT_BYTES_PER_SIGOP / WITNESS_SCALE_FACTOR, V3_CHILD_MAX_VSIZE)}; + auto result{SingleV3Checks(tx_many_sigops, *ancestors, empty_conflicts_set, + GetVirtualTransactionSize(*tx_many_sigops, /*nSigOpCost=*/total_sigops, /*bytes_per_sigop=*/ DEFAULT_BYTES_PER_SIGOP))}; + BOOST_CHECK_EQUAL(result->first, expected_error_str); + BOOST_CHECK_EQUAL(result->second, nullptr); + + Package package_child_sigops{mempool_tx_v3, tx_many_sigops}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_many_sigops, total_sigops * DEFAULT_BYTES_PER_SIGOP / WITNESS_SCALE_FACTOR, package_child_sigops, empty_ancestors), + expected_error_str); + } + + // Parent + child with v3 in the mempool. Child is allowed as long as it is under V3_CHILD_MAX_VSIZE. + auto tx_mempool_v3_child = make_tx({COutPoint{mempool_tx_v3->GetHash(), 0}}, /*version=*/3); + { + BOOST_CHECK(GetTransactionWeight(*tx_mempool_v3_child) <= V3_CHILD_MAX_VSIZE * WITNESS_SCALE_FACTOR); + auto ancestors{pool.CalculateMemPoolAncestors(entry.FromTx(tx_mempool_v3_child), m_limits)}; + BOOST_CHECK(SingleV3Checks(tx_mempool_v3_child, *ancestors, empty_conflicts_set, GetVirtualTransactionSize(*tx_mempool_v3_child)) == std::nullopt); + pool.addUnchecked(entry.FromTx(tx_mempool_v3_child)); + + Package package_v3_1p1c{mempool_tx_v3, tx_mempool_v3_child}; + BOOST_CHECK(PackageV3Checks(tx_mempool_v3_child, GetVirtualTransactionSize(*tx_mempool_v3_child), package_v3_1p1c, empty_ancestors) == std::nullopt); + } + + // A v3 transaction cannot have more than 1 descendant. Sibling is returned when exactly 1 exists. + { + auto tx_v3_child2 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 1}}, /*version=*/3); + + // Configuration where parent already has 1 other child in mempool + auto ancestors_1sibling{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_child2), m_limits)}; + const auto expected_error_str{strprintf("tx %s (wtxid=%s) would exceed descendant count limit", + mempool_tx_v3->GetHash().ToString(), mempool_tx_v3->GetWitnessHash().ToString())}; + auto result_with_sibling_eviction{SingleV3Checks(tx_v3_child2, *ancestors_1sibling, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_child2))}; + BOOST_CHECK_EQUAL(result_with_sibling_eviction->first, expected_error_str); + // The other mempool child is returned to allow for sibling eviction. + BOOST_CHECK_EQUAL(result_with_sibling_eviction->second, tx_mempool_v3_child); + + // If directly replacing the child, make sure there is no double-counting. + BOOST_CHECK(SingleV3Checks(tx_v3_child2, *ancestors_1sibling, {tx_mempool_v3_child->GetHash()}, GetVirtualTransactionSize(*tx_v3_child2)) + == std::nullopt); + + Package package_v3_1p2c{mempool_tx_v3, tx_mempool_v3_child, tx_v3_child2}; + BOOST_CHECK_EQUAL(*PackageV3Checks(tx_v3_child2, GetVirtualTransactionSize(*tx_v3_child2), package_v3_1p2c, empty_ancestors), + expected_error_str); + + // Configuration where parent already has 2 other children in mempool (no sibling eviction allowed). This may happen as the result of a reorg. + pool.addUnchecked(entry.FromTx(tx_v3_child2)); + auto tx_v3_child3 = make_tx({COutPoint{mempool_tx_v3->GetHash(), 24}}, /*version=*/3); + auto entry_mempool_parent = pool.GetIter(mempool_tx_v3->GetHash().ToUint256()).value(); + BOOST_CHECK_EQUAL(entry_mempool_parent->GetCountWithDescendants(), 3); + auto ancestors_2siblings{pool.CalculateMemPoolAncestors(entry.FromTx(tx_v3_child3), m_limits)}; + + auto result_2children{SingleV3Checks(tx_v3_child3, *ancestors_2siblings, empty_conflicts_set, GetVirtualTransactionSize(*tx_v3_child3))}; + BOOST_CHECK_EQUAL(result_2children->first, expected_error_str); + // The other mempool child is not returned because sibling eviction is not allowed. + BOOST_CHECK_EQUAL(result_2children->second, nullptr); + } + + // Sibling eviction: parent already has 1 other child, which also has its own child (no sibling eviction allowed). This may happen as the result of a reorg. + { + auto tx_mempool_grandparent = make_tx(random_outpoints(1), /*version=*/3); + auto tx_mempool_sibling = make_tx({COutPoint{tx_mempool_grandparent->GetHash(), 0}}, /*version=*/3); + auto tx_mempool_nibling = make_tx({COutPoint{tx_mempool_sibling->GetHash(), 0}}, /*version=*/3); + auto tx_to_submit = make_tx({COutPoint{tx_mempool_grandparent->GetHash(), 1}}, /*version=*/3); + + pool.addUnchecked(entry.FromTx(tx_mempool_grandparent)); + pool.addUnchecked(entry.FromTx(tx_mempool_sibling)); + pool.addUnchecked(entry.FromTx(tx_mempool_nibling)); + + auto ancestors_3gen{pool.CalculateMemPoolAncestors(entry.FromTx(tx_to_submit), m_limits)}; + const auto expected_error_str{strprintf("tx %s (wtxid=%s) would exceed descendant count limit", + tx_mempool_grandparent->GetHash().ToString(), tx_mempool_grandparent->GetWitnessHash().ToString())}; + auto result_3gen{SingleV3Checks(tx_to_submit, *ancestors_3gen, empty_conflicts_set, GetVirtualTransactionSize(*tx_to_submit))}; + BOOST_CHECK_EQUAL(result_3gen->first, expected_error_str); + // The other mempool child is not returned because sibling eviction is not allowed. + BOOST_CHECK_EQUAL(result_3gen->second, nullptr); + } + + // Configuration where tx has multiple generations of descendants is not tested because that is + // equivalent to the tx with multiple generations of ancestors. +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util/chainstate.h b/src/test/util/chainstate.h index e2a88eacdd..ff95e64b7e 100644 --- a/src/test/util/chainstate.h +++ b/src/test/util/chainstate.h @@ -91,13 +91,16 @@ CreateAndActivateUTXOSnapshot( // these blocks instead CBlockIndex *pindex = orig_tip; while (pindex && pindex != chain.m_chain.Tip()) { - pindex->nStatus &= ~BLOCK_HAVE_DATA; - pindex->nStatus &= ~BLOCK_HAVE_UNDO; - // We have to set the ASSUMED_VALID flag, because otherwise it - // would not be possible to have a block index entry without HAVE_DATA - // and with nTx > 0 (since we aren't setting the pruned flag); - // see CheckBlockIndex(). - pindex->nStatus |= BLOCK_ASSUMED_VALID; + // Remove all data and validity flags by just setting + // BLOCK_VALID_TREE. Also reset transaction counts and sequence + // ids that are set when blocks are received, to make test setup + // more realistic and satisfy consistency checks in + // CheckBlockIndex(). + assert(pindex->IsValid(BlockStatus::BLOCK_VALID_TREE)); + pindex->nStatus = BlockStatus::BLOCK_VALID_TREE; + pindex->nTx = 0; + pindex->nChainTx = 0; + pindex->nSequenceId = 0; pindex = pindex->pprev; } } diff --git a/src/test/util/mining.cpp b/src/test/util/mining.cpp index 08d1b4c902..ad7a38d3fe 100644 --- a/src/test/util/mining.cpp +++ b/src/test/util/mining.cpp @@ -95,12 +95,12 @@ COutPoint MineBlock(const NodeContext& node, std::shared_ptr<CBlock>& block) const auto old_height = WITH_LOCK(chainman.GetMutex(), return chainman.ActiveHeight()); bool new_block; BlockValidationStateCatcher bvsc{block->GetHash()}; - RegisterValidationInterface(&bvsc); + node.validation_signals->RegisterValidationInterface(&bvsc); const bool processed{chainman.ProcessNewBlock(block, true, true, &new_block)}; const bool duplicate{!new_block && processed}; assert(!duplicate); - UnregisterValidationInterface(&bvsc); - SyncWithValidationInterfaceQueue(); + node.validation_signals->UnregisterValidationInterface(&bvsc); + node.validation_signals->SyncWithValidationInterfaceQueue(); const bool was_valid{bvsc.m_state && bvsc.m_state->IsValid()}; assert(old_height + was_valid == WITH_LOCK(chainman.GetMutex(), return chainman.ActiveHeight())); diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 8789e86196..e9566cf50b 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -2,6 +2,8 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include <config/bitcoin-config.h> // IWYU pragma: keep + #include <test/util/setup_common.h> #include <kernel/validation_cache_sizes.h> @@ -10,7 +12,6 @@ #include <banman.h> #include <chainparams.h> #include <common/system.h> -#include <common/url.h> #include <consensus/consensus.h> #include <consensus/params.h> #include <consensus/validation.h> @@ -44,11 +45,11 @@ #include <test/util/net.h> #include <test/util/random.h> #include <test/util/txmempool.h> -#include <timedata.h> #include <txdb.h> #include <txmempool.h> #include <util/chaintype.h> #include <util/check.h> +#include <util/fs_helpers.h> #include <util/rbf.h> #include <util/strencodings.h> #include <util/string.h> @@ -77,7 +78,6 @@ using node::RegenerateCommitments; using node::VerifyLoadedChainstate; const std::function<std::string(const char*)> G_TRANSLATION_FUN = nullptr; -UrlDecodeFn* const URL_DECODE = nullptr; /** Random context to get unique temp data dirs. Separate from g_insecure_rand_ctx, which can be seeded from a const env var */ static FastRandomContext g_insecure_rand_ctx_temp_path; @@ -97,9 +97,22 @@ struct NetworkSetup }; static NetworkSetup g_networksetup_instance; +/** Register test-only arguments */ +static void SetupUnitTestArgs(ArgsManager& argsman) +{ + argsman.AddArg("-testdatadir", strprintf("Custom data directory (default: %s<random_string>)", fs::PathToString(fs::temp_directory_path() / "test_common_" PACKAGE_NAME / "")), + ArgsManager::ALLOW_ANY, OptionsCategory::DEBUG_TEST); +} + +/** Test setup failure */ +static void ExitFailure(std::string_view str_err) +{ + std::cerr << str_err << std::endl; + exit(EXIT_FAILURE); +} + BasicTestingSetup::BasicTestingSetup(const ChainType chainType, const std::vector<const char*>& extra_args) - : m_path_root{fs::temp_directory_path() / "test_common_" PACKAGE_NAME / g_insecure_rand_ctx_temp_path.rand256().ToString()}, - m_args{} + : m_args{} { m_node.shutdown = &m_interrupt; m_node.args = &gArgs; @@ -120,18 +133,49 @@ BasicTestingSetup::BasicTestingSetup(const ChainType chainType, const std::vecto arguments = Cat(arguments, G_TEST_COMMAND_LINE_ARGUMENTS()); } util::ThreadRename("test"); - fs::create_directories(m_path_root); - m_args.ForceSetArg("-datadir", fs::PathToString(m_path_root)); - gArgs.ForceSetArg("-datadir", fs::PathToString(m_path_root)); gArgs.ClearPathCache(); { SetupServerArgs(*m_node.args); + SetupUnitTestArgs(*m_node.args); std::string error; if (!m_node.args->ParseParameters(arguments.size(), arguments.data(), error)) { m_node.args->ClearArgs(); throw std::runtime_error{error}; } } + + if (!m_node.args->IsArgSet("-testdatadir")) { + // By default, the data directory has a random name + const auto rand_str{g_insecure_rand_ctx_temp_path.rand256().ToString()}; + m_path_root = fs::temp_directory_path() / "test_common_" PACKAGE_NAME / rand_str; + TryCreateDirectories(m_path_root); + } else { + // Custom data directory + m_has_custom_datadir = true; + fs::path root_dir{m_node.args->GetPathArg("-testdatadir")}; + if (root_dir.empty()) ExitFailure("-testdatadir argument is empty, please specify a path"); + + root_dir = fs::absolute(root_dir); + const std::string test_path{G_TEST_GET_FULL_NAME ? G_TEST_GET_FULL_NAME() : ""}; + m_path_lock = root_dir / "test_common_" PACKAGE_NAME / fs::PathFromString(test_path); + m_path_root = m_path_lock / "datadir"; + + // Try to obtain the lock; if unsuccessful don't disturb the existing test. + TryCreateDirectories(m_path_lock); + if (util::LockDirectory(m_path_lock, ".lock", /*probe_only=*/false) != util::LockResult::Success) { + ExitFailure("Cannot obtain a lock on test data lock directory " + fs::PathToString(m_path_lock) + '\n' + "The test executable is probably already running."); + } + + // Always start with a fresh data directory; this doesn't delete the .lock file located one level above. + fs::remove_all(m_path_root); + if (!TryCreateDirectories(m_path_root)) ExitFailure("Cannot create test data directory"); + + // Print the test directory name if custom. + std::cout << "Test directory (will not be deleted): " << m_path_root << std::endl; + } + m_args.ForceSetArg("-datadir", fs::PathToString(m_path_root)); + gArgs.ForceSetArg("-datadir", fs::PathToString(m_path_root)); + SelectParams(chainType); SeedInsecureRand(); if (G_TEST_LOG_FUN) LogInstance().PushBackCallback(G_TEST_LOG_FUN); @@ -139,6 +183,7 @@ BasicTestingSetup::BasicTestingSetup(const ChainType chainType, const std::vecto AppInitParameterInteraction(*m_node.args); LogInstance().StartLogging(); m_node.kernel = std::make_unique<kernel::Context>(); + m_node.ecc_context = std::make_unique<ECC_Context>(); SetupEnvironment(); ValidationCacheSizes validation_cache_sizes{}; @@ -156,10 +201,17 @@ BasicTestingSetup::BasicTestingSetup(const ChainType chainType, const std::vecto BasicTestingSetup::~BasicTestingSetup() { + m_node.ecc_context.reset(); m_node.kernel.reset(); SetMockTime(0s); // Reset mocktime for following tests LogInstance().DisconnectTestLogger(); - fs::remove_all(m_path_root); + if (m_has_custom_datadir) { + // Only remove the lock file, preserve the data directory. + UnlockDirectory(m_path_lock, ".lock"); + fs::remove(m_path_lock / ".lock"); + } else { + fs::remove_all(m_path_root); + } gArgs.ClearArgs(); } @@ -172,7 +224,7 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, const std::vecto // from blocking due to queue overrun. m_node.scheduler = std::make_unique<CScheduler>(); m_node.scheduler->m_service_thread = std::thread(util::TraceThread, "scheduler", [&] { m_node.scheduler->serviceQueue(); }); - GetMainSignals().RegisterBackgroundSignalScheduler(*m_node.scheduler); + m_node.validation_signals = std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(*m_node.scheduler)); m_node.fee_estimator = std::make_unique<CBlockPolicyEstimator>(FeeestPath(*m_node.args), DEFAULT_ACCEPT_STALE_FEE_ESTIMATES); m_node.mempool = std::make_unique<CTxMemPool>(MemPoolOptionsForTest(m_node)); @@ -184,9 +236,9 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, const std::vecto const ChainstateManager::Options chainman_opts{ .chainparams = chainparams, .datadir = m_args.GetDataDirNet(), - .adjusted_time_callback = GetAdjustedTime, .check_block_index = true, .notifications = *m_node.notifications, + .signals = m_node.validation_signals.get(), .worker_threads_num = 2, }; const BlockManager::Options blockman_opts{ @@ -204,8 +256,7 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, const std::vecto ChainTestingSetup::~ChainTestingSetup() { if (m_node.scheduler) m_node.scheduler->stop(); - GetMainSignals().FlushBackgroundCallbacks(); - GetMainSignals().UnregisterBackgroundSignalScheduler(); + m_node.validation_signals->FlushBackgroundCallbacks(); m_node.connman.reset(); m_node.banman.reset(); m_node.addrman.reset(); @@ -214,6 +265,7 @@ ChainTestingSetup::~ChainTestingSetup() m_node.mempool.reset(); m_node.fee_estimator.reset(); m_node.chainman.reset(); + m_node.validation_signals.reset(); m_node.scheduler.reset(); } @@ -500,9 +552,9 @@ void TestChain100Setup::MockMempoolMinFee(const CFeeRate& target_feerate) assert(m_node.mempool->size() == 0); // The target feerate cannot be too low... // ...otherwise the transaction's feerate will need to be negative. - assert(target_feerate > m_node.mempool->m_incremental_relay_feerate); + assert(target_feerate > m_node.mempool->m_opts.incremental_relay_feerate); // ...otherwise this is not meaningful. The feerate policy uses the maximum of both feerates. - assert(target_feerate > m_node.mempool->m_min_relay_feerate); + assert(target_feerate > m_node.mempool->m_opts.min_relay_feerate); // Manually create an invalid transaction. Manually set the fee in the CTxMemPoolEntry to // achieve the exact target feerate. @@ -513,7 +565,7 @@ void TestChain100Setup::MockMempoolMinFee(const CFeeRate& target_feerate) LockPoints lp; // The new mempool min feerate is equal to the removed package's feerate + incremental feerate. const auto tx_fee = target_feerate.GetFee(GetVirtualTransactionSize(*tx)) - - m_node.mempool->m_incremental_relay_feerate.GetFee(GetVirtualTransactionSize(*tx)); + m_node.mempool->m_opts.incremental_relay_feerate.GetFee(GetVirtualTransactionSize(*tx)); m_node.mempool->addUnchecked(CTxMemPoolEntry(tx, /*fee=*/tx_fee, /*time=*/0, /*entry_height=*/1, /*entry_sequence=*/0, /*spends_coinbase=*/true, /*sigops_cost=*/1, lp)); diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h index 9ff4c372a5..dbd66e3585 100644 --- a/src/test/util/setup_common.h +++ b/src/test/util/setup_common.h @@ -6,6 +6,7 @@ #define BITCOIN_TEST_UTIL_SETUP_COMMON_H #include <common/args.h> // IWYU pragma: export +#include <kernel/context.h> #include <key.h> #include <node/caches.h> #include <node/context.h> // IWYU pragma: export @@ -15,6 +16,7 @@ #include <util/chaintype.h> // IWYU pragma: export #include <util/check.h> #include <util/fs.h> +#include <util/signalinterrupt.h> #include <util/string.h> #include <util/vector.h> @@ -32,6 +34,9 @@ extern const std::function<void(const std::string&)> G_TEST_LOG_FUN; /** Retrieve the command line arguments. */ extern const std::function<std::vector<const char*>()> G_TEST_COMMAND_LINE_ARGUMENTS; +/** Retrieve the unit test name. */ +extern const std::function<std::string()> G_TEST_GET_FULL_NAME; + // Enable BOOST_CHECK_EQUAL for enum class types namespace std { template <typename T> @@ -53,7 +58,9 @@ struct BasicTestingSetup { explicit BasicTestingSetup(const ChainType chainType = ChainType::MAIN, const std::vector<const char*>& extra_args = {}); ~BasicTestingSetup(); - const fs::path m_path_root; + fs::path m_path_root; + fs::path m_path_lock; + bool m_has_custom_datadir{false}; ArgsManager m_args; }; diff --git a/src/test/util/txmempool.cpp b/src/test/util/txmempool.cpp index 379c3c9329..870cdd0b32 100644 --- a/src/test/util/txmempool.cpp +++ b/src/test/util/txmempool.cpp @@ -7,6 +7,7 @@ #include <chainparams.h> #include <node/context.h> #include <node/mempool_args.h> +#include <policy/v3_policy.h> #include <txmempool.h> #include <util/check.h> #include <util/time.h> @@ -21,6 +22,7 @@ CTxMemPool::Options MemPoolOptionsForTest(const NodeContext& node) // Default to always checking mempool regardless of // chainparams.DefaultConsistencyChecks for tests .check_ratio = 1, + .signals = node.validation_signals.get(), }; const auto result{ApplyArgsManOptions(*node.args, ::Params(), mempool_opts)}; Assert(result); @@ -66,12 +68,6 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns, return strprintf("tx %s unexpectedly failed: %s", wtxid.ToString(), atmp_result.m_state.ToString()); } - //m_replaced_transactions should exist iff the result was VALID - if (atmp_result.m_replaced_transactions.has_value() != valid) { - return strprintf("tx %s result should %shave m_replaced_transactions", - wtxid.ToString(), valid ? "" : "not "); - } - // m_vsize and m_base_fees should exist iff the result was VALID or MEMPOOL_ENTRY const bool mempool_entry{atmp_result.m_result_type == MempoolAcceptResult::ResultType::MEMPOOL_ENTRY}; if (atmp_result.m_base_fees.has_value() != (valid || mempool_entry)) { @@ -116,3 +112,28 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns, } return std::nullopt; } + +void CheckMempoolV3Invariants(const CTxMemPool& tx_pool) +{ + LOCK(tx_pool.cs); + for (const auto& tx_info : tx_pool.infoAll()) { + const auto& entry = *Assert(tx_pool.GetEntry(tx_info.tx->GetHash())); + if (tx_info.tx->nVersion == 3) { + // Check that special v3 ancestor/descendant limits and rules are always respected + Assert(entry.GetCountWithDescendants() <= V3_DESCENDANT_LIMIT); + Assert(entry.GetCountWithAncestors() <= V3_ANCESTOR_LIMIT); + // If this transaction has at least 1 ancestor, it's a "child" and has restricted weight. + if (entry.GetCountWithAncestors() > 1) { + Assert(entry.GetTxSize() <= V3_CHILD_MAX_VSIZE); + // All v3 transactions must only have v3 unconfirmed parents. + const auto& parents = entry.GetMemPoolParentsConst(); + Assert(parents.begin()->get().GetSharedTx()->nVersion == 3); + } + } else if (entry.GetCountWithAncestors() > 1) { + // All non-v3 transactions must only have non-v3 unconfirmed parents. + for (const auto& parent : entry.GetMemPoolParentsConst()) { + Assert(parent.get().GetSharedTx()->nVersion != 3); + } + } + } +} diff --git a/src/test/util/txmempool.h b/src/test/util/txmempool.h index a866d1ce74..b3022af7df 100644 --- a/src/test/util/txmempool.h +++ b/src/test/util/txmempool.h @@ -46,4 +46,14 @@ std::optional<std::string> CheckPackageMempoolAcceptResult(const Package& txns, const PackageMempoolAcceptResult& result, bool expect_valid, const CTxMemPool* mempool); + +/** For every transaction in tx_pool, check v3 invariants: + * - a v3 tx's ancestor count must be within V3_ANCESTOR_LIMIT + * - a v3 tx's descendant count must be within V3_DESCENDANT_LIMIT + * - if a v3 tx has ancestors, its sigop-adjusted vsize must be within V3_CHILD_MAX_VSIZE + * - any non-v3 tx must only have non-v3 parents + * - any v3 tx must only have v3 parents + * */ +void CheckMempoolV3Invariants(const CTxMemPool& tx_pool); + #endif // BITCOIN_TEST_UTIL_TXMEMPOOL_H diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 47808a2a58..9a2add748e 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -290,13 +290,18 @@ BOOST_AUTO_TEST_CASE(util_TrimString) BOOST_AUTO_TEST_CASE(util_FormatISO8601DateTime) { + BOOST_CHECK_EQUAL(FormatISO8601DateTime(971890963199), "32767-12-31T23:59:59Z"); + BOOST_CHECK_EQUAL(FormatISO8601DateTime(971890876800), "32767-12-31T00:00:00Z"); BOOST_CHECK_EQUAL(FormatISO8601DateTime(1317425777), "2011-09-30T23:36:17Z"); BOOST_CHECK_EQUAL(FormatISO8601DateTime(0), "1970-01-01T00:00:00Z"); } BOOST_AUTO_TEST_CASE(util_FormatISO8601Date) { + BOOST_CHECK_EQUAL(FormatISO8601Date(971890963199), "32767-12-31"); + BOOST_CHECK_EQUAL(FormatISO8601Date(971890876800), "32767-12-31"); BOOST_CHECK_EQUAL(FormatISO8601Date(1317425777), "2011-09-30"); + BOOST_CHECK_EQUAL(FormatISO8601Date(0), "1970-01-01"); } BOOST_AUTO_TEST_CASE(util_FormatMoney) diff --git a/src/test/util_threadnames_tests.cpp b/src/test/util_threadnames_tests.cpp index 45d3a58fd3..df5b1a4461 100644 --- a/src/test/util_threadnames_tests.cpp +++ b/src/test/util_threadnames_tests.cpp @@ -11,9 +11,7 @@ #include <thread> #include <vector> -#if defined(HAVE_CONFIG_H) -#include <config/bitcoin-config.h> -#endif +#include <config/bitcoin-config.h> // IWYU pragma: keep #include <boost/test/unit_test.hpp> diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp index 35e5c6a037..69f4e305ab 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -127,6 +127,7 @@ std::shared_ptr<const CBlock> MinerTestingSetup::BadBlock(const uint256& prev_ha return ret; } +// NOLINTNEXTLINE(misc-no-recursion) void MinerTestingSetup::BuildChain(const uint256& root, int height, const unsigned int invalid_rate, const unsigned int branch_rate, const unsigned int max_size, std::vector<std::shared_ptr<const CBlock>>& blocks) { if (height <= 0 || blocks.size() >= max_size) return; @@ -158,7 +159,7 @@ BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering) bool ignored; // Connect the genesis block and drain any outstanding events BOOST_CHECK(Assert(m_node.chainman)->ProcessNewBlock(std::make_shared<CBlock>(Params().GenesisBlock()), true, true, &ignored)); - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); // subscribe to events (this subscriber will validate event ordering) const CBlockIndex* initial_tip = nullptr; @@ -167,7 +168,7 @@ BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering) initial_tip = m_node.chainman->ActiveChain().Tip(); } auto sub = std::make_shared<TestSubscriber>(initial_tip->GetBlockHash()); - RegisterSharedValidationInterface(sub); + m_node.validation_signals->RegisterSharedValidationInterface(sub); // create a bunch of threads that repeatedly process a block generated above at random // this will create parallelism and randomness inside validation - the ValidationInterface @@ -196,9 +197,9 @@ BOOST_AUTO_TEST_CASE(processnewblock_signals_ordering) for (auto& t : threads) { t.join(); } - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); - UnregisterSharedValidationInterface(sub); + m_node.validation_signals->UnregisterSharedValidationInterface(sub); LOCK(cs_main); BOOST_CHECK_EQUAL(sub->m_expected_tip, m_node.chainman->ActiveChain().Tip()->GetBlockHash()); diff --git a/src/test/validation_chainstate_tests.cpp b/src/test/validation_chainstate_tests.cpp index fe2d2ba592..1c02066047 100644 --- a/src/test/validation_chainstate_tests.cpp +++ b/src/test/validation_chainstate_tests.cpp @@ -12,6 +12,7 @@ #include <test/util/random.h> #include <test/util/setup_common.h> #include <uint256.h> +#include <util/check.h> #include <validation.h> #include <vector> @@ -102,14 +103,14 @@ BOOST_FIXTURE_TEST_CASE(chainstate_update_tip, TestChain100Setup) BOOST_CHECK_EQUAL(chainman.GetAll().size(), 2); - Chainstate& background_cs{*[&] { + Chainstate& background_cs{*Assert([&]() -> Chainstate* { for (Chainstate* cs : chainman.GetAll()) { if (cs != &chainman.ActiveChainstate()) { return cs; } } - assert(false); - }()}; + return nullptr; + }())}; // Append the first block to the background chain. BlockValidationState state; diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp index 368ba8bee4..4bf66a55eb 100644 --- a/src/test/validation_chainstatemanager_tests.cpp +++ b/src/test/validation_chainstatemanager_tests.cpp @@ -15,7 +15,6 @@ #include <test/util/random.h> #include <test/util/setup_common.h> #include <test/util/validation.h> -#include <timedata.h> #include <uint256.h> #include <validation.h> #include <validationinterface.h> @@ -103,7 +102,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup) BOOST_CHECK_EQUAL(active_tip2, c2.m_chain.Tip()); // Let scheduler events finish running to avoid accessing memory that is going to be unloaded - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); } //! Test rebalancing the caches associated with each chainstate. @@ -277,9 +276,6 @@ struct SnapshotTestSetup : TestChain100Setup { BOOST_CHECK_EQUAL( *node::ReadSnapshotBaseBlockhash(found), *chainman.SnapshotBlockhash()); - - // Ensure that the genesis block was not marked assumed-valid. - BOOST_CHECK(!chainman.ActiveChain().Genesis()->IsAssumedValid()); } const auto& au_data = ::Params().AssumeutxoForHeight(snapshot_height); @@ -375,7 +371,7 @@ struct SnapshotTestSetup : TestChain100Setup { cs->ForceFlushStateToDisk(); } // Process all callbacks referring to the old manager before wiping it. - SyncWithValidationInterfaceQueue(); + m_node.validation_signals->SyncWithValidationInterfaceQueue(); LOCK(::cs_main); chainman.ResetChainstates(); BOOST_CHECK_EQUAL(chainman.GetAll().size(), 0); @@ -383,8 +379,8 @@ struct SnapshotTestSetup : TestChain100Setup { const ChainstateManager::Options chainman_opts{ .chainparams = ::Params(), .datadir = chainman.m_options.datadir, - .adjusted_time_callback = GetAdjustedTime, .notifications = *m_node.notifications, + .signals = m_node.validation_signals.get(), }; const BlockManager::Options blockman_opts{ .chainparams = chainman_opts.chainparams, @@ -411,7 +407,7 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_activate_snapshot, SnapshotTestSetup) //! - First, verify that setBlockIndexCandidates is as expected when using a single, //! fully-validating chainstate. //! -//! - Then mark a region of the chain BLOCK_ASSUMED_VALID and introduce a second chainstate +//! - Then mark a region of the chain as missing data and introduce a second chainstate //! that will tolerate assumed-valid blocks. Run LoadBlockIndex() and ensure that the first //! chainstate only contains fully validated blocks and the other chainstate contains all blocks, //! except those marked assume-valid, because those entries don't HAVE_DATA. @@ -422,7 +418,6 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) Chainstate& cs1 = chainman.ActiveChainstate(); int num_indexes{0}; - int num_assumed_valid{0}; // Blocks in range [assumed_valid_start_idx, last_assumed_valid_idx) will be // marked as assumed-valid and not having data. const int expected_assumed_valid{20}; @@ -457,35 +452,30 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager_loadblockindex, TestChain100Setup) reload_all_block_indexes(); BOOST_CHECK_EQUAL(cs1.setBlockIndexCandidates.size(), 1); - // Mark some region of the chain assumed-valid, and remove the HAVE_DATA flag. + // Reset some region of the chain's nStatus, removing the HAVE_DATA flag. for (int i = 0; i <= cs1.m_chain.Height(); ++i) { LOCK(::cs_main); auto index = cs1.m_chain[i]; - // Blocks with heights in range [91, 110] are marked ASSUMED_VALID + // Blocks with heights in range [91, 110] are marked as missing data. if (i < last_assumed_valid_idx && i >= assumed_valid_start_idx) { - index->nStatus = BlockStatus::BLOCK_VALID_TREE | BlockStatus::BLOCK_ASSUMED_VALID; + index->nStatus = BlockStatus::BLOCK_VALID_TREE; + index->nTx = 0; + index->nChainTx = 0; } ++num_indexes; - if (index->IsAssumedValid()) ++num_assumed_valid; // Note the last fully-validated block as the expected validated tip. if (i == (assumed_valid_start_idx - 1)) { validated_tip = index; - BOOST_CHECK(!index->IsAssumedValid()); } // Note the last assumed valid block as the snapshot base if (i == last_assumed_valid_idx - 1) { assumed_base = index; - BOOST_CHECK(index->IsAssumedValid()); - } else if (i == last_assumed_valid_idx) { - BOOST_CHECK(!index->IsAssumedValid()); } } - BOOST_CHECK_EQUAL(expected_assumed_valid, num_assumed_valid); - // Note: cs2's tip is not set when ActivateExistingSnapshot is called. Chainstate& cs2 = WITH_LOCK(::cs_main, return chainman.ActivateExistingSnapshot(*assumed_base->phashBlock)); diff --git a/src/test/validation_tests.cpp b/src/test/validation_tests.cpp index 14440571eb..93a884be6d 100644 --- a/src/test/validation_tests.cpp +++ b/src/test/validation_tests.cpp @@ -4,12 +4,17 @@ #include <chainparams.h> #include <consensus/amount.h> +#include <consensus/merkle.h> +#include <core_io.h> +#include <hash.h> #include <net.h> #include <signet.h> #include <uint256.h> #include <util/chaintype.h> #include <validation.h> +#include <string> + #include <test/util/setup_common.h> #include <boost/test/unit_test.hpp> @@ -145,4 +150,214 @@ BOOST_AUTO_TEST_CASE(test_assumeutxo) BOOST_CHECK_EQUAL(out110_2.nChainTx, 111U); } +BOOST_AUTO_TEST_CASE(block_malleation) +{ + // Test utilities that calls `IsBlockMutated` and then clears the validity + // cache flags on `CBlock`. + auto is_mutated = [](CBlock& block, bool check_witness_root) { + bool mutated{IsBlockMutated(block, check_witness_root)}; + block.fChecked = false; + block.m_checked_witness_commitment = false; + block.m_checked_merkle_root = false; + return mutated; + }; + auto is_not_mutated = [&is_mutated](CBlock& block, bool check_witness_root) { + return !is_mutated(block, check_witness_root); + }; + + // Test utilities to create coinbase transactions and insert witness + // commitments. + // + // Note: this will not include the witness stack by default to avoid + // triggering the "no witnesses allowed for blocks that don't commit to + // witnesses" rule when testing other malleation vectors. + auto create_coinbase_tx = [](bool include_witness = false) { + CMutableTransaction coinbase; + coinbase.vin.resize(1); + if (include_witness) { + coinbase.vin[0].scriptWitness.stack.resize(1); + coinbase.vin[0].scriptWitness.stack[0] = std::vector<unsigned char>(32, 0x00); + } + + coinbase.vout.resize(1); + coinbase.vout[0].scriptPubKey.resize(MINIMUM_WITNESS_COMMITMENT); + coinbase.vout[0].scriptPubKey[0] = OP_RETURN; + coinbase.vout[0].scriptPubKey[1] = 0x24; + coinbase.vout[0].scriptPubKey[2] = 0xaa; + coinbase.vout[0].scriptPubKey[3] = 0x21; + coinbase.vout[0].scriptPubKey[4] = 0xa9; + coinbase.vout[0].scriptPubKey[5] = 0xed; + + auto tx = MakeTransactionRef(coinbase); + assert(tx->IsCoinBase()); + return tx; + }; + auto insert_witness_commitment = [](CBlock& block, uint256 commitment) { + assert(!block.vtx.empty() && block.vtx[0]->IsCoinBase() && !block.vtx[0]->vout.empty()); + + CMutableTransaction mtx{*block.vtx[0]}; + CHash256().Write(commitment).Write(std::vector<unsigned char>(32, 0x00)).Finalize(commitment); + memcpy(&mtx.vout[0].scriptPubKey[6], commitment.begin(), 32); + block.vtx[0] = MakeTransactionRef(mtx); + }; + + { + CBlock block; + + // Empty block is expected to have merkle root of 0x0. + BOOST_CHECK(block.vtx.empty()); + block.hashMerkleRoot = uint256{1}; + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false)); + block.hashMerkleRoot = uint256{}; + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false)); + + // Block with a single coinbase tx is mutated if the merkle root is not + // equal to the coinbase tx's hash. + block.vtx.push_back(create_coinbase_tx()); + BOOST_CHECK(block.vtx[0]->GetHash() != block.hashMerkleRoot); + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false)); + block.hashMerkleRoot = block.vtx[0]->GetHash(); + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false)); + + // Block with two transactions is mutated if the merkle root does not + // match the double sha256 of the concatenation of the two transaction + // hashes. + block.vtx.push_back(MakeTransactionRef(CMutableTransaction{})); + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false)); + HashWriter hasher; + hasher.write(block.vtx[0]->GetHash()); + hasher.write(block.vtx[1]->GetHash()); + block.hashMerkleRoot = hasher.GetHash(); + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false)); + + // Block with two transactions is mutated if any node is duplicate. + { + block.vtx[1] = block.vtx[0]; + HashWriter hasher; + hasher.write(block.vtx[0]->GetHash()); + hasher.write(block.vtx[1]->GetHash()); + block.hashMerkleRoot = hasher.GetHash(); + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false)); + } + + // Blocks with 64-byte coinbase transactions are not considered mutated + block.vtx.clear(); + { + CMutableTransaction mtx; + mtx.vin.resize(1); + mtx.vout.resize(1); + mtx.vout[0].scriptPubKey.resize(4); + block.vtx.push_back(MakeTransactionRef(mtx)); + block.hashMerkleRoot = block.vtx.back()->GetHash(); + assert(block.vtx.back()->IsCoinBase()); + assert(GetSerializeSize(TX_NO_WITNESS(block.vtx.back())) == 64); + } + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false)); + } + + { + // Test merkle root malleation + + // Pseudo code to mine transactions tx{1,2,3}: + // + // ``` + // loop { + // tx1 = random_tx() + // tx2 = random_tx() + // tx3 = deserialize_tx(txid(tx1) || txid(tx2)); + // if serialized_size_without_witness(tx3) == 64 { + // print(hex(tx3)) + // break + // } + // } + // ``` + // + // The `random_tx` function used to mine the txs below simply created + // empty transactions with a random version field. + CMutableTransaction tx1; + BOOST_CHECK(DecodeHexTx(tx1, "ff204bd0000000000000", /*try_no_witness=*/true, /*try_witness=*/false)); + CMutableTransaction tx2; + BOOST_CHECK(DecodeHexTx(tx2, "8ae53c92000000000000", /*try_no_witness=*/true, /*try_witness=*/false)); + CMutableTransaction tx3; + BOOST_CHECK(DecodeHexTx(tx3, "cdaf22d00002c6a7f848f8ae4d30054e61dcf3303d6fe01d282163341f06feecc10032b3160fcab87bdfe3ecfb769206ef2d991b92f8a268e423a6ef4d485f06", /*try_no_witness=*/true, /*try_witness=*/false)); + { + // Verify that double_sha256(txid1||txid2) == txid3 + HashWriter hasher; + hasher.write(tx1.GetHash()); + hasher.write(tx2.GetHash()); + assert(hasher.GetHash() == tx3.GetHash()); + // Verify that tx3 is 64 bytes in size (without witness). + assert(GetSerializeSize(TX_NO_WITNESS(tx3)) == 64); + } + + CBlock block; + block.vtx.push_back(MakeTransactionRef(tx1)); + block.vtx.push_back(MakeTransactionRef(tx2)); + uint256 merkle_root = block.hashMerkleRoot = BlockMerkleRoot(block); + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/false)); + + // Mutate the block by replacing the two transactions with one 64-byte + // transaction that serializes into the concatenation of the txids of + // the transactions in the unmutated block. + block.vtx.clear(); + block.vtx.push_back(MakeTransactionRef(tx3)); + BOOST_CHECK(!block.vtx.back()->IsCoinBase()); + BOOST_CHECK(BlockMerkleRoot(block) == merkle_root); + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false)); + } + + { + CBlock block; + block.vtx.push_back(create_coinbase_tx(/*include_witness=*/true)); + { + CMutableTransaction mtx; + mtx.vin.resize(1); + mtx.vin[0].scriptWitness.stack.resize(1); + mtx.vin[0].scriptWitness.stack[0] = {0}; + block.vtx.push_back(MakeTransactionRef(mtx)); + } + block.hashMerkleRoot = BlockMerkleRoot(block); + // Block with witnesses is considered mutated if the witness commitment + // is not validated. + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/false)); + // Block with invalid witness commitment is considered mutated. + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/true)); + + // Block with valid commitment is not mutated + { + auto commitment{BlockWitnessMerkleRoot(block)}; + insert_witness_commitment(block, commitment); + block.hashMerkleRoot = BlockMerkleRoot(block); + } + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/true)); + + // Malleating witnesses should be caught by `IsBlockMutated`. + { + CMutableTransaction mtx{*block.vtx[1]}; + assert(!mtx.vin[0].scriptWitness.stack[0].empty()); + ++mtx.vin[0].scriptWitness.stack[0][0]; + block.vtx[1] = MakeTransactionRef(mtx); + } + // Without also updating the witness commitment, the merkle root should + // not change when changing one of the witnesses. + BOOST_CHECK(block.hashMerkleRoot == BlockMerkleRoot(block)); + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/true)); + { + auto commitment{BlockWitnessMerkleRoot(block)}; + insert_witness_commitment(block, commitment); + block.hashMerkleRoot = BlockMerkleRoot(block); + } + BOOST_CHECK(is_not_mutated(block, /*check_witness_root=*/true)); + + // Test malleating the coinbase witness reserved value + { + CMutableTransaction mtx{*block.vtx[0]}; + mtx.vin[0].scriptWitness.stack.resize(0); + block.vtx[0] = MakeTransactionRef(mtx); + block.hashMerkleRoot = BlockMerkleRoot(block); + } + BOOST_CHECK(is_mutated(block, /*check_witness_root=*/true)); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/validationinterface_tests.cpp b/src/test/validationinterface_tests.cpp index 5979441057..a46cfc3029 100644 --- a/src/test/validationinterface_tests.cpp +++ b/src/test/validationinterface_tests.cpp @@ -28,7 +28,7 @@ BOOST_AUTO_TEST_CASE(unregister_validation_interface_race) const CBlock block_dummy; BlockValidationState state_dummy; while (generate) { - GetMainSignals().BlockChecked(block_dummy, state_dummy); + m_node.validation_signals->BlockChecked(block_dummy, state_dummy); } }}; @@ -37,8 +37,8 @@ BOOST_AUTO_TEST_CASE(unregister_validation_interface_race) // keep going for about 1 sec, which is 250k iterations for (int i = 0; i < 250000; i++) { auto sub = std::make_shared<TestSubscriberNoop>(); - RegisterSharedValidationInterface(sub); - UnregisterSharedValidationInterface(sub); + m_node.validation_signals->RegisterSharedValidationInterface(sub); + m_node.validation_signals->UnregisterSharedValidationInterface(sub); } // tell the other thread we are done generate = false; @@ -52,8 +52,8 @@ BOOST_AUTO_TEST_CASE(unregister_validation_interface_race) class TestInterface : public CValidationInterface { public: - TestInterface(std::function<void()> on_call = nullptr, std::function<void()> on_destroy = nullptr) - : m_on_call(std::move(on_call)), m_on_destroy(std::move(on_destroy)) + TestInterface(ValidationSignals& signals, std::function<void()> on_call = nullptr, std::function<void()> on_destroy = nullptr) + : m_on_call(std::move(on_call)), m_on_destroy(std::move(on_destroy)), m_signals{signals} { } virtual ~TestInterface() @@ -64,14 +64,15 @@ public: { if (m_on_call) m_on_call(); } - static void Call() + void Call() { CBlock block; BlockValidationState state; - GetMainSignals().BlockChecked(block, state); + m_signals.BlockChecked(block, state); } std::function<void()> m_on_call; std::function<void()> m_on_destroy; + ValidationSignals& m_signals; }; // Regression test to ensure UnregisterAllValidationInterfaces calls don't @@ -80,17 +81,23 @@ public: BOOST_AUTO_TEST_CASE(unregister_all_during_call) { bool destroyed = false; - RegisterSharedValidationInterface(std::make_shared<TestInterface>( + auto shared{std::make_shared<TestInterface>( + *m_node.validation_signals, [&] { // First call should decrements reference count 2 -> 1 - UnregisterAllValidationInterfaces(); + m_node.validation_signals->UnregisterAllValidationInterfaces(); BOOST_CHECK(!destroyed); // Second call should not decrement reference count 1 -> 0 - UnregisterAllValidationInterfaces(); + m_node.validation_signals->UnregisterAllValidationInterfaces(); BOOST_CHECK(!destroyed); }, - [&] { destroyed = true; })); - TestInterface::Call(); + [&] { destroyed = true; })}; + m_node.validation_signals->RegisterSharedValidationInterface(shared); + BOOST_CHECK(shared.use_count() == 2); + shared->Call(); + BOOST_CHECK(shared.use_count() == 1); + BOOST_CHECK(!destroyed); + shared.reset(); BOOST_CHECK(destroyed); } |