From 37ef521f0891b792f0dd090daaa29bd65a772e2a Mon Sep 17 00:00:00 2001 From: Tom Briar Date: Tue, 11 Jul 2023 16:55:45 -0400 Subject: [PATCH] rpc: Added RPC endpoints for Compressed Transactions --- src/rpc/rawtransaction.cpp | 174 ++++++++++++++++++++++++++ test/functional/rpc_rawtransaction.py | 18 +++ 2 files changed, 192 insertions(+) diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index eb0200ccf5ffcc..356facf38cd8a2 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -45,12 +47,16 @@ #include #include +#include + +#include using node::AnalyzePSBT; using node::FindCoins; using node::GetTransaction; using node::NodeContext; using node::PSBTAnalysis; +using node::BlockManager; static void TxToJSON(const CTransaction& tx, const uint256 hashBlock, UniValue& entry, Chainstate& active_chainstate, const CTxUndo* txundo = nullptr, @@ -495,6 +501,172 @@ static RPCHelpMan decoderawtransaction() }; } +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; + } +}; +SecpContext secp_context = SecpContext(); + +static RPCHelpMan compressrawtransaction() +{ + return RPCHelpMan{"compressrawtransaction", + "Return a JSON object representing the compressed, serialized, hex-encoded transaction.", + { + {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The transaction hex string"}, + }, + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "result", "The compressed transaction hex string"}, + {RPCResult::Type::STR, "error", "Errors encountered during the compression"}, + }, + }, + RPCExamples{ + HelpExampleCli("compressrawtransaction", "\"hexstring\"") + + HelpExampleRpc("compressrawtransaction", "\"hexstring\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const NodeContext& node = EnsureAnyNodeContext(request.context); + ChainstateManager& chainman = EnsureChainman(node); + UniValue r(UniValue::VOBJ); + CMutableTransaction mtx; + std::string error = ""; + + std::vector input_scripts; + std::vector txids; + if (DecodeHexTx(mtx, request.params[0].get_str(), true, true)) { + std::map coins; + for (size_t index = 0; index < mtx.vin.size(); index++) { + coins[COutPoint(mtx.vin.at(index).prevout.hash, mtx.vin.at(index).prevout.n)]; // Create empty map entry keyed by prevout. + } + FindCoins(node, coins); + for (size_t index = 0; index < mtx.vin.size(); index++) { + Coin coin = coins[COutPoint(mtx.vin.at(index).prevout.hash, mtx.vin.at(index).prevout.n)]; + input_scripts.push_back(coin.out.scriptPubKey); + uint256 block_hash; + bool compressed_txid = false; + const CBlockIndex* pindex{nullptr}; + { + LOCK(cs_main); + pindex = chainman.ActiveChain()[coin.nHeight]; + } + uint32_t block_height = pindex->nHeight; + CBlock block; + if (chainman.m_blockman.ReadBlockFromDisk(block, *pindex)) { + const auto& optional_block_index = block.LookupTransactionIndex(mtx.vin.at(index).prevout.hash); + if (optional_block_index.has_value()) { + uint32_t block_index = optional_block_index.value(); + { + LOCK(cs_main); + Chainstate& active_chainstate = chainman.ActiveChainstate(); + + const CBlockIndex& tip{*CHECK_NONFATAL(active_chainstate.m_chain.Tip())}; + const int height{tip.nHeight}; + if ((height-100) >= int(block_height)) { + txids.push_back(CCompressedTxId(block_height, block_index)); + compressed_txid = true; + } else { + error = strprintf("UTXO (%u): Is not older then one hundred blocks and so we will not compress the TXID", index); + } + } + } + } + if (!compressed_txid) { + txids.push_back(CCompressedTxId(mtx.vin.at(index).prevout.hash)); + if (error == "") error = strprintf("UTXO (%u): Could not find Transaction Index to Compress", index); + } + } + } else throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "TX decode failed"); + + CCompressedTransaction ctx = CCompressedTransaction(secp_context.GetContext(), CTransaction(mtx), txids, input_scripts); + CDataStream stream(SER_DISK, 0); + ctx.Serialize(stream); + std::vector hex(stream.size()); + stream.read(MakeWritableByteSpan(hex)); + r.pushKV("result", HexStr(hex)); + r.pushKV("error", error); + return r; +}, + }; +} + +static RPCHelpMan decompressrawtransaction() +{ + return RPCHelpMan{"decompressrawtransaction", + "Return a JSON object representing the decompressed, serialized, hex-encoded transaction.", + { + {"hexstring", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The compressed transaction hex string"}, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "result", "The decompressed transaction hex string" + }, + RPCExamples{ + HelpExampleCli("decompressrawtransaction", "\"hexstring\"") + + HelpExampleRpc("decompressrawtransaction", "\"hexstring\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + const NodeContext& node = EnsureAnyNodeContext(request.context); + ChainstateManager& chainman = EnsureChainman(node); + UniValue r(UniValue::VOBJ); + try { + CDataStream ssData(ParseHex(request.params[0].get_str()), SER_NETWORK, PROTOCOL_VERSION); + CCompressedTransaction ctx = CCompressedTransaction(deserialize, ssData); + std::vector txids; + if (ctx.vin().size() > 0) { + std::vector blocks; + blocks = chainman.m_blockman.GetAllBlockIndices(); + + for (size_t index = 0; index < ctx.vin().size(); index++) { + if (ctx.vin().at(index).prevout().txid().IsCompressed()) { + uint256 txid; + const CBlockIndex* pindex{nullptr}; + { + LOCK(cs_main); + pindex = chainman.ActiveChain()[ctx.vin().at(index).prevout().txid().block_height()]; + } + if (pindex != nullptr) { + CBlock block; + chainman.m_blockman.ReadBlockFromDisk(block, *pindex); + if (ctx.vin().at(index).prevout().txid().block_index() < block.vtx.size()) { + txid = (*block.vtx.at(ctx.vin().at(index).prevout().txid().block_index())).GetHash(); + txids.push_back(txid); + } + } + if (txid.IsNull()) throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Couldn't find TxId"); + } else txids.push_back(ctx.vin().at(index).prevout().txid().txid()); + } + } + + std::map coins; + for (size_t index = 0; index < ctx.vin().size(); index++) { + coins[COutPoint(txids.at(index), ctx.vin().at(index).prevout().n())]; // Create empty map entry keyed by prevout. + } + FindCoins(node, coins); + std::vector outs; + for (size_t index = 0; index < ctx.vin().size(); index++) { + outs.push_back(coins[COutPoint(txids.at(index), ctx.vin().at(index).prevout().n())].out); + } + CTransaction tx = CTransaction(CMutableTransaction(secp_context.GetContext(), ctx, txids, outs)); + return EncodeHexTx(tx); + } catch (const std::exception& exc) { + // Fall through. + } + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Compressed TX decode failed"); +}, + }; +} + static RPCHelpMan decodescript() { return RPCHelpMan{ @@ -2000,6 +2172,8 @@ void RegisterRawTransactionRPCCommands(CRPCTable& t) {"rawtransactions", &getrawtransaction}, {"rawtransactions", &createrawtransaction}, {"rawtransactions", &decoderawtransaction}, + {"rawtransactions", &compressrawtransaction}, + {"rawtransactions", &decompressrawtransaction}, {"rawtransactions", &decodescript}, {"rawtransactions", &combinerawtransaction}, {"rawtransactions", &signrawtransactionwithkey}, diff --git a/test/functional/rpc_rawtransaction.py b/test/functional/rpc_rawtransaction.py index 23959356200f3f..d0585aace16d36 100755 --- a/test/functional/rpc_rawtransaction.py +++ b/test/functional/rpc_rawtransaction.py @@ -6,10 +6,12 @@ Test the following RPCs: - getrawtransaction + - compressrawtransaction - createrawtransaction - signrawtransactionwithwallet - sendrawtransaction - decoderawtransaction + - decompressrawtransaction """ from collections import OrderedDict @@ -86,10 +88,12 @@ def run_test(self): self.getrawtransaction_tests() self.getrawtransaction_verbosity_tests() + self.compressrawtransaction_tests() self.createrawtransaction_tests() self.sendrawtransaction_tests() self.sendrawtransaction_testmempoolaccept_tests() self.decoderawtransaction_tests() + self.decompressrawtransaction_tests() self.transaction_version_number_tests() if self.is_specified_wallet_compiled() and not self.options.descriptors: self.import_deterministic_coinbase_privkeys() @@ -244,6 +248,13 @@ def getrawtransaction_verbosity_tests(self): gottx = self.nodes[1].getrawtransaction(txid=coin_base, verbosity=2, blockhash=block1) assert 'fee' not in gottx + def compressrawtransaction_tests(self): + self.log.info("Test compressrawtransaction") + tx = "010000000001010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f50500000000000102616100000000" + compressed_transaction = self.nodes[0].compressrawtransaction(tx) + compressed_tx_target = "1580690000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000005000102616100aed6c100" + assert_equal(compressed_tx_target, compressed_transaction["result"]) + def createrawtransaction_tests(self): self.log.info("Test createrawtransaction") # Test `createrawtransaction` required parameters @@ -451,6 +462,13 @@ def decoderawtransaction_tests(self): assert_equal(decrawtx, decrawtx_wit) # the witness interpretation should be chosen assert_equal(decrawtx['vin'][0]['coinbase'], coinbase) + def decompressrawtransaction_tests(self): + self.log.info("Test decompressrawtransaction") + tx = "1580690000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000005000102616100aed6c100" + decompressed_transaction = self.nodes[0].decompressrawtransaction(tx) + decompressed_tx_target = "010000000001010000000000000072c1a6a246ae63f74f931e8365e15a089c68d61900000000000000000000ffffffff0100e1f50500000000000102616100000000" + assert_equal(decompressed_transaction, decompressed_tx_target) + def transaction_version_number_tests(self): self.log.info("Test transaction version numbers")