From 79b1bf84dce50c9e2e41a3118cc2f81dd387c081 Mon Sep 17 00:00:00 2001 From: Tom Briar Date: Tue, 11 Jul 2023 16:56:25 -0400 Subject: [PATCH] fuzz: Added Fuzz tests for Compressed Transactions --- src/Makefile.test.include | 1 + src/test/fuzz/compression_roundtrip.cpp | 381 ++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 src/test/fuzz/compression_roundtrip.cpp diff --git a/src/Makefile.test.include b/src/Makefile.test.include index 416a11b0c01127..e0ad7a98c90200 100644 --- a/src/Makefile.test.include +++ b/src/Makefile.test.include @@ -262,6 +262,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 \ diff --git a/src/test/fuzz/compression_roundtrip.cpp b/src/test/fuzz/compression_roundtrip.cpp new file mode 100644 index 00000000000000..c92e9a477be833 --- /dev/null +++ b/src/test/fuzz/compression_roundtrip.cpp @@ -0,0 +1,381 @@ +// 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 + +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 Location { + secp256k1_keypair keypair; + CScript scriptPubKey; + CScript redeemScript; + }; + struct UTXO { + uint256 txid; + uint32_t vout; + Location location; + CAmount nValue; + bool signable; + }; + struct CompressionRoundtripFuzzTestingSetup : public TestChain100Setup { + CompressionRoundtripFuzzTestingSetup(const ChainType& chain_type, const std::vector& extra_args) : TestChain100Setup{chain_type, extra_args} + {} + + std::vector GetSerializedPubKey(secp256k1_pubkey pubkey, bool compressed) { + size_t p_size = 33; + auto p_type = SECP256K1_EC_COMPRESSED; + if (!compressed) { + p_size = 65; + p_type = SECP256K1_EC_UNCOMPRESSED; + } + std::vector p_vec(p_size); + secp256k1_ec_pubkey_serialize(secp256k1_context_static, p_vec.data(), &p_size, &pubkey, p_type); + return p_vec; + }; + std::tuple> CompressOutPoints(CTransaction tx) { + LOCK(cs_main); + std::vector warnings; + return EnsureChainman(m_node).ActiveChainstate().CompressOutPoints(tx, warnings); + } + + std::tuple, std::vector> UncompressOutPoints(CCompressedTransaction tx) { + LOCK(cs_main); + std::vector prevouts = EnsureChainman(m_node).ActiveChainstate().UncompressOutPoints(tx); + std::vector coins = FindCoins(m_node, prevouts); + std::vector outs; + for (auto& coin : coins) { + outs.push_back(coin.out); + } + return std::make_tuple(prevouts, outs); + } + + //Get Block Hash instead of the block + bool IsTopBlock(CBlock block) { + LOCK(cs_main); + return m_node.chainman->ActiveChain().Tip()->GetBlockHash() == block.GetHash(); + } + + Location DeriveScript(int scriptType, secp256k1_pubkey pubkey) { + switch (scriptType) { + case 0: //Uncompressed PUBKEY + case 1: { //Compressed PUBKEY + bool compressed = static_cast(scriptType); + CScript scriptPubKey; + scriptPubKey << GetSerializedPubKey(pubkey, compressed); + scriptPubKey << opcodetype::OP_CHECKSIG; + return Location{secp256k1_keypair(), scriptPubKey, CScript()}; + } + case 2: //Uncompressed PUBKEYHASH + case 3: { //Compressed PUBKEYHASH + bool compressed = static_cast(scriptType-2); + return Location{secp256k1_keypair(), GetScriptForDestination(PKHash(CPubKey(GetSerializedPubKey(pubkey, compressed)))), CScript()}; + } + case 4: { //WITNESS_V0_KEYHASH + return Location{secp256k1_keypair(), GetScriptForDestination(WitnessV0KeyHash(CPubKey(GetSerializedPubKey(pubkey, true)))), CScript()}; + } + case 5: { //WITNESS_V1_TAPROOT + secp256k1_xonly_pubkey xonlyPubkey; + assert(secp256k1_xonly_pubkey_from_pubkey(secp256k1_context_static, &xonlyPubkey, NULL, &pubkey)); + std::vector xonlyPubkeyBytes(32); + secp256k1_xonly_pubkey_serialize(secp256k1_context_static, &xonlyPubkeyBytes[0], &xonlyPubkey); + return Location{secp256k1_keypair(), GetScriptForDestination(WitnessV1Taproot(XOnlyPubKey(xonlyPubkeyBytes))), CScript()}; + } + case 6: //SCRIPTHASH(Uncompressed PUBKEYHASH) + case 7: //SCRIPTHASH(Compressed PUBKEYHASH) + case 8: { //SCRIPTHASH(WITNESS_V0_KEYHASH) + CScript redeemScript = DeriveScript((scriptType%3)+2, pubkey).scriptPubKey; + return Location{secp256k1_keypair(), GetScriptForDestination(ScriptHash(redeemScript)), redeemScript}; + } + case 9: {//WITNESS_V0_SCRIPTHASH(Compressed PUBKEYHASH) + CScript redeemScript = DeriveScript(3, pubkey).scriptPubKey; + return Location{secp256k1_keypair(), GetScriptForDestination(WitnessV0ScriptHash(redeemScript)), redeemScript}; + } + default: + assert(false); + } + } + + bool GenerateLocation(secp256k1_context *ctx, std::function>(int)> getrandbytes, Location& location, bool sign = true) { + secp256k1_keypair keypair; + auto optionRandBytes = getrandbytes(32); + if (!optionRandBytes.has_value()) return false; + std::vector randBytes = optionRandBytes.value(); + + if (!sign) { + location.scriptPubKey = CScript(randBytes.begin(), randBytes.end()); + return true; + } + + if (!secp256k1_keypair_create(ctx, &keypair, randBytes.data())) return false; + secp256k1_pubkey pubkey; + assert(secp256k1_keypair_pub(secp256k1_context_static, &pubkey, &keypair)); + + auto optionRandByte = getrandbytes(1); + if (!optionRandBytes.has_value()) return false; + int scriptType = optionRandBytes.value()[0]%10; + location = DeriveScript(scriptType, pubkey); + location.keypair = keypair; + return true; + } + + bool SignTransaction(CMutableTransaction& mtx, const std::vector& keypairs, const std::vector& redeemScripts) { + FillableSigningProvider keystore; + for (const secp256k1_keypair& keypair : keypairs) { + std::vector secret_key(32); + if (!secp256k1_keypair_sec(secp256k1_context_static, &secret_key[0], &keypair)) 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; + } + + for (const CScript& redeemScript: redeemScripts) { + keystore.AddCScript(redeemScript); + keystore.AddCScript(GetScriptForDestination(WitnessV0ScriptHash(redeemScript))); + } + std::map coins; + for (const CTxIn& txin : mtx.vin) { + coins[txin.prevout]; + } + FindCoins(m_node, coins); + + std::map input_errors; + ::SignTransaction(mtx, &keystore, coins, SIGHASH_ALL, input_errors); + return input_errors.size() == 0; + } + }; + secp256k1_context* ctx = nullptr; + SecpContext secp_context = SecpContext(); + CompressionRoundtripFuzzTestingSetup* fuzz_ctx = nullptr; + std::vector> savings; + + std::vector unspent_transactions; + std::vector coinbase_transactions; + CompressionRoundtripFuzzTestingSetup* InitializeCompressionRoundtripFuzzTestingSetup() + { + static const auto setup = MakeNoLogFileContext(); + return setup.get(); + } +}; + +void compression_roundtrip_initialize() +{ + SelectParams(ChainType::REGTEST); + fuzz_ctx = InitializeCompressionRoundtripFuzzTestingSetup(); + ctx = secp_context.GetContext(); + FastRandomContext frandom_ctx{uint256{125}}; + auto getrandbytes = [&frandom_ctx](int len) -> std::optional> { return frandom_ctx.randbytes(len);}; + + //Create 300 coinbase transactions, Ignore the first hundred, Push the second hundred UTXOs to coinbase_transactions, and add the final hundred to the unspent_transaction to be used in the fuzz test + for (int i = 0; i < 300; i++) { + Location location; + assert(fuzz_ctx->GenerateLocation(ctx, getrandbytes, location)); + + CBlock coinbase_block = fuzz_ctx->CreateAndProcessBlock({}, location.scriptPubKey); + assert(fuzz_ctx->IsTopBlock(coinbase_block)); + UTXO transaction = UTXO{coinbase_block.vtx.at(0)->GetHash(), 0, location, coinbase_block.vtx.at(0)->vout.at(0).nValue, true}; + if (i > 100) { + if (i > 200) { + unspent_transactions.push_back(transaction); + } else { + coinbase_transactions.push_back(transaction); + } + } + } + + //Loop through the coinbase_transactions to assemble a bunch of random valued UTXOs for the fuzz test + for (const auto& cbtx : coinbase_transactions) { + + CMutableTransaction mtx; + mtx.nVersion = 0; + mtx.nLockTime = 0; + + CTxIn in; + in.prevout = COutPoint{Txid::FromUint256(cbtx.txid), cbtx.vout}; + in.nSequence = 0; + mtx.vin.push_back(in); + + uint32_t index = 0; + std::vector partial_transactions; + uint32_t remaining_amount = cbtx.nValue; + //Loop through the value of the coinbase transaction to create random valued outputs + LIMITED_WHILE(remaining_amount > 2000, 10000) { + uint32_t amount = frandom_ctx.randrange(remaining_amount-1000)+1; + remaining_amount -= amount; + + bool sign = frandom_ctx.randbool(); + Location location; + assert(fuzz_ctx->GenerateLocation(ctx, getrandbytes, location, sign)); + + CTxOut out; + out.nValue = amount; + out.scriptPubKey = location.scriptPubKey; + mtx.vout.push_back(out); + + partial_transactions.push_back(UTXO{uint256{0}, index, location, amount, sign}); + index++; + } + assert(mtx.vout.size() != 0); + assert(fuzz_ctx->SignTransaction(mtx, {cbtx.location.keypair}, {cbtx.location.redeemScript})); + //Add random valued UTXOs to the unspent_transactions after adding its txid + uint256 txid = mtx.GetHash(); + for (auto &ptx : partial_transactions) { + ptx.txid = txid; + unspent_transactions.push_back(ptx); + } + + Location location; + assert(fuzz_ctx->GenerateLocation(ctx, getrandbytes, location)); + CScript main_scriptPubKey = location.scriptPubKey; + CBlock main_block = fuzz_ctx->CreateAndProcessBlock({mtx}, main_scriptPubKey); + assert(fuzz_ctx->IsTopBlock(main_block)); + //Add the resulting coinbase transaction to the unspent_transactions for the fuzz test + unspent_transactions.push_back(UTXO{main_block.vtx[0]->GetHash(), 0, location, main_block.vtx[0]->vout[0].nValue, true}); + } +} + +FUZZ_TARGET(compression_roundtrip, .init=compression_roundtrip_initialize) +{ + FuzzedDataProvider fdp(buffer.data(), buffer.size()); + auto getrandbytes = [&fdp](int len) -> std::optional> { + std::vector data = fdp.ConsumeBytes(len); + if (data.size() != (size_t)len) return {}; + return data; + }; + + //Create the transaction to be compressed + CMutableTransaction mtx; + mtx.nVersion = fdp.ConsumeIntegral(); + mtx.nLockTime = fdp.ConsumeIntegral(); + + //Generate and add inputs to the Transaction + int64_t total = 0; + std::vector keypairs; + std::vector redeemScripts; + std::vector used_indexs; + bool sign_all = true; + + 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); + UTXO tx = unspent_transactions[index]; + + keypairs.push_back(tx.location.keypair); + redeemScripts.push_back(tx.location.redeemScript); + total += static_cast(tx.nValue); + if (!tx.signable) sign_all = false; + + CTxIn in; + in.prevout = COutPoint{Txid::FromUint256(tx.txid), tx.vout}; + in.nSequence = fdp.ConsumeIntegral(); + mtx.vin.push_back(in); + } + + + //Generate and add outputs to the transaction based on the inputs with random output values + int64_t remaining_amount = total; + LIMITED_WHILE(remaining_amount > 2000 && (fdp.ConsumeBool() || mtx.vout.size() == 0), 10000) { + CTxOut out; + if (sign_all) { + int64_t limit = pow(2, 16); + int range_amount; + if (remaining_amount > limit) { + range_amount = limit-1000; + } else { + range_amount = remaining_amount-1000; + } + int64_t amount = fdp.ConsumeIntegralInRange(1, range_amount); + remaining_amount -= amount; + out.nValue = amount; + } else { + out.nValue = fdp.ConsumeIntegral(); + } + Location location; + if (!fuzz_ctx->GenerateLocation(ctx, getrandbytes, location)) return; + out.scriptPubKey = location.scriptPubKey; + mtx.vout.push_back(out); + } + if (mtx.vout.size() == 0) return; + + //If all randomly chosen inputs were signable sign the transaction + if (sign_all) { + assert(fuzz_ctx->SignTransaction(mtx, keypairs, redeemScripts)); + } else { + fuzz_ctx->SignTransaction(mtx, keypairs, redeemScripts); + } + + const CTransaction tx = CTransaction(mtx); + //Compressed OutPoints for Inputs + std::tuple> cinputstup = fuzz_ctx->CompressOutPoints(tx); + //Compress Transaction + CCompressedTransaction compressed_transaction = CCompressedTransaction(tx, std::get<0>(cinputstup), std::get<1>(cinputstup)); + + //Serialize Transaction + DataStream stream; + compressed_transaction.Serialize(stream); + + //Deserialize Transaction + CCompressedTransaction uct = CCompressedTransaction(deserialize, stream); + assert(compressed_transaction == uct); + + //Decompress OutPoints for Inputs + std::tuple, std::vector> result2 = fuzz_ctx->UncompressOutPoints(uct); + //Decompress Transaction + CTransaction new_tx = CTransaction(CMutableTransaction(uct, std::get<0>(result2), std::get<1>(result2))); + //Verify Decompressed Transaction matches original + assert(tx == new_tx); +}