diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 224f1fe301ac62..21b30a3fa5eca4 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -256,6 +256,7 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/checkqueue.cpp \ test/fuzz/coins_view.cpp \ test/fuzz/coinscache_sim.cpp \ + test/fuzz/compression_roundtrip.cpp \ test/fuzz/connman.cpp \ test/fuzz/crypto.cpp \ test/fuzz/crypto_aes256.cpp \ @@ -342,6 +343,8 @@ test_fuzz_fuzz_SOURCES = \ test/fuzz/timedata.cpp \ test/fuzz/torcontrol.cpp \ test/fuzz/transaction.cpp \ + test/fuzz/tx_compression.cpp \ + test/fuzz/tx_decompression.cpp \ test/fuzz/tx_in.cpp \ test/fuzz/tx_out.cpp \ test/fuzz/tx_pool.cpp \ diff --git a/src/test/fuzz/compression_roundtrip.cpp b/src/test/fuzz/compression_roundtrip.cpp new file mode 100644 index 00000000000000..8965ecf594856a --- /dev/null +++ b/src/test/fuzz/compression_roundtrip.cpp @@ -0,0 +1,467 @@ +// Copyright (c) 2019-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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +using node::BlockManager; +using node::BlockAssembler; +using node::CBlockTemplate; +using node::GetTransaction; +using node::RegenerateCommitments; +using node::FindCoins; + +namespace { + class SecpContext { + secp256k1_context* ctx; + + public: + SecpContext() { + ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); + } + ~SecpContext() { + secp256k1_context_destroy(ctx); + } + secp256k1_context* GetContext() { + return ctx; + } + }; + struct CompressionRoundtripFuzzTestingSetup : public TestChain100Setup { + CompressionRoundtripFuzzTestingSetup(const ChainType& chain_type, const std::vector& extra_args) : TestChain100Setup{chain_type, extra_args} + {} + + CCompressedTxId GetCompressedTxId(uint256 txid, CBlock block) { + ChainstateManager& chainman = EnsureChainman(m_node); + Chainstate& active_chainstate = chainman.ActiveChainstate(); + BlockManager* blockman = &active_chainstate.m_blockman; + + const CBlockIndex* pindex{nullptr}; + { + LOCK(cs_main); + pindex = blockman->LookupBlockIndex(block.GetHash()); + } + assert(pindex); + uint32_t block_height = pindex->nHeight; + const auto& optional_block_index = block.LookupTransactionIndex(txid); + assert(optional_block_index.has_value()); + return CCompressedTxId(block_height, optional_block_index.value()); + } + + void GetCoins(std::map& map) { + FindCoins(m_node, map); + } + + bool IsTopBlock(CBlock block) { + LOCK(cs_main); + if (m_node.chainman->ActiveChain().Tip()->GetBlockHash() == block.GetHash()) return true; + return false; + } + + CScript GenerateDestination(secp256k1_context* ctx, secp256k1_keypair kp, int scriptInt, std::vector custom_script_bytes){ + secp256k1_pubkey pubkey; + assert(secp256k1_keypair_pub(ctx, &pubkey, &kp)); + std::vector> vSolutions; + TxoutType scriptType = TxoutType::NONSTANDARD; + bool pc = true; + //TODO: use scripthashes + switch(scriptInt) { + case 0: + scriptType = TxoutType::PUBKEY; + break; + case 1: + scriptType = TxoutType::PUBKEYHASH; + break; + case 2: + //scriptType = TxoutType::SCRIPTHASH; + scriptType = TxoutType::PUBKEYHASH; + break; + case 3: + scriptType = TxoutType::WITNESS_V0_KEYHASH; + break; + case 4: + //scriptType = TxoutType::WITNESS_V0_SCRIPTHASH; + scriptType = TxoutType::PUBKEYHASH; + break; + case 5: + scriptType = TxoutType::WITNESS_V1_TAPROOT; + break; + case 6: + scriptType = TxoutType::PUBKEYHASH; + pc = false; + break; + } + + if (scriptType == TxoutType::PUBKEY) { + std::vector ucPubkey (65); + size_t ucSize = 65; + secp256k1_ec_pubkey_serialize(ctx, &ucPubkey[0], &ucSize, &pubkey, SECP256K1_EC_UNCOMPRESSED); + vSolutions.push_back(ucPubkey); + } else if (scriptType == TxoutType::PUBKEYHASH) { + if (pc) { + std::vector cPubkey (33); + size_t cSize = 33; + secp256k1_ec_pubkey_serialize(ctx, &cPubkey[0], &cSize, &pubkey, SECP256K1_EC_COMPRESSED); + uint160 cpHash; + CHash160().Write(cPubkey).Finalize(cpHash); + std::vector cpHashBytes(20); + copy(cpHash.begin(), cpHash.end(), cpHashBytes.begin()); + vSolutions.push_back(cpHashBytes); + } else { + std::vector ucPubkey (65); + size_t ucSize = 65; + secp256k1_ec_pubkey_serialize(ctx, &ucPubkey[0], &ucSize, &pubkey, SECP256K1_EC_UNCOMPRESSED); + uint160 ucpHash; + CHash160().Write(ucPubkey).Finalize(ucpHash); + std::vector ucpHashBytes(20); + copy(ucpHash.begin(), ucpHash.end(), ucpHashBytes.begin()); + vSolutions.push_back(ucpHashBytes); + } + } else if (scriptType == TxoutType::WITNESS_V0_SCRIPTHASH || scriptType == TxoutType::SCRIPTHASH) { + std::vector cPubkey (33); + size_t cSize = 33; + secp256k1_ec_pubkey_serialize(ctx, &cPubkey[0], &cSize, &pubkey, SECP256K1_EC_COMPRESSED); + uint160 cpHash; + CHash160().Write(cPubkey).Finalize(cpHash); + std::vector cpHashBytes(20); + copy(cpHash.begin(), cpHash.end(), cpHashBytes.begin()); + std::vector> vSolutionsTemp; + vSolutionsTemp.push_back(cpHashBytes); + CTxDestination destination; + BuildDestination(vSolutionsTemp, scriptType, destination); + CScript p2pkh = GetScriptForDestination(destination); + uint160 scriptHash; + CHash160().Write(p2pkh).Finalize(scriptHash); + std::vector script(20); + copy(scriptHash.begin(), scriptHash.end(), script.begin()); + vSolutions.push_back(script); + } else if (scriptType == TxoutType::WITNESS_V0_KEYHASH) { + std::vector cPubkey (33); + size_t cSize = 33; + secp256k1_ec_pubkey_serialize(ctx, &cPubkey[0], &cSize, &pubkey, SECP256K1_EC_COMPRESSED); + uint160 cpHash; + CHash160().Write(cPubkey).Finalize(cpHash); + std::vector cpHashBytes(20); + copy(cpHash.begin(), cpHash.end(), cpHashBytes.begin()); + vSolutions.push_back(cpHashBytes); + } else if (scriptType == TxoutType::WITNESS_V1_TAPROOT) { + /* Serilize XOnly Pubkey */ + secp256k1_xonly_pubkey xonly_pubkey; + assert(secp256k1_xonly_pubkey_from_pubkey(ctx, &xonly_pubkey, NULL, &pubkey)); + std::vector xonly_pubkey_bytes (32); + secp256k1_xonly_pubkey_serialize(ctx, &xonly_pubkey_bytes[0], &xonly_pubkey); + + /* Construct Script */ + std::vector taprootScript(32); + copy(xonly_pubkey_bytes.begin(), xonly_pubkey_bytes.end(), taprootScript.begin()); + vSolutions.push_back(taprootScript); + } else if (scriptType == TxoutType::NONSTANDARD) { + vSolutions.push_back(custom_script_bytes); + } + CTxDestination destination; + BuildDestination(vSolutions, scriptType, destination); + return GetScriptForDestination(destination); + } + + bool SignTransaction(secp256k1_context* ctx, CMutableTransaction& mtx, std::vector kps) { + FillableSigningProvider keystore; + int kps_length = kps.size(); + for (int kps_index = 0; kps_index < kps_length; kps_index++) { + std::vector secret_key(32); + if (!secp256k1_keypair_sec(ctx, &secret_key[0], &kps.at(kps_index))) return false; + CKey key; + key.Set(secret_key.begin(), secret_key.end(), true); + keystore.AddKey(key); + if (!key.IsValid()) return false; + CKey key2; + key2.Set(secret_key.begin(), secret_key.end(), false); + keystore.AddKey(key2); + if (!key2.IsValid()) return false; + } + + // Fetch previous transactions (inputs): + std::map coins; + for (const CTxIn& txin : mtx.vin) { + coins[txin.prevout]; // Create empty map entry keyed by prevout. + } + FindCoins(m_node, coins); + + // Parse the prevtxs array + //ParsePrevouts(NULL, &keystore, coins); + + std::map input_errors; + ::SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors); + assert(input_errors.size() == 0); + return true; + } + bool InitTXIndex() { + g_txindex = std::make_unique(interfaces::MakeChain(m_node), m_cache_sizes.tx_index, false, node::fReindex); + if (!g_txindex->Start()) { + return false; + } + return true; + } + }; + secp256k1_context* ctx = nullptr; + SecpContext secp_context = SecpContext(); + CompressionRoundtripFuzzTestingSetup* rpc = nullptr; + std::vector> unspent_transactions; + std::vector> coinbase_transactions; + CompressionRoundtripFuzzTestingSetup* InitializeCompressionRoundtripFuzzTestingSetup() + { + static const auto setup = MakeNoLogFileContext(); + SetRPCWarmupFinished(); + return setup.get(); + } +}; + +void compression_roundtrip_initialize() +{ + SelectParams(ChainType::REGTEST); + rpc = InitializeCompressionRoundtripFuzzTestingSetup(); + ctx = secp_context.GetContext(); + assert(rpc->InitTXIndex()); + FastRandomContext frandom_ctx(true); + + //Generate Coinbase Transactions For Future Inputs + for (int i = 0; i < 100; i++) { + secp256k1_keypair coinbase_kp; + std::vector secret_key = frandom_ctx.randbytes(32); + assert(secp256k1_keypair_create(ctx, &coinbase_kp, &secret_key[0])); + + int script_type = frandom_ctx.randrange(7); + std::vector custom_script_bytes = frandom_ctx.randbytes(128); + CScript coinbase_scriptPubKey = rpc->GenerateDestination(ctx, coinbase_kp, script_type, custom_script_bytes); + + std::vector txins; + CBlock coinbase_block = rpc->CreateAndProcessBlock(txins, coinbase_scriptPubKey); + assert(rpc->IsTopBlock(coinbase_block)); + coinbase_transactions.push_back(std::make_tuple(coinbase_block.vtx.at(0)->GetHash(), coinbase_kp, 0, coinbase_block.vtx.at(0)->vout.at(0).nValue)); + } + + //Generate Coinbase Transactions For Compression Inputs + for (int i = 0; i < 100; i++) { + secp256k1_keypair coinbase_kp; + std::vector secret_key = frandom_ctx.randbytes(32); + assert(secp256k1_keypair_create(ctx, &coinbase_kp, &secret_key[0])); + int script_type = frandom_ctx.randrange(7); + std::vector custom_script_bytes = frandom_ctx.randbytes(128); + CScript coinbase_scriptPubKey = rpc->GenerateDestination(ctx, coinbase_kp, script_type, custom_script_bytes); + + std::vector txins; + CBlock coinbase_block = rpc->CreateAndProcessBlock(txins, coinbase_scriptPubKey); + assert(rpc->IsTopBlock(coinbase_block)); + uint256 txid = coinbase_block.vtx.at(0)->GetHash(); + unspent_transactions.push_back(std::make_tuple(txid, coinbase_kp, 0, coinbase_block.vtx.at(0)->vout.at(0).nValue, coinbase_scriptPubKey, txid, rpc->GetCompressedTxId(txid, coinbase_block))); + } + + //Generate Transactions For Compression Inputs + int coinbase_length = coinbase_transactions.size(); + for (int coinbase_index = 0; coinbase_index < coinbase_length; coinbase_index++) { + uint256 coinbase_txid = std::get<0>(coinbase_transactions.at(coinbase_index)); + secp256k1_keypair coinbase_kp = std::get<1>(coinbase_transactions.at(coinbase_index)); + uint32_t coinbase_vout = std::get<2>(coinbase_transactions.at(coinbase_index)); + int coinbase_amount = std::get<3>(coinbase_transactions.at(coinbase_index)); + + CMutableTransaction mtx; + mtx.nVersion = 0; + mtx.nLockTime = 0; + + CTxIn in; + in.prevout = COutPoint{coinbase_txid, coinbase_vout}; + in.nSequence = 0; + mtx.vin.push_back(in); + + int index = 0; + std::vector> outs; + uint32_t remaining_amount = coinbase_amount; + LIMITED_WHILE(remaining_amount > 2000, 10000) { + CTxOut out; + + uint32_t amount = frandom_ctx.randrange(remaining_amount-1000)+1; + remaining_amount -= amount; + out.nValue = amount; + + secp256k1_keypair out_kp; + std::vector secret_key = frandom_ctx.randbytes(32); + assert(secp256k1_keypair_create(ctx, &out_kp, &secret_key[0])); + + int script_type = frandom_ctx.randrange(7); + std::vector custom_script_bytes = frandom_ctx.randbytes(128); + out.scriptPubKey = rpc->GenerateDestination(ctx, out_kp, script_type, custom_script_bytes); + mtx.vout.push_back(out); + outs.push_back(std::make_tuple(out_kp, index, amount, out.scriptPubKey)); + index++; + } + + assert(mtx.vout.size() != 0); + assert(rpc->SignTransaction(ctx, mtx, {coinbase_kp})); + + std::vector secret_key = frandom_ctx.randbytes(32); + secp256k1_keypair main_kp; + assert(secp256k1_keypair_create(ctx, &main_kp, &secret_key[0])); + + CScript main_scriptPubKey; + + int script_type = frandom_ctx.randrange(7); + std::vector custom_script_bytes = frandom_ctx.randbytes(128); + main_scriptPubKey = rpc->GenerateDestination(ctx, main_kp, script_type, custom_script_bytes); + + std::vector txins; + txins.push_back(mtx); + CBlock main_block = rpc->CreateAndProcessBlock(txins, main_scriptPubKey); + assert(rpc->IsTopBlock(main_block)); + for (auto const& out : outs) { + uint256 txid = mtx.GetHash(); + unspent_transactions.push_back(std::make_tuple(txid, std::get<0>(out), std::get<1>(out), std::get<2>(out), std::get<3>(out), txid, rpc->GetCompressedTxId(txid, main_block))); + } + } +} + +FUZZ_TARGET_INIT(compression_roundtrip, compression_roundtrip_initialize) +{ + FuzzedDataProvider fdp(buffer.data(), buffer.size()); + std::vector keypairs; + CMutableTransaction mtx; + + mtx.nVersion = fdp.ConsumeIntegral(); + if (fdp.ConsumeBool()) { + mtx.nLockTime = fdp.ConsumeIntegral(); + } else { + mtx.nLockTime = fdp.ConsumeIntegral(); + } + + uint32_t total = 0; + std::vector input_scripts; + std::vector txids; + std::vector compressed_txids; + std::vector used_indexs; + + + // GENERATE INPUTS // + LIMITED_WHILE(total == 0 || fdp.ConsumeBool(), static_cast(unspent_transactions.size()-1)) { + int index = fdp.ConsumeIntegralInRange(0, unspent_transactions.size()-1); + if (std::find(used_indexs.begin(), used_indexs.end(), index) != used_indexs.end()) + break; + used_indexs.push_back(index); + uint256 txid = std::get<0>(unspent_transactions.at(index)); + keypairs.push_back(std::get<1>(unspent_transactions.at(index))); + uint32_t vout = std::get<2>(unspent_transactions.at(index)); + input_scripts.push_back(std::get<4>(unspent_transactions.at(index))); + txids.push_back(std::get<5>(unspent_transactions.at(index))); + compressed_txids.push_back(std::get<6>(unspent_transactions.at(index))); + + total += std::get<3>(unspent_transactions.at(index)); + + CTxIn in; + in.prevout = COutPoint{txid, vout}; + in.nSequence = fdp.ConsumeIntegral(); + mtx.vin.push_back(in); + } + + + // GENERATE OUTPUTS // + uint32_t remaining_amount = total; + bool sign = true; + LIMITED_WHILE(remaining_amount > 2000 && (fdp.ConsumeBool() || mtx.vout.size() == 0), 10000) { + CTxOut out; + uint32_t limit = pow(2, 16); + int range_amount; + if (remaining_amount > limit) { + range_amount = limit-1000; + } else { + range_amount = remaining_amount-1000; + } + uint16_t amount = fdp.ConsumeIntegralInRange(1, range_amount); + remaining_amount -= amount; + out.nValue = amount; + + std::vector secret_key = fdp.ConsumeBytes(32); + if (secret_key.size() != 32) return; + secp256k1_keypair out_kp; + if (!secp256k1_keypair_create(ctx, &out_kp, &secret_key[0])) return; + + CScript out_scriptPubKey; + + int script_type = fdp.ConsumeIntegralInRange(0, 7); + if (script_type == 7) + sign = false; + std::vector custom_script_bytes = fdp.ConsumeBytes(128); + out.scriptPubKey = rpc->GenerateDestination(ctx, out_kp, script_type, custom_script_bytes); + mtx.vout.push_back(out); + } + if (mtx.vout.size() == 0) return; + + if (sign) + assert(rpc->SignTransaction(ctx, mtx, keypairs)); + + + const CTransaction tx = CTransaction(mtx); + CCompressedTransaction compressed_transaction = CCompressedTransaction(ctx, tx, compressed_txids, input_scripts); + + CDataStream stream(SER_DISK, 0); + compressed_transaction.Serialize(stream); + CCompressedTransaction uct = CCompressedTransaction(deserialize, stream); + assert(compressed_transaction == uct); + + std::map coins; + for (size_t index = 0; index < uct.vin().size(); index++) { + coins[COutPoint(txids.at(index), uct.vin().at(index).prevout().n())]; // Create empty map entry keyed by prevout. + } + rpc->GetCoins(coins); + std::vector outs; + for (size_t index = 0; index < uct.vin().size(); index++) { + outs.push_back(coins[COutPoint(txids.at(index), uct.vin().at(index).prevout().n())].out); + } + CTransaction new_tx = CTransaction(CMutableTransaction(ctx, uct, txids, outs)); + assert(tx == new_tx); +} diff --git a/src/test/fuzz/tx_compression.cpp b/src/test/fuzz/tx_compression.cpp new file mode 100644 index 00000000000000..034c8f41d4fe66 --- /dev/null +++ b/src/test/fuzz/tx_compression.cpp @@ -0,0 +1,96 @@ +// Copyright (c) 2019-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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace { + struct TXCompressionFuzzTestingSetup : public TestingSetup { + TXCompressionFuzzTestingSetup(const ChainType& chain_type, const std::vector& extra_args) : TestingSetup{chain_type, extra_args} + {} + + UniValue CallRPC(const std::string& rpc_method, const std::vector& arguments) + { + JSONRPCRequest request; + request.context = &m_node; + request.strMethod = rpc_method; + try { + request.params = RPCConvertValues(rpc_method, arguments); + } catch (const std::runtime_error&) { + return NullUniValue; + } + return tableRPC.execute(request); + } + }; + + TXCompressionFuzzTestingSetup* rpc = nullptr; + TXCompressionFuzzTestingSetup* InitializeTXCompressionFuzzTestingSetup() + { + static const auto setup = MakeNoLogFileContext(); + SetRPCWarmupFinished(); + return setup.get(); + } +}; + +void tx_compression_initialize() +{ + rpc = InitializeTXCompressionFuzzTestingSetup(); +} + +FUZZ_TARGET_INIT(tx_compression, tx_compression_initialize) +{ + FuzzedDataProvider fdp(buffer.data(), buffer.size()); + CMutableTransaction mtx; + mtx = ConsumeTransaction(fdp, {}, 100, 100); + std::vector arguments; + std::string rpc_method = "compressrawtransaction"; + arguments.push_back(EncodeHexTx(CTransaction(mtx))); + try { + rpc->CallRPC(rpc_method, arguments); + } catch (const UniValue& json_rpc_error) { + const std::string error_msg{json_rpc_error.find_value("message").get_str()}; + // Once c++20 is allowed, starts_with can be used. + // if (error_msg.starts_with("Internal bug detected")) { + if (0 == error_msg.rfind("Internal bug detected", 0)) { + // Only allow the intentional internal bug + assert(error_msg.find("trigger_internal_bug") != std::string::npos); + } + } +} diff --git a/src/test/fuzz/tx_decompression.cpp b/src/test/fuzz/tx_decompression.cpp new file mode 100644 index 00000000000000..8fb0a5354dc75e --- /dev/null +++ b/src/test/fuzz/tx_decompression.cpp @@ -0,0 +1,73 @@ +// Copyright (c) 2019-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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + struct TXDecompressionFuzzTestingSetup : public TestingSetup { + TXDecompressionFuzzTestingSetup(const ChainType& chain_type, const std::vector& extra_args) : TestingSetup{chain_type, extra_args} + {} + + UniValue CallRPC(const std::string& rpc_method, const std::vector& arguments) + { + JSONRPCRequest request; + request.context = &m_node; + request.strMethod = rpc_method; + try { + request.params = RPCConvertValues(rpc_method, arguments); + } catch (const std::runtime_error&) { + return NullUniValue; + } + return tableRPC.execute(request); + } + }; + + TXDecompressionFuzzTestingSetup* rpc = nullptr; + TXDecompressionFuzzTestingSetup* InitializeTXDecompressionFuzzTestingSetup() + { + static const auto setup = MakeNoLogFileContext(); + SetRPCWarmupFinished(); + return setup.get(); + } +}; + +void tx_decompression_initialize() +{ + rpc = InitializeTXDecompressionFuzzTestingSetup(); +} + +FUZZ_TARGET_INIT(tx_decompression, tx_decompression_initialize) +{ + const std::string tx_hex = HexStr(buffer); + std::vector arguments; + std::string rpc_method = "decompressrawtransaction"; + arguments.push_back(tx_hex); + try { + rpc->CallRPC(rpc_method, arguments); + } catch (const UniValue& json_rpc_error) { + const std::string error_msg{json_rpc_error.find_value("message").get_str()}; + // Once c++20 is allowed, starts_with can be used. + // if (error_msg.starts_with("Internal bug detected")) { + if (0 == error_msg.rfind("Internal bug detected", 0)) { + // Only allow the intentional internal bug + assert(error_msg.find("trigger_internal_bug") != std::string::npos); + } + } +}