Skip to content

Commit

Permalink
rpc: Added RPC endpoints for Compressed Transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
TomBriar committed Jul 18, 2023
1 parent 89b5278 commit 37ef521
Show file tree
Hide file tree
Showing 2 changed files with 192 additions and 0 deletions.
174 changes: 174 additions & 0 deletions src/rpc/rawtransaction.cpp
Expand Up @@ -12,6 +12,7 @@
#include <index/txindex.h>
#include <key_io.h>
#include <node/blockstorage.h>
#include <node/chainstate.h>
#include <node/coin.h>
#include <node/context.h>
#include <node/psbt.h>
Expand All @@ -20,6 +21,7 @@
#include <policy/policy.h>
#include <policy/rbf.h>
#include <primitives/transaction.h>
#include <primitives/compression.h>
#include <psbt.h>
#include <random.h>
#include <rpc/blockchain.h>
Expand All @@ -45,12 +47,16 @@
#include <stdint.h>

#include <univalue.h>
#include <optional>

#include <secp256k1.h>

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,
Expand Down Expand Up @@ -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<CScript> input_scripts;
std::vector<CCompressedTxId> txids;
if (DecodeHexTx(mtx, request.params[0].get_str(), true, true)) {
std::map<COutPoint, Coin> 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<unsigned char> 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<uint256> txids;
if (ctx.vin().size() > 0) {
std::vector<CBlockIndex*> 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<COutPoint, Coin> 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<CTxOut> 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{
Expand Down Expand Up @@ -2000,6 +2172,8 @@ void RegisterRawTransactionRPCCommands(CRPCTable& t)
{"rawtransactions", &getrawtransaction},
{"rawtransactions", &createrawtransaction},
{"rawtransactions", &decoderawtransaction},
{"rawtransactions", &compressrawtransaction},
{"rawtransactions", &decompressrawtransaction},
{"rawtransactions", &decodescript},
{"rawtransactions", &combinerawtransaction},
{"rawtransactions", &signrawtransactionwithkey},
Expand Down
18 changes: 18 additions & 0 deletions test/functional/rpc_rawtransaction.py
Expand Up @@ -6,10 +6,12 @@
Test the following RPCs:
- getrawtransaction
- compressrawtransaction
- createrawtransaction
- signrawtransactionwithwallet
- sendrawtransaction
- decoderawtransaction
- decompressrawtransaction
"""

from collections import OrderedDict
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")

Expand Down

0 comments on commit 37ef521

Please sign in to comment.