diff --git a/chronik/chronik-cpp/chronik_bridge.cpp b/chronik/chronik-cpp/chronik_bridge.cpp index 3eca1060d..5ae0f40fc 100644 --- a/chronik/chronik-cpp/chronik_bridge.cpp +++ b/chronik/chronik-cpp/chronik_bridge.cpp @@ -1,311 +1,311 @@ // Copyright (c) 2022 The Bitcoin 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 chronik_bridge::OutPoint BridgeOutPoint(const COutPoint &outpoint) { return { .txid = chronik::util::HashToArray(outpoint.GetTxId()), .out_idx = outpoint.GetN(), }; } chronik_bridge::TxOutput BridgeTxOutput(const CTxOut &output) { return { .value = output.nValue / Amount::satoshi(), .script = chronik::util::ToRustVec(output.scriptPubKey), }; } chronik_bridge::Coin BridgeCoin(const Coin &coin) { const int32_t nHeight = coin.GetHeight() == 0x7fff'ffff ? -1 : coin.GetHeight(); return { .output = BridgeTxOutput(coin.GetTxOut()), .height = nHeight, .is_coinbase = coin.IsCoinBase(), }; } rust::Vec BridgeTxInputs(bool isCoinbase, const std::vector &inputs, const std::vector &spent_coins) { rust::Vec bridged_inputs; bridged_inputs.reserve(inputs.size()); for (size_t idx = 0; idx < inputs.size(); ++idx) { const CTxIn &input = inputs[idx]; chronik_bridge::Coin bridge_coin{}; // empty coin if (!isCoinbase) { if (idx >= spent_coins.size()) { throw std::runtime_error("Missing coin for input"); } bridge_coin = BridgeCoin(spent_coins[idx]); } bridged_inputs.push_back({ .prev_out = BridgeOutPoint(input.prevout), .script = chronik::util::ToRustVec(input.scriptSig), .sequence = input.nSequence, .coin = std::move(bridge_coin), }); } return bridged_inputs; } rust::Vec BridgeTxOutputs(const std::vector &outputs) { rust::Vec bridged_outputs; bridged_outputs.reserve(outputs.size()); for (const CTxOut &output : outputs) { bridged_outputs.push_back(BridgeTxOutput(output)); } return bridged_outputs; } chronik_bridge::Tx BridgeTx(bool isCoinbase, const CTransaction &tx, const std::vector &spent_coins) { return { .txid = chronik::util::HashToArray(tx.GetId()), .version = tx.nVersion, .inputs = BridgeTxInputs(isCoinbase, tx.vin, spent_coins), .outputs = BridgeTxOutputs(tx.vout), .locktime = tx.nLockTime, }; } chronik_bridge::BlockTx BridgeBlockTx(bool isCoinbase, const CTransaction &tx, const std::vector &spent_coins, size_t data_pos, size_t undo_pos) { return {.tx = BridgeTx(isCoinbase, tx, spent_coins), .data_pos = uint32_t(data_pos), .undo_pos = uint32_t(isCoinbase ? 0 : undo_pos)}; } size_t GetFirstBlockTxOffset(const CBlock &block, const CBlockIndex &bindex) { return bindex.nDataPos + ::GetSerializeSize(CBlockHeader()) + GetSizeOfCompactSize(block.vtx.size()); } size_t GetFirstUndoOffset(const CBlock &block, const CBlockIndex &bindex) { // We have to -1 here, because coinbase txs don't have undo data. return bindex.nUndoPos + GetSizeOfCompactSize(block.vtx.size() - 1); } chronik_bridge::Block BridgeBlock(const CBlock &block, const CBlockIndex &bindex) { size_t data_pos = GetFirstBlockTxOffset(block, bindex); size_t undo_pos = 0; CBlockUndo block_undo; // Read undo data (genesis block doesn't have undo data) if (bindex.nHeight > 0) { undo_pos = GetFirstUndoOffset(block, bindex); if (!node::UndoReadFromDisk(block_undo, &bindex)) { throw std::runtime_error("Reading block undo data failed"); } } rust::Vec bridged_txs; for (size_t tx_idx = 0; tx_idx < block.vtx.size(); ++tx_idx) { const bool isCoinbase = tx_idx == 0; const CTransaction &tx = *block.vtx[tx_idx]; if (!isCoinbase && tx_idx - 1 >= block_undo.vtxundo.size()) { throw std::runtime_error("Missing undo data for tx"); } const std::vector &spent_coins = isCoinbase ? std::vector() : block_undo.vtxundo[tx_idx - 1].vprevout; bridged_txs.push_back( BridgeBlockTx(isCoinbase, tx, spent_coins, data_pos, undo_pos)); // advance data_pos and undo_pos positions data_pos += ::GetSerializeSize(tx); if (!isCoinbase) { undo_pos += ::GetSerializeSize(block_undo.vtxundo[tx_idx - 1]); } } return {.hash = chronik::util::HashToArray(block.GetHash()), .prev_hash = chronik::util::HashToArray(block.hashPrevBlock), .n_bits = block.nBits, .timestamp = block.GetBlockTime(), .height = bindex.nHeight, .file_num = uint32_t(bindex.nFile), .data_pos = bindex.nDataPos, .undo_pos = bindex.nUndoPos, - .size = bindex.nSize, + .size = ::GetSerializeSize(block), .txs = bridged_txs}; } namespace chronik_bridge { void log_print(const rust::Str logging_function, const rust::Str source_file, const uint32_t source_line, const rust::Str msg) { LogInstance().LogPrintStr(std::string(msg), std::string(logging_function), std::string(source_file), source_line); } void log_print_chronik(const rust::Str logging_function, const rust::Str source_file, const uint32_t source_line, const rust::Str msg) { if (LogInstance().WillLogCategory(BCLog::CHRONIK)) { log_print(logging_function, source_file, source_line, msg); } } const CBlockIndex &ChronikBridge::get_chain_tip() const { const CBlockIndex *tip = WITH_LOCK(cs_main, return m_node.chainman->ActiveTip()); if (tip == nullptr) { throw block_index_not_found(); } return *tip; } const CBlockIndex & ChronikBridge::lookup_block_index(std::array hash) const { BlockHash block_hash{chronik::util::ArrayToHash(hash)}; const CBlockIndex *pindex = WITH_LOCK( cs_main, return m_node.chainman->m_blockman.LookupBlockIndex(block_hash)); if (!pindex) { throw block_index_not_found(); } return *pindex; } std::unique_ptr ChronikBridge::load_block(const CBlockIndex &bindex) const { CBlock block; if (!node::ReadBlockFromDisk(block, &bindex, m_consensus)) { throw std::runtime_error("Reading block data failed"); } return std::make_unique(std::move(block)); } Tx ChronikBridge::bridge_tx(const CTransaction &tx) const { std::map coins; for (const CTxIn &input : tx.vin) { coins[input.prevout]; } FindCoins(m_node, coins); std::vector<::Coin> spent_coins; spent_coins.reserve(tx.vin.size()); for (const CTxIn &input : tx.vin) { const ::Coin &coin = coins[input.prevout]; if (coin.GetTxOut().IsNull()) { throw std::runtime_error("Couldn't find coin for input"); } spent_coins.push_back(coin); } return BridgeTx(false, tx, spent_coins); } const CBlockIndex &ChronikBridge::find_fork(const CBlockIndex &index) const { const CBlockIndex *fork = WITH_LOCK( cs_main, return m_node.chainman->ActiveChainstate().m_chain.FindFork(&index)); if (!fork) { throw block_index_not_found(); } return *fork; } std::unique_ptr make_bridge(const Config &config, const node::NodeContext &node) { return std::make_unique( config.GetChainParams().GetConsensus(), node); } chronik_bridge::Block bridge_block(const CBlock &block, const CBlockIndex &bindex) { return BridgeBlock(block, bindex); } Tx load_tx(uint32_t file_num, uint32_t data_pos, uint32_t undo_pos) { CMutableTransaction tx; CTxUndo txundo{}; const bool isCoinbase = undo_pos == 0; if (!node::ReadTxFromDisk(tx, FlatFilePos(file_num, data_pos))) { throw std::runtime_error("Reading tx data from disk failed"); } if (!isCoinbase) { if (!node::ReadTxUndoFromDisk(txundo, FlatFilePos(file_num, undo_pos))) { throw std::runtime_error("Reading tx undo data from disk failed"); } } return BridgeTx(isCoinbase, CTransaction(std::move(tx)), txundo.vprevout); } rust::Vec load_raw_tx(uint32_t file_num, uint32_t data_pos) { CMutableTransaction tx; if (!node::ReadTxFromDisk(tx, FlatFilePos(file_num, data_pos))) { throw std::runtime_error("Reading tx data from disk failed"); } CDataStream raw_tx{SER_NETWORK, PROTOCOL_VERSION}; raw_tx << tx; return chronik::util::ToRustVec(raw_tx); } BlockInfo get_block_info(const CBlockIndex &bindex) { return { .hash = chronik::util::HashToArray(bindex.GetBlockHash()), .height = bindex.nHeight, }; } const CBlockIndex &get_block_ancestor(const CBlockIndex &index, int32_t height) { const CBlockIndex *pindex = index.GetAncestor(height); if (!pindex) { throw block_index_not_found(); } return *pindex; } rust::Vec compress_script(rust::Slice bytecode) { std::vector vec = chronik::util::FromRustSlice(bytecode); CScript script{vec.begin(), vec.end()}; CDataStream compressed{SER_NETWORK, PROTOCOL_VERSION}; compressed << Using(script); return chronik::util::ToRustVec(compressed); } rust::Vec decompress_script(rust::Slice compressed) { std::vector vec = chronik::util::FromRustSlice(compressed); CDataStream stream{vec, SER_NETWORK, PROTOCOL_VERSION}; CScript script; stream >> Using(script); return chronik::util::ToRustVec(script); } bool init_error(const rust::Str msg) { return InitError(Untranslated(std::string(msg))); } void abort_node(const rust::Str msg, const rust::Str user_msg) { AbortNode(std::string(msg), Untranslated(std::string(user_msg))); } bool shutdown_requested() { return ShutdownRequested(); } } // namespace chronik_bridge diff --git a/test/functional/chronik_block.py b/test/functional/chronik_block.py index 8c7cc70de..a38f47871 100644 --- a/test/functional/chronik_block.py +++ b/test/functional/chronik_block.py @@ -1,147 +1,152 @@ #!/usr/bin/env python3 # Copyright (c) 2023 The Bitcoin developers # Distributed under the MIT software license, see the accompanying # file COPYING or http://www.opensource.org/licenses/mit-license.php. """ Test Chronik's /block endpoint. """ from test_framework.address import ADDRESS_ECREG_P2SH_OP_TRUE, ADDRESS_ECREG_UNSPENDABLE from test_framework.blocktools import GENESIS_BLOCK_HASH, TIME_GENESIS_BLOCK from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal class ChronikBlockTest(BitcoinTestFramework): def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 1 self.extra_args = [["-chronik"]] self.rpc_timeout = 240 def skip_test_if_missing_module(self): self.skip_if_no_chronik() def run_test(self): from test_framework.chronik.client import ChronikClient, pb node = self.nodes[0] chronik = ChronikClient("127.0.0.1", node.chronik_port) expected_genesis_block = pb.Block( block_info=pb.BlockInfo( hash=bytes.fromhex(GENESIS_BLOCK_HASH)[::-1], prev_hash=bytes(32), height=0, n_bits=0x207FFFFF, timestamp=TIME_GENESIS_BLOCK, block_size=285, num_txs=1, num_inputs=1, num_outputs=1, sum_input_sats=0, sum_coinbase_output_sats=5000000000, sum_normal_output_sats=0, sum_burned_sats=0, ), ) # Not a valid hash or height assert_equal( chronik.block("1234f").err(400).msg, "400: Not a hash or height: 1234f" ) assert_equal( chronik.block("00" * 31).err(400).msg, f'400: Not a hash or height: {"00"*31}', ) assert_equal(chronik.block("01").err(400).msg, "400: Not a hash or height: 01") assert_equal( chronik.block("12345678901").err(400).msg, "400: Not a hash or height: 12345678901", ) # Query genesis block using height assert_equal(chronik.block(0).ok(), expected_genesis_block) # Or hash assert_equal(chronik.block(GENESIS_BLOCK_HASH).ok(), expected_genesis_block) # Block 1 not found assert_equal(chronik.block(1).err(404).msg, "404: Block not found: 1") # Block "0000...0000" not found assert_equal( chronik.block("00" * 32).err(404).msg, f'404: Block not found: {"00"*32}' ) # Generate 100 blocks, verify they form a chain block_hashes = [GENESIS_BLOCK_HASH] + self.generatetoaddress( node, 100, ADDRESS_ECREG_P2SH_OP_TRUE ) + expected_proto_blocks = [] for i in range(1, 101): proto_block = chronik.block(i).ok() - assert_equal( - proto_block, - pb.Block( - block_info=pb.BlockInfo( - hash=bytes.fromhex(block_hashes[i])[::-1], - prev_hash=bytes.fromhex(block_hashes[i - 1])[::-1], - height=i, - n_bits=0x207FFFFF, - timestamp=proto_block.block_info.timestamp, - block_size=181, - num_txs=1, - num_inputs=1, - num_outputs=1, - sum_input_sats=0, - sum_coinbase_output_sats=5000000000, - sum_normal_output_sats=0, - sum_burned_sats=0, - ), + expected_proto = pb.Block( + block_info=pb.BlockInfo( + hash=bytes.fromhex(block_hashes[i])[::-1], + prev_hash=bytes.fromhex(block_hashes[i - 1])[::-1], + height=i, + n_bits=0x207FFFFF, + timestamp=proto_block.block_info.timestamp, + block_size=181, + num_txs=1, + num_inputs=1, + num_outputs=1, + sum_input_sats=0, + sum_coinbase_output_sats=5000000000, + sum_normal_output_sats=0, + sum_burned_sats=0, ), ) + expected_proto_blocks.append(expected_proto) + assert_equal(proto_block, expected_proto) assert_equal(proto_block, chronik.block(block_hashes[i]).ok()) block_hashes.append(proto_block.block_info.hash) + # Using -chronikreindex results in the same data + self.restart_node(0, ["-chronik", "-chronikreindex"]) + for i in range(1, 101): + assert_equal(chronik.block(i).ok(), expected_proto_blocks[i - 1]) + # Invalidate in the middle of the chain node.invalidateblock(block_hashes[50]) # Gives 404 for the invalidated blocks for i in range(50, 101): assert_equal(chronik.block(i).err(404).msg, f"404: Block not found: {i}") assert_equal( chronik.block(block_hashes[i]).err(404).msg, f"404: Block not found: {block_hashes[i]}", ) # Previous blocks are still fine for i in range(0, 50): chronik.block(i).ok() chronik.block(block_hashes[i]).ok() # Mine fork block and check it connects fork_hash = self.generatetoaddress(node, 1, ADDRESS_ECREG_UNSPENDABLE)[0] proto_block = chronik.block(50).ok() assert_equal( proto_block, pb.Block( block_info=pb.BlockInfo( hash=bytes.fromhex(fork_hash)[::-1], prev_hash=bytes.fromhex(block_hashes[49])[::-1], height=50, n_bits=0x207FFFFF, timestamp=proto_block.block_info.timestamp, block_size=181, num_txs=1, num_inputs=1, num_outputs=1, sum_input_sats=0, sum_coinbase_output_sats=5000000000, sum_normal_output_sats=0, sum_burned_sats=0, ), ), ) assert_equal(chronik.block(fork_hash).ok(), proto_block) if __name__ == "__main__": ChronikBlockTest().main()