diff --git a/chronik/chronik-bridge/src/ffi.rs b/chronik/chronik-bridge/src/ffi.rs index 8b596cfe0..446ec6aec 100644 --- a/chronik/chronik-bridge/src/ffi.rs +++ b/chronik/chronik-bridge/src/ffi.rs @@ -1,284 +1,308 @@ // 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. //! Module containing the cxx definitions for the bridge from C++ to Rust. pub use self::ffi_inner::*; #[allow(unsafe_code)] #[cxx::bridge(namespace = "chronik_bridge")] mod ffi_inner { /// Info about a block #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct BlockInfo { /// Hash of the block (or 000...000 if no block) pub hash: [u8; 32], /// Height of the block (or -1 if no block) pub height: i32, } /// Block coming from bitcoind to Chronik. /// /// We don't index all fields (e.g. hashMerkleRoot), only those that are /// needed when querying a range of blocks. /// /// Instead of storing all the block data for Chronik again, we only store /// file_num, data_pos and undo_pos of the block data of the node. /// /// This makes the index relatively small, as it's mostly just pointing to /// the data the node already stores. /// /// Note that this prohibits us from using Chronik in pruned mode. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Block { /// Block hash pub hash: [u8; 32], /// hashPrevBlock, hash of the previous block in the chain pub prev_hash: [u8; 32], /// nBits, difficulty of the header pub n_bits: u32, /// Timestamp of the block pub timestamp: i64, /// Height of the block in the chain. pub height: i32, /// File number of the block file this block is stored in. /// This can be used to later slice out transactions, so we don't have /// to index txs twice. pub file_num: u32, /// Position of the block within the block file, starting at the block /// header. pub data_pos: u32, /// Position of the undo data within the undo file. pub undo_pos: u32, /// Serialized size of the block pub size: u64, /// Txs of this block, including positions within the block/undo files. pub txs: Vec<BlockTx>, } /// Tx in a block #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct BlockTx { /// Tx (without disk data) pub tx: Tx, /// Where the tx is stored within the block file. pub data_pos: u32, /// Where the tx's undo data is stored within the block's undo file. pub undo_pos: u32, } /// CTransaction, in a block or in the mempool. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Tx { /// TxId of the tx. pub txid: [u8; 32], /// nVersion of the tx. pub version: i32, /// Tx inputs. pub inputs: Vec<TxInput>, /// Tx outputs. pub outputs: Vec<TxOutput>, /// Locktime of the tx. pub locktime: u32, } /// COutPoint, pointing to a coin being spent. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct OutPoint { /// TxId of the output of the coin. pub txid: [u8; 32], /// Index in the outputs of the tx of the coin. pub out_idx: u32, } /// CTxIn, spending an unspent output. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TxInput { /// Points to an output being spent. pub prev_out: OutPoint, /// scriptSig unlocking the output. pub script: Vec<u8>, /// nSequence. pub sequence: u32, /// Coin being spent by this tx. pub coin: Coin, } /// CTxOut, creating a new output. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct TxOutput { /// Value of the output. pub value: i64, /// Script locking the output. pub script: Vec<u8>, } /// Coin, can be spent by providing a valid unlocking script. #[derive(Clone, Debug, Default, Eq, PartialEq)] pub struct Coin { /// Output, locking the coins. pub output: TxOutput, /// Height of the coin in the chain. pub height: i32, /// Whether the coin is a coinbase. pub is_coinbase: bool, } #[allow(missing_debug_implementations)] unsafe extern "C++" { include!("blockindex.h"); include!("chronik-cpp/chronik_bridge.h"); include!("coins.h"); include!("node/context.h"); include!("primitives/block.h"); include!("primitives/transaction.h"); /// node::NodeContext from node/context.h #[namespace = "node"] type NodeContext; /// ::CBlockIndex from blockindex.h #[namespace = ""] type CBlockIndex; /// ::CBlock from primitives/block.h #[namespace = ""] type CBlock; /// ::Coin from coins.h (renamed to CCoin to prevent a name clash) #[namespace = ""] #[cxx_name = "Coin"] type CCoin; /// ::Config from config.h #[namespace = ""] type Config; /// ::CTransaction from primitives/transaction.h #[namespace = ""] type CTransaction; /// Bridge to bitcoind to access the node type ChronikBridge; /// Print the message to bitcoind's logs. fn log_print( logging_function: &str, source_file: &str, source_line: u32, msg: &str, ); /// Print the message to bitcoind's logs under the BCLog::Chronik /// category. fn log_print_chronik( logging_function: &str, source_file: &str, source_line: u32, msg: &str, ); /// Make the bridge given the NodeContext fn make_bridge( config: &Config, node: &NodeContext, ) -> UniquePtr<ChronikBridge>; /// Return the tip of the chain of the node. /// Returns hash=000...000, height=-1 if there's no block on the chain. fn get_chain_tip(self: &ChronikBridge) -> Result<&CBlockIndex>; /// Lookup the block index with the given hash, or throw an error /// if it couldn't be found. fn lookup_block_index( self: &ChronikBridge, hash: [u8; 32], ) -> Result<&CBlockIndex>; /// Load the CBlock data of this CBlockIndex from the disk fn load_block( self: &ChronikBridge, block_index: &CBlockIndex, ) -> Result<UniquePtr<CBlock>>; /// Find at which block the given block_index forks off from the node. fn find_fork( self: &ChronikBridge, block_index: &CBlockIndex, ) -> Result<&CBlockIndex>; + /// Lookup the spent coins of a tx and fill them in in-place. + /// - `not_found` will be the outpoints that couldn't be found in the + /// node or the DB. + /// - `coins_to_uncache` will be the outpoints that need to be uncached + /// if the tx doesn't end up being broadcast. This is so that clients + /// can't fill our cache with useless old coins. It mirrors the + /// behavior of `MemPoolAccept::PreChecks`, which uncaches the queried + /// coins if they don't end up being spent. + fn lookup_spent_coins( + self: &ChronikBridge, + tx: &mut Tx, + not_found: &mut Vec<OutPoint>, + coins_to_uncache: &mut Vec<OutPoint>, + ) -> Result<()>; + + /// Remove the coins from the coin cache. + /// This must be done after a call to `lookup_spent_coins` where the tx + /// wasn't broadcast, to avoid clients filling our cache with unneeded + /// coins. + fn uncache_coins( + self: &ChronikBridge, + coins: &[OutPoint], + ) -> Result<()>; + /// Add the given tx to the mempool, and if that succeeds, broadcast it /// to all our peers. /// Also check the actual tx fee doesn't exceed max_fee. /// Note max_fee is absolute, not a fee rate (as in sendrawtransaction). fn broadcast_tx( self: &ChronikBridge, raw_tx: &[u8], max_fee: i64, ) -> Result<[u8; 32]>; /// Bridge CTransaction -> ffi::Tx, using the given spent coins. fn bridge_tx( tx: &CTransaction, spent_coins: &CxxVector<CCoin>, ) -> Result<Tx>; /// Bridge bitcoind's classes to the shared struct [`Block`]. fn bridge_block( block: &CBlock, block_index: &CBlockIndex, ) -> Result<Block>; /// Load the CTransaction and CTxUndo data from disk and turn it into a /// bridged Tx, containing spent coins etc. fn load_tx(file_num: u32, data_pos: u32, undo_pos: u32) -> Result<Tx>; /// Load the CTransaction from disk and serialize it. fn load_raw_tx(file_num: u32, data_pos: u32) -> Result<Vec<u8>>; /// Get a BlockInfo for this CBlockIndex. fn get_block_info(block_index: &CBlockIndex) -> BlockInfo; /// CBlockIndex::GetAncestor fn get_block_ancestor( block_index: &CBlockIndex, height: i32, ) -> Result<&CBlockIndex>; /// Compress the given script using `ScriptCompression`. fn compress_script(script: &[u8]) -> Vec<u8>; /// Decompress the given script using `ScriptCompression`. fn decompress_script(compressed: &[u8]) -> Result<Vec<u8>>; /// Calls `InitError` from `node/ui_interface.h` to report an error to /// the user and then gracefully shut down the node. fn init_error(msg: &str) -> bool; /// Calls `AbortNode` from shutdown.h to gracefully shut down the node /// when an unrecoverable error occured. fn abort_node(msg: &str, user_msg: &str); /// Returns true if a shutdown is requested, false otherwise. /// See `ShutdownRequested` in `shutdown.h`. fn shutdown_requested() -> bool; } } /// SAFETY: All fields of ChronikBridge (const Consensus::Params &, const /// node::NodeContext &) can be moved betweed threads safely. #[allow(unsafe_code)] unsafe impl Send for ChronikBridge {} /// SAFETY: All fields of ChronikBridge (const Consensus::Params &, const /// node::NodeContext &) can be accessed from different threads safely. #[allow(unsafe_code)] unsafe impl Sync for ChronikBridge {} impl std::fmt::Debug for ChronikBridge { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("ChronikBridge").finish_non_exhaustive() } } diff --git a/chronik/chronik-cpp/chronik_bridge.cpp b/chronik/chronik-cpp/chronik_bridge.cpp index cb7e79b6d..dddb59873 100644 --- a/chronik/chronik-cpp/chronik_bridge.cpp +++ b/chronik/chronik-cpp/chronik_bridge.cpp @@ -1,323 +1,367 @@ // 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 <blockindex.h> #include <chainparams.h> #include <chronik-bridge/src/ffi.rs.h> #include <chronik-cpp/chronik_bridge.h> #include <chronik-cpp/util/collection.h> #include <chronik-cpp/util/hash.h> #include <compressor.h> #include <config.h> #include <logging.h> #include <node/blockstorage.h> #include <node/coin.h> #include <node/context.h> #include <node/transaction.h> #include <node/ui_interface.h> #include <shutdown.h> #include <streams.h> #include <undo.h> #include <util/error.h> #include <validation.h> 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<uint8_t>(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<chronik_bridge::TxInput> BridgeTxInputs(bool isCoinbase, const std::vector<CTxIn> &inputs, const std::vector<Coin> &spent_coins) { rust::Vec<chronik_bridge::TxInput> 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<uint8_t>(input.scriptSig), .sequence = input.nSequence, .coin = std::move(bridge_coin), }); } return bridged_inputs; } rust::Vec<chronik_bridge::TxOutput> BridgeTxOutputs(const std::vector<CTxOut> &outputs) { rust::Vec<chronik_bridge::TxOutput> 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<Coin> &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<Coin> &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<chronik_bridge::BlockTx> 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<Coin> &spent_coins = isCoinbase ? std::vector<Coin>() : 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 = ::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<uint8_t, 32> 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<CBlock> 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<CBlock>(std::move(block)); } Tx bridge_tx(const CTransaction &tx, const std::vector<::Coin> &spent_coins) { 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; } +void ChronikBridge::lookup_spent_coins( + Tx &tx, rust::Vec<OutPoint> ¬_found, + rust::Vec<OutPoint> &coins_to_uncache) const { + not_found.clear(); + coins_to_uncache.clear(); + LOCK(cs_main); + CCoinsViewCache &coins_cache = + m_node.chainman->ActiveChainstate().CoinsTip(); + CCoinsViewMemPool coin_view(&coins_cache, *m_node.mempool); + for (TxInput &input : tx.inputs) { + TxId txid = TxId(chronik::util::ArrayToHash(input.prev_out.txid)); + COutPoint outpoint = COutPoint(txid, input.prev_out.out_idx); + + // Remember if coin was already cached + const bool had_cached = coins_cache.HaveCoinInCache(outpoint); + + ::Coin coin; + if (!coin_view.GetCoin(outpoint, coin)) { + not_found.push_back(input.prev_out); + continue; + } + + if (!had_cached) { + // Only add if previously uncached. + // We don't check if the prev_out is now cached (which wouldn't be + // the case for a mempool UTXO), as uncaching an outpoint is cheap, + // so we save one extra cache lookup here. + coins_to_uncache.push_back(input.prev_out); + } + input.coin = BridgeCoin(coin); + } +} + +void ChronikBridge::uncache_coins( + rust::Slice<const OutPoint> coins_to_uncache) const { + LOCK(cs_main); + CCoinsViewCache &coins_cache = + m_node.chainman->ActiveChainstate().CoinsTip(); + for (const OutPoint &outpoint : coins_to_uncache) { + TxId txid = TxId(chronik::util::ArrayToHash(outpoint.txid)); + coins_cache.Uncache(COutPoint(txid, outpoint.out_idx)); + } +} + std::array<uint8_t, 32> ChronikBridge::broadcast_tx(rust::Slice<const uint8_t> raw_tx, int64_t max_fee) const { std::vector<uint8_t> vec = chronik::util::FromRustSlice(raw_tx); CDataStream stream{vec, SER_NETWORK, PROTOCOL_VERSION}; CMutableTransaction tx; stream >> tx; CTransactionRef tx_ref = MakeTransactionRef(tx); std::string err_str; TransactionError error = node::BroadcastTransaction( m_node, tx_ref, err_str, max_fee * Amount::satoshi(), /*relay=*/true, /*wait_callback=*/false); if (error != TransactionError::OK) { bilingual_str txErrorMsg = TransactionErrorString(error); if (err_str.empty()) { throw std::runtime_error(txErrorMsg.original.c_str()); } else { std::string msg = strprintf("%s: %s", txErrorMsg.original, err_str); throw std::runtime_error(msg.c_str()); } } return chronik::util::HashToArray(tx_ref->GetId()); } std::unique_ptr<ChronikBridge> make_bridge(const Config &config, const node::NodeContext &node) { return std::make_unique<ChronikBridge>( 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<uint8_t> 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<uint8_t>(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<uint8_t> compress_script(rust::Slice<const uint8_t> bytecode) { std::vector<uint8_t> vec = chronik::util::FromRustSlice(bytecode); CScript script{vec.begin(), vec.end()}; CDataStream compressed{SER_NETWORK, PROTOCOL_VERSION}; compressed << Using<ScriptCompression>(script); return chronik::util::ToRustVec<uint8_t>(compressed); } rust::Vec<uint8_t> decompress_script(rust::Slice<const uint8_t> compressed) { std::vector<uint8_t> vec = chronik::util::FromRustSlice(compressed); CDataStream stream{vec, SER_NETWORK, PROTOCOL_VERSION}; CScript script; stream >> Using<ScriptCompression>(script); return chronik::util::ToRustVec<uint8_t>(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/chronik/chronik-cpp/chronik_bridge.h b/chronik/chronik-cpp/chronik_bridge.h index f185e387f..770ee26d8 100644 --- a/chronik/chronik-cpp/chronik_bridge.h +++ b/chronik/chronik-cpp/chronik_bridge.h @@ -1,97 +1,102 @@ // 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. #ifndef BITCOIN_CHRONIK_CPP_CHRONIK_BRIDGE_H #define BITCOIN_CHRONIK_CPP_CHRONIK_BRIDGE_H #include <memory> #include <rust/cxx.h> #include <vector> class CBlock; class CBlockIndex; class Coin; class Config; class CTransaction; namespace Consensus { struct Params; } // namespace Consensus namespace node { struct NodeContext; } // namespace node class uint256; namespace chronik_bridge { struct BlockInfo; struct Block; struct Tx; +struct OutPoint; class block_index_not_found : public std::exception { public: const char *what() const noexcept override { return "CBlockIndex not found"; } }; void log_print(const rust::Str logging_function, const rust::Str source_file, const uint32_t source_line, const rust::Str msg); void log_print_chronik(const rust::Str logging_function, const rust::Str source_file, const uint32_t source_line, const rust::Str msg); /** * Bridge to bitcoind to access the node. */ class ChronikBridge { const Consensus::Params &m_consensus; const node::NodeContext &m_node; public: ChronikBridge(const Consensus::Params &consensus, const node::NodeContext &node) : m_consensus(consensus), m_node(node) {} const CBlockIndex &get_chain_tip() const; const CBlockIndex &lookup_block_index(std::array<uint8_t, 32> hash) const; std::unique_ptr<CBlock> load_block(const CBlockIndex &bindex) const; const CBlockIndex &find_fork(const CBlockIndex &index) const; + void lookup_spent_coins(Tx &, rust::Vec<OutPoint> ¬_found, + rust::Vec<OutPoint> &coins_to_uncache) const; + void uncache_coins(rust::Slice<const OutPoint>) const; + std::array<uint8_t, 32> broadcast_tx(rust::Slice<const uint8_t> raw_tx, int64_t max_fee) const; }; std::unique_ptr<ChronikBridge> make_bridge(const Config &config, const node::NodeContext &node); Tx bridge_tx(const CTransaction &tx, const std::vector<Coin> &spent_coins); Block bridge_block(const CBlock &block, const CBlockIndex &bindex); Tx load_tx(uint32_t file_num, uint32_t data_pos, uint32_t undo_pos); rust::Vec<uint8_t> load_raw_tx(uint32_t file_num, uint32_t data_pos); BlockInfo get_block_info(const CBlockIndex &index); const CBlockIndex &get_block_ancestor(const CBlockIndex &index, int32_t height); rust::Vec<uint8_t> compress_script(rust::Slice<const uint8_t> script); rust::Vec<uint8_t> decompress_script(rust::Slice<const uint8_t> compressed); bool init_error(const rust::Str msg); void abort_node(const rust::Str msg, const rust::Str user_msg); bool shutdown_requested(); } // namespace chronik_bridge #endif // BITCOIN_CHRONIK_CPP_CHRONIK_BRIDGE_H diff --git a/chronik/test/chronikbridge_tests.cpp b/chronik/test/chronikbridge_tests.cpp index a62caaf37..41117e1da 100644 --- a/chronik/test/chronikbridge_tests.cpp +++ b/chronik/test/chronikbridge_tests.cpp @@ -1,233 +1,334 @@ // 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 <chainparams.h> #include <chronik-bridge/src/ffi.rs.h> #include <chronik-cpp/chronik_bridge.h> #include <chronik-cpp/util/hash.h> #include <config.h> #include <streams.h> #include <util/strencodings.h> #include <validation.h> #include <test/util/setup_common.h> #include <boost/test/unit_test.hpp> BOOST_AUTO_TEST_SUITE(chronikbridge_tests) BOOST_FIXTURE_TEST_CASE(test_get_chain_tip_empty, ChainTestingSetup) { // Setup chainstate { LOCK(::cs_main); m_node.chainman->InitializeChainstate(m_node.mempool.get()); } // Chain has no blocks yet: // get_chain_tip throws block_index_not_found const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); BOOST_CHECK_THROW(bridge.get_chain_tip(), chronik_bridge::block_index_not_found); } BOOST_FIXTURE_TEST_CASE(test_get_chain_tip_genesis, TestingSetup) { const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); // Check for genesis block const CBlockIndex &bindex = bridge.get_chain_tip(); BOOST_CHECK_EQUAL(bindex.GetBlockHash(), params.GenesisBlock().GetHash()); } BOOST_FIXTURE_TEST_CASE(test_get_chain_tip_100, TestChain100Setup) { const CChainParams ¶ms = GetConfig().GetChainParams(); // Generate new block (at height 101) CBlock tip_block = CreateAndProcessBlock( {}, CScript() << std::vector<uint8_t>(33) << OP_CHECKSIG); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); // Check if block is 101th const CBlockIndex &bindex = bridge.get_chain_tip(); BOOST_CHECK_EQUAL(bindex.GetBlockHash(), tip_block.GetHash()); } BOOST_FIXTURE_TEST_CASE(test_lookup_block_index, TestChain100Setup) { const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); BlockHash genesis_hash = params.GenesisBlock().GetHash(); const CBlockIndex &bindex_genesis = bridge.lookup_block_index(chronik::util::HashToArray(genesis_hash)); BOOST_CHECK_EQUAL(bindex_genesis.GetBlockHash(), genesis_hash); // Generate new block (at height 101) CBlock tip_block = CreateAndProcessBlock( {}, CScript() << std::vector<uint8_t>(33) << OP_CHECKSIG); // Query block const CBlockIndex &bindex_tip = bridge.lookup_block_index( chronik::util::HashToArray(tip_block.GetHash())); BOOST_CHECK_EQUAL(bindex_tip.GetBlockHash(), tip_block.GetHash()); // Block 000...000 doesn't exist BOOST_CHECK_THROW(bridge.lookup_block_index({}), chronik_bridge::block_index_not_found); } BOOST_FIXTURE_TEST_CASE(test_find_fork, TestChain100Setup) { const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); ChainstateManager &chainman = *Assert(m_node.chainman); CBlockIndex *tip = chainman.ActiveTip(); // Fork of the tip is the tip BOOST_CHECK_EQUAL(bridge.find_fork(*tip).GetBlockHash(), tip->GetBlockHash()); // Fork of the genesis block is the genesis block BOOST_CHECK_EQUAL(bridge.find_fork(*tip->GetAncestor(0)).GetBlockHash(), params.GenesisBlock().GetHash()); // Invalidate block in the middle of the chain BlockValidationState state; chainman.ActiveChainstate().InvalidateBlock(GetConfig(), state, tip->GetAncestor(50)); // Mine 100 blocks, up to height 150 mineBlocks(100); // Fork of old tip is block 49 BOOST_CHECK_EQUAL(bridge.find_fork(*tip).GetBlockHash(), chainman.ActiveTip()->GetAncestor(49)->GetBlockHash()); } +BOOST_FIXTURE_TEST_CASE(test_lookup_spent_coin, TestChain100Setup) { + ChainstateManager &chainman = *Assert(m_node.chainman); + LOCK(cs_main); + const CChainParams ¶ms = GetConfig().GetChainParams(); + const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); + CCoinsViewCache &coins_cache = chainman.ActiveChainstate().CoinsTip(); + + CScript anyoneScript = CScript() << OP_1; + CScript anyoneP2sh = GetScriptForDestination(ScriptHash(anyoneScript)); + CBlock coinsBlock = + CreateAndProcessBlock({}, anyoneP2sh, &chainman.ActiveChainstate()); + mineBlocks(100); + + const CTransactionRef coinTx = coinsBlock.vtx[0]; + + CScript scriptPad = CScript() << OP_RETURN << std::vector<uint8_t>(100); + CMutableTransaction tx; + tx.vin = {CTxIn( + coinTx->GetId(), 0, + CScript() << std::vector(anyoneScript.begin(), anyoneScript.end()))}; + tx.vout = { + CTxOut(1000 * SATOSHI, anyoneP2sh), + CTxOut(coinTx->vout[0].nValue - 10000 * SATOSHI, anyoneP2sh), + }; + const MempoolAcceptResult result = + m_node.chainman->ProcessTransaction(MakeTransactionRef(tx)); + BOOST_CHECK_EQUAL(result.m_result_type, + MempoolAcceptResult::ResultType::VALID); + TxId txid = tx.GetId(); + + // Tx we look up coins for + chronik_bridge::Tx query_tx = { + .inputs = { + {.prev_out = {chronik::util::HashToArray(txid), 0}}, + {.prev_out = {chronik::util::HashToArray(txid), 1}}, + {.prev_out = {{}, 0x12345678}}, + }}; + + // Do lookup + rust::Vec<chronik_bridge::OutPoint> not_found; + rust::Vec<chronik_bridge::OutPoint> coins_to_uncache; + bridge.lookup_spent_coins(query_tx, not_found, coins_to_uncache); + + // One of the coins was not found + BOOST_CHECK_EQUAL(not_found.size(), 1); + BOOST_CHECK(not_found[0] == query_tx.inputs[2].prev_out); + + // Mempool UTXOs aren't in the cache, so lookup_spent_coins thinks they need + // to be uncached, which seems weird but is intended behavior. + BOOST_CHECK_EQUAL(coins_to_uncache.size(), 2); + BOOST_CHECK(coins_to_uncache[0] == query_tx.inputs[0].prev_out); + BOOST_CHECK(coins_to_uncache[1] == query_tx.inputs[1].prev_out); + BOOST_CHECK(!coins_cache.HaveCoinInCache(COutPoint(txid, 0))); + BOOST_CHECK(!coins_cache.HaveCoinInCache(COutPoint(txid, 1))); + + // lookup_spent_coins mutates our query_tx to set the queried coins + const rust::Vec<uint8_t> &script0 = query_tx.inputs[0].coin.output.script; + const rust::Vec<uint8_t> &script1 = query_tx.inputs[1].coin.output.script; + BOOST_CHECK_EQUAL(query_tx.inputs[0].coin.output.value, 1000); + BOOST_CHECK(CScript(script0.data(), script0.data() + script0.size()) == + anyoneP2sh); + BOOST_CHECK_EQUAL(query_tx.inputs[1].coin.output.value, + coinTx->vout[0].nValue / SATOSHI - 10000); + BOOST_CHECK(CScript(script1.data(), script1.data() + script1.size()) == + anyoneP2sh); + + // Mine tx + CreateAndProcessBlock({tx}, CScript() << OP_1, + &chainman.ActiveChainstate()); + // Coins are now in the cache + BOOST_CHECK(coins_cache.HaveCoinInCache(COutPoint(txid, 0))); + BOOST_CHECK(coins_cache.HaveCoinInCache(COutPoint(txid, 1))); + + // Write cache to DB & clear cache + coins_cache.Flush(); + BOOST_CHECK(!coins_cache.HaveCoinInCache(COutPoint(txid, 0))); + BOOST_CHECK(!coins_cache.HaveCoinInCache(COutPoint(txid, 1))); + + // lookup puts the coins back into the cache + bridge.lookup_spent_coins(query_tx, not_found, coins_to_uncache); + BOOST_CHECK_EQUAL(coins_to_uncache.size(), 2); + BOOST_CHECK(coins_to_uncache[0] == query_tx.inputs[0].prev_out); + BOOST_CHECK(coins_to_uncache[1] == query_tx.inputs[1].prev_out); + BOOST_CHECK(coins_cache.HaveCoinInCache(COutPoint(txid, 0))); + BOOST_CHECK(coins_cache.HaveCoinInCache(COutPoint(txid, 1))); + + // Now, we don't get any coins_to_uncache (because the call didn't add + // anything to it) + bridge.lookup_spent_coins(query_tx, not_found, coins_to_uncache); + BOOST_CHECK_EQUAL(coins_to_uncache.size(), 0); + + // Call uncache_coins to uncache the 1st coin + const std::vector<chronik_bridge::OutPoint> uncache_outpoints{ + query_tx.inputs[0].prev_out}; + bridge.uncache_coins( + rust::Slice(uncache_outpoints.data(), uncache_outpoints.size())); + // Only the 2nd coin is now in cache + BOOST_CHECK(!coins_cache.HaveCoinInCache(COutPoint(txid, 0))); + BOOST_CHECK(coins_cache.HaveCoinInCache(COutPoint(txid, 1))); +} + BOOST_FIXTURE_TEST_CASE(test_load_block, TestChain100Setup) { const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); ChainstateManager &chainman = *Assert(m_node.chainman); const CBlockIndex &tip = *chainman.ActiveTip(); BOOST_CHECK_EQUAL(bridge.load_block(tip)->GetHash(), tip.GetBlockHash()); { CDataStream expected(SER_NETWORK, PROTOCOL_VERSION); CDataStream actual(SER_NETWORK, PROTOCOL_VERSION); expected << params.GenesisBlock(); actual << *bridge.load_block(*tip.GetAncestor(0)); BOOST_CHECK_EQUAL(HexStr(actual), HexStr(expected)); } } BOOST_FIXTURE_TEST_CASE(test_get_block_ancestor, TestChain100Setup) { const CChainParams ¶ms = GetConfig().GetChainParams(); ChainstateManager &chainman = *Assert(m_node.chainman); const CBlockIndex &tip = *chainman.ActiveTip(); // Block 100 is the tip BOOST_CHECK_EQUAL( chronik_bridge::get_block_ancestor(tip, 100).GetBlockHash(), tip.GetBlockHash()); // Block 99 is the prev of the tip BOOST_CHECK_EQUAL( chronik_bridge::get_block_ancestor(tip, 99).GetBlockHash(), tip.GetBlockHeader().hashPrevBlock); // Genesis block is block 0 BOOST_CHECK_EQUAL(chronik_bridge::get_block_ancestor(tip, 0).GetBlockHash(), params.GenesisBlock().GetHash()); // Block -1 doesn't exist BOOST_CHECK_THROW(chronik_bridge::get_block_ancestor(tip, -1), chronik_bridge::block_index_not_found); // Block 101 doesn't exist BOOST_CHECK_THROW(chronik_bridge::get_block_ancestor(tip, tip.nHeight + 1), chronik_bridge::block_index_not_found); } BOOST_FIXTURE_TEST_CASE(test_get_block_info, TestChain100Setup) { const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); ChainstateManager &chainman = *Assert(m_node.chainman); const CBlockIndex &tip = *chainman.ActiveTip(); chronik_bridge::BlockInfo expected_genesis_info{ .hash = chronik::util::HashToArray(params.GenesisBlock().GetHash()), .height = 0}; BOOST_CHECK(chronik_bridge::get_block_info(*tip.GetAncestor(0)) == expected_genesis_info); chronik_bridge::BlockInfo expected_tip_info{ .hash = chronik::util::HashToArray(tip.GetBlockHash()), .height = tip.nHeight}; BOOST_CHECK(chronik_bridge::get_block_info(tip) == expected_tip_info); } BOOST_FIXTURE_TEST_CASE(test_bridge_broadcast_tx, TestChain100Setup) { ChainstateManager &chainman = *Assert(m_node.chainman); const CChainParams ¶ms = GetConfig().GetChainParams(); const chronik_bridge::ChronikBridge bridge(params.GetConsensus(), m_node); CScript anyoneScript = CScript() << OP_1; CScript anyoneP2sh = GetScriptForDestination(ScriptHash(anyoneScript)); CBlock coinsBlock = CreateAndProcessBlock({}, anyoneP2sh, &chainman.ActiveChainstate()); mineBlocks(100); const CTransactionRef coinTx = coinsBlock.vtx[0]; CScript scriptPad = CScript() << OP_RETURN << std::vector<uint8_t>(100); CMutableTransaction tx; tx.vin = {CTxIn(coinTx->GetId(), 0)}; tx.vout = { CTxOut(coinTx->vout[0].nValue - 10000 * SATOSHI, scriptPad), }; { // Failed broadcast: mempool rejected tx CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << tx; rust::Slice<const uint8_t> raw_tx{(const uint8_t *)ss.data(), ss.size()}; BOOST_CHECK_EXCEPTION( bridge.broadcast_tx(raw_tx, 10000), std::runtime_error, [](const std::runtime_error &ex) { BOOST_CHECK_EQUAL( ex.what(), "Transaction rejected by mempool: " "mandatory-script-verify-flag-failed (Operation " "not valid with the current stack size)"); return true; }); } tx.vin[0].scriptSig = CScript() << std::vector(anyoneScript.begin(), anyoneScript.end()); { // Failed broadcast from excessive fee (10000 > 9999). CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << tx; rust::Slice<const uint8_t> raw_tx{(const uint8_t *)ss.data(), ss.size()}; BOOST_CHECK_EXCEPTION( bridge.broadcast_tx(raw_tx, 9999), std::runtime_error, [](const std::runtime_error &ex) { BOOST_CHECK_EQUAL(ex.what(), "Fee exceeds maximum configured by user " "(e.g. -maxtxfee, maxfeerate)"); return true; }); } { // Successful broadcast CDataStream ss(SER_NETWORK, PROTOCOL_VERSION); ss << tx; rust::Slice<const uint8_t> raw_tx{(const uint8_t *)ss.data(), ss.size()}; BOOST_CHECK_EQUAL(HexStr(bridge.broadcast_tx(raw_tx, 10000)), HexStr(chronik::util::HashToArray(tx.GetId()))); // Broadcast again simply returns the hash (fee check skipped here) BOOST_CHECK_EQUAL(HexStr(bridge.broadcast_tx(raw_tx, 9999)), HexStr(chronik::util::HashToArray(tx.GetId()))); } } BOOST_AUTO_TEST_SUITE_END()