diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2704867315cef..8f3e41eac1e11 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -192,6 +192,7 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL httpserver.cpp i2p.cpp index/base.cpp + index/bip352.cpp index/blockfilterindex.cpp index/coinstatsindex.cpp index/txindex.cpp @@ -280,6 +281,7 @@ target_link_libraries(bitcoin_node $ leveldb minisketch + secp256k1 univalue Boost::headers $ diff --git a/src/common/bip352.cpp b/src/common/bip352.cpp index 0cb6cdc8a79f6..f9bfca10bbeeb 100644 --- a/src/common/bip352.cpp +++ b/src/common/bip352.cpp @@ -155,6 +155,18 @@ std::optional CreateInputPubkeysTweak( return prevouts_summary; } +bool MaybeSilentPayment(const CTransactionRef &tx) { + if (tx->IsCoinBase()) return false; + + if (std::none_of(tx->vout.begin(), tx->vout.end(), [](const CTxOut& txout) { + return txout.scriptPubKey.IsPayToTaproot(); + })) { + return false; + } + + return true; +} + std::optional GetSilentPaymentsPrevoutsSummary(const std::vector& vin, const std::map& coins) { // Extract the keys from the inputs diff --git a/src/common/bip352.h b/src/common/bip352.h index 5f919dfc8d8a3..d2d7b09963216 100644 --- a/src/common/bip352.h +++ b/src/common/bip352.h @@ -117,6 +117,17 @@ std::pair CreateLabelTweak(const CKey& scan_key, const int m); */ V0SilentPaymentDestination GenerateSilentPaymentLabeledAddress(const V0SilentPaymentDestination& recipient, const uint256& label); +/** + * @brief: Check if a transaction could be a silent payment. + * + * A coinbase transaction can't be a silent payment. + * A transaction with no Taproot outputs can't be a silent payment. + * + * @param tx The transaction to check + * @return false if a transaction can't be a silent payment, true otherwise. + */ +bool MaybeSilentPayment(const CTransactionRef &tx); + /** * @brief Get silent payment public data from transaction inputs. * diff --git a/src/index/base.cpp b/src/index/base.cpp index 82259ac046a6e..140809af7faec 100644 --- a/src/index/base.cpp +++ b/src/index/base.cpp @@ -73,8 +73,8 @@ void BaseIndex::DB::WriteBestBlock(CDBBatch& batch, const CBlockLocator& locator batch.Write(DB_BEST_BLOCK, locator); } -BaseIndex::BaseIndex(std::unique_ptr chain, std::string name) - : m_chain{std::move(chain)}, m_name{std::move(name)} {} +BaseIndex::BaseIndex(std::unique_ptr chain, std::string name, int start_height) + : m_chain{std::move(chain)}, m_name{std::move(name)}, m_start_height{std::move(start_height)} {} BaseIndex::~BaseIndex() { @@ -131,11 +131,16 @@ bool BaseIndex::Init() return true; } -static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain& chain) EXCLUSIVE_LOCKS_REQUIRED(cs_main) +static const CBlockIndex* NextSyncBlock(const CBlockIndex* pindex_prev, CChain& chain, int start_height) EXCLUSIVE_LOCKS_REQUIRED(cs_main) { AssertLockHeld(cs_main); if (!pindex_prev) { + // pindex_prev is null, we are starting our sync. Return the genesis block + // or start from the specified start_height. + if (start_height > 0) { + return chain[start_height]; + } return chain.Genesis(); } @@ -200,7 +205,7 @@ void BaseIndex::Sync() return; } - const CBlockIndex* pindex_next = WITH_LOCK(cs_main, return NextSyncBlock(pindex, m_chainstate->m_chain)); + const CBlockIndex* pindex_next = WITH_LOCK(cs_main, return NextSyncBlock(pindex, m_chainstate->m_chain, m_start_height)); // If pindex_next is null, it means pindex is the chain tip, so // commit data indexed so far. if (!pindex_next) { @@ -214,12 +219,17 @@ void BaseIndex::Sync() // attached while m_synced is still false, and it would not be // indexed. LOCK(::cs_main); - pindex_next = NextSyncBlock(pindex, m_chainstate->m_chain); if (!pindex_next) { m_synced = true; break; } } + // If pindex_next is our first block and we are starting from a custom height, + // set pindex to be the previous block. This ensures we test that we can still rewind + // from our custom start height in the event of a reorg. + if (pindex_next->nHeight == m_start_height && m_start_height > 0) { + pindex = pindex_next->pprev; + } if (pindex_next->pprev != pindex && !Rewind(pindex, pindex_next->pprev)) { FatalErrorf("Failed to rewind %s to a previous chain tip", GetName()); return; diff --git a/src/index/base.h b/src/index/base.h index 4131b06cad9de..5e300c8590b28 100644 --- a/src/index/base.h +++ b/src/index/base.h @@ -104,6 +104,7 @@ class BaseIndex : public CValidationInterface std::unique_ptr m_chain; Chainstate* m_chainstate{nullptr}; const std::string m_name; + const int m_start_height{0}; void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override; @@ -131,7 +132,7 @@ class BaseIndex : public CValidationInterface void SetBestBlockIndex(const CBlockIndex* block); public: - BaseIndex(std::unique_ptr chain, std::string name); + BaseIndex(std::unique_ptr chain, std::string name, int start_height = 0); /// Destructor interrupts sync thread if running and blocks until it exits. virtual ~BaseIndex(); diff --git a/src/index/bip352.cpp b/src/index/bip352.cpp new file mode 100644 index 0000000000000..f7b26749d5322 --- /dev/null +++ b/src/index/bip352.cpp @@ -0,0 +1,144 @@ +// Copyright (c) 2023-present 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 + +constexpr uint8_t DB_SILENT_PAYMENT_INDEX{'s'}; +/* Save space on mainnet by starting the index at Taproot activation. + * Copying the height here assuming DEPLOYMENT_TAPROOT will be dropped: + * https://github.com/bitcoin/bitcoin/pull/26201/ + * Only apply this storage optimization on mainnet. + */ +const int TAPROOT_MAINNET_ACTIVATION_HEIGHT{709632}; + +std::unique_ptr g_bip352_index; +std::unique_ptr g_bip352_ct_index; + +/** Access to the silent payment index database (indexes/bip352/) */ +class BIP352Index::DB : public BaseIndex::DB +{ +public: + explicit DB(fs::path file_name, size_t n_cache_size, bool f_memory = false, bool f_wipe = false); + + bool WriteSilentPayments(const std::pair& kv); +}; + +BIP352Index::DB::DB(fs::path file_name, size_t n_cache_size, bool f_memory, bool f_wipe) : + BaseIndex::DB(gArgs.GetDataDirNet() / "indexes" / file_name, n_cache_size, f_memory, f_wipe) +{} + +bool BIP352Index::DB::WriteSilentPayments(const std::pair& kv) +{ + CDBBatch batch(*this); + batch.Write(std::make_pair(DB_SILENT_PAYMENT_INDEX, kv.first), kv.second); + return WriteBatch(batch); +} + +BIP352Index::BIP352Index(bool cut_through, std::unique_ptr chain, size_t n_cache_size, bool f_memory, bool f_wipe) + : BaseIndex(std::move(chain), strprintf("bip352 %sindex", cut_through ? "cut-through " : ""), /*start_height=*/Params().IsTestChain() ? 0 : TAPROOT_MAINNET_ACTIVATION_HEIGHT), m_db(std::make_unique(fs::u8path(strprintf("bip352%s", cut_through ? "ct" : "")), n_cache_size, f_memory, f_wipe)) +{ + m_cut_through = cut_through; +} + +BIP352Index::~BIP352Index() = default; + +bool BIP352Index::GetSilentPaymentKeys(const std::vector& txs, const CBlockUndo& block_undo, tweak_index_entry& index_entry) const +{ + assert(txs.size() - 1 == block_undo.vtxundo.size()); + + for (size_t i=0; i < txs.size(); i++) { + auto& tx = txs.at(i); + + if (!bip352::MaybeSilentPayment(tx)) continue; + + // -1 as blockundo does not have coinbase tx + CTxUndo undoTX{block_undo.vtxundo.at(i - 1)}; + std::map coins; + + for (size_t j = 0; j < tx->vin.size(); j++) { + coins[tx->vin.at(j).prevout] = undoTX.vprevout.at(j); + } + + std::optional tweaked_pk = bip352::GetSerializedSilentPaymentsPrevoutsSummary(tx->vin, coins); + if (tweaked_pk) { + // Used to filter dust. To keep the index small we use only one byte + // and measure in hexasats. + uint8_t max_output_hsat = 0; + for (const CTxOut& txout : tx->vout) { + if (!txout.scriptPubKey.IsPayToTaproot()) continue; + uint8_t output_hsat = txout.nValue > max_dust_threshold ? UINT8_MAX : txout.nValue >> dust_shift; + max_output_hsat = std::max(output_hsat, max_output_hsat); + } + + if (m_cut_through) { + // Skip entry if all outputs have been spent. + // This is only effective when the index is generated while + // the tip is far ahead. + // + // This is done after calculating the tweak in order to minimize + // the number of UTXO lookups. + LOCK(cs_main); + const CCoinsViewCache& coins_cache = m_chainstate->CoinsTip(); + + uint32_t spent{0}; + for (size_t j{0}; j < tx->vout.size(); j++) { + COutPoint outpoint(tx->GetHash(), j); + // Many new blocks may be processed while generating the index, + // in between HaveCoin calls. This is not a problem, because + // the cut-through index can safely have false positives. + if (!coins_cache.HaveCoin(outpoint)) spent++; + } + if (spent == tx->vout.size()) continue; + } + index_entry.emplace_back(std::make_pair(tweaked_pk.value(), max_output_hsat)); + } + } + + return true; +} + +interfaces::Chain::NotifyOptions BIP352Index::CustomOptions() +{ + interfaces::Chain::NotifyOptions options; + options.connect_undo_data = true; + return options; +} + +bool BIP352Index::CustomAppend(const interfaces::BlockInfo& block) +{ + // Exclude genesis block transaction because outputs are not spendable. This + // is needed on non-mainnet chains because m_start_height is 0 by default. + if (block.height == 0) return true; + + // Exclude pre-taproot + if (block.height < m_start_height) return true; + + tweak_index_entry index_entry; + GetSilentPaymentKeys(Assert(block.data)->vtx, *Assert(block.undo_data), index_entry); + + return m_db->WriteSilentPayments(make_pair(block.hash, index_entry)); +} + +bool BIP352Index::FindSilentPayment(const uint256& block_hash, tweak_index_entry& index_entry) const +{ + return m_db->Read(std::make_pair(DB_SILENT_PAYMENT_INDEX, block_hash), index_entry); +} + +BaseIndex::DB& BIP352Index::GetDB() const { return *m_db; } diff --git a/src/index/bip352.h b/src/index/bip352.h new file mode 100644 index 0000000000000..1bf5493b62a7e --- /dev/null +++ b/src/index/bip352.h @@ -0,0 +1,83 @@ +// Copyright (c) 2023-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_INDEX_BIP352_H +#define BITCOIN_INDEX_BIP352_H + +#include +#include +#include + +class COutPoint; +class CBlockUndo; + +static constexpr bool DEFAULT_BIP352_INDEX{false}; +static constexpr bool DEFAULT_BIP352_CT_INDEX{false}; + +/** + * This index is used to look up the tweaked sum of eligible public keys for a + * given transaction hash. See BIP352. + * + * Currently only silent payments v0 exists. Future versions may expand the + * existing index or create a (perhaps overlapping) new one. + */ +class BIP352Index final : public BaseIndex +{ +public: + /** + * For each block hash we store and array of transactions. For each + * transaction we store: + * - the tweaked pubkey sum + * - the highest output amount (for dust filtering) + */ + using tweak_index_entry = std::vector>; + static constexpr uint8_t dust_shift{4}; // Hexasat + /* Maximum dust threshold that we can filter */ + static constexpr CAmount max_dust_threshold{CAmount(UINT8_MAX - 1) << dust_shift}; + + class DB; + +private: + const std::unique_ptr m_db; + + /** Whether this index has transaction cut-through enabled */ + bool m_cut_through{false}; + + bool AllowPrune() const override { return false; } + + /** + * Derive the silent payment tweaked public key for every block transaction. + * + * + * @param[in] txs all block transactions + * @param[in] block_undo block undo data + * @param[out] index_entry the tweaked public keys, only for transactions that have one + * @return false if something went wrong + */ + bool GetSilentPaymentKeys(const std::vector& txs, const CBlockUndo& block_undo, tweak_index_entry& index_entry) const; + +protected: + interfaces::Chain::NotifyOptions CustomOptions() override; + + bool CustomAppend(const interfaces::BlockInfo& block) override; + + BaseIndex::DB& GetDB() const override; +public: + + explicit BIP352Index(bool cut_through, std::unique_ptr chain, size_t n_cache_size, bool f_memory = false, bool f_wipe = false); + + // Destructor is declared because this class contains a unique_ptr to an incomplete type. + virtual ~BIP352Index() override; + + bool FindSilentPayment(const uint256& block_hash, tweak_index_entry& index_entry) const; +}; + +/// The global BIP325 index. May be null. +extern std::unique_ptr g_bip352_index; + +/// The global BIP325 with transaction cut-through index. May be null. +extern std::unique_ptr g_bip352_ct_index; + + +#endif // BITCOIN_INDEX_BIP352_H diff --git a/src/init.cpp b/src/init.cpp index b2f59a60b7278..3a2e2a771beaf 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -361,6 +362,8 @@ void Shutdown(NodeContext& node) for (auto* index : node.indexes) index->Stop(); if (g_txindex) g_txindex.reset(); if (g_coin_stats_index) g_coin_stats_index.reset(); + if (g_bip352_index) g_bip352_index.reset(); + if (g_bip352_ct_index) g_bip352_ct_index.reset(); DestroyAllBlockFilterIndexes(); node.indexes.clear(); // all instances are nullptr now @@ -526,6 +529,12 @@ void SetupServerArgs(ArgsManager& argsman, bool can_listen_ipc) argsman.AddArg("-shutdownnotify=", "Execute command immediately before beginning shutdown. The need for shutdown may be urgent, so be careful not to delay it long (if the command doesn't require interaction with the server, consider having it fork into the background).", ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); #endif argsman.AddArg("-txindex", strprintf("Maintain a full transaction index, used by the getrawtransaction rpc call (default: %u)", DEFAULT_TXINDEX), ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-bip352index", + strprintf("Maintain an index of BIP352 v0 tweaked public keys. (default: %s)", DEFAULT_BIP352_INDEX), + ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); + argsman.AddArg("-bip352ctindex", + strprintf("Maintain an index of BIP352 v0 tweaked public keys with transaction cut-through. (default: %s)", DEFAULT_BIP352_CT_INDEX), + ArgsManager::ALLOW_ANY, OptionsCategory::OPTIONS); argsman.AddArg("-blockfilterindex=", strprintf("Maintain an index of compact filters by block (default: %s, values: %s).", DEFAULT_BLOCKFILTERINDEX, ListBlockFilterTypes()) + " If is not supplied or if = 1, indexes for all known types are enabled.", @@ -979,6 +988,10 @@ bool AppInitParameterInteraction(const ArgsManager& args) if (args.GetIntArg("-prune", 0)) { if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) return InitError(_("Prune mode is incompatible with -txindex.")); + if (args.GetBoolArg("-bip352index", DEFAULT_BIP352_INDEX)) + return InitError(_("Prune mode is incompatible with -bip352index.")); + if (args.GetBoolArg("-bip352index", DEFAULT_BIP352_CT_INDEX)) + return InitError(_("Prune mode is incompatible with -bip352ctindex.")); if (args.GetBoolArg("-reindex-chainstate", false)) { return InitError(_("Prune mode is incompatible with -reindex-chainstate. Use full -reindex instead.")); } @@ -1740,6 +1753,12 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) if (args.GetBoolArg("-txindex", DEFAULT_TXINDEX)) { LogInfo("* Using %.1f MiB for transaction index database", index_cache_sizes.tx_index * (1.0 / 1024 / 1024)); } + if (args.GetBoolArg("-bip352index", DEFAULT_BIP352_INDEX)) { + LogPrintf("* Using %.1f MiB for BIP352 index database\n", index_cache_sizes.bip352_index * (1.0 / 1024 / 1024)); + } + if (args.GetBoolArg("-bip352ctindex", DEFAULT_BIP352_CT_INDEX)) { + LogPrintf("* Using %.1f MiB for BIP352 cut-through index database\n", index_cache_sizes.bip352_ct_index * (1.0 / 1024 / 1024)); + } for (BlockFilterType filter_type : g_enabled_filter_types) { LogInfo("* Using %.1f MiB for %s block filter index database", index_cache_sizes.filter_index * (1.0 / 1024 / 1024), BlockFilterTypeName(filter_type)); @@ -1818,6 +1837,15 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info) node.indexes.emplace_back(g_coin_stats_index.get()); } + if (args.GetBoolArg("-bip352index", DEFAULT_BIP352_INDEX)) { + g_bip352_index = std::make_unique(/*cut_through=*/false, interfaces::MakeChain(node), index_cache_sizes.bip352_index, false, do_reindex); + node.indexes.emplace_back(g_bip352_index.get()); + } + if (args.GetBoolArg("-bip352ctindex", DEFAULT_BIP352_CT_INDEX)) { + g_bip352_ct_index = std::make_unique(/*cut_through=*/true, interfaces::MakeChain(node), index_cache_sizes.bip352_ct_index, false, do_reindex); + node.indexes.emplace_back(g_bip352_ct_index.get()); + } + // Init indexes for (auto index : node.indexes) if (!index->Init()) return false; diff --git a/src/node/caches.cpp b/src/node/caches.cpp index d5d69fc204430..9357779b23363 100644 --- a/src/node/caches.cpp +++ b/src/node/caches.cpp @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,8 @@ static constexpr size_t MAX_TX_INDEX_CACHE{1024_MiB}; static constexpr size_t MAX_FILTER_INDEX_CACHE{1024_MiB}; //! Maximum dbcache size on 32-bit systems. static constexpr size_t MAX_32BIT_DBCACHE{1024_MiB}; +//! Max memory allocated to the BIP352 index caches in bytes. +static constexpr size_t MAX_BIP352_INDEX_CACHE{1024_MiB}; namespace node { CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes) @@ -36,12 +39,16 @@ CacheSizes CalculateCacheSizes(const ArgsManager& args, size_t n_indexes) IndexCacheSizes index_sizes; index_sizes.tx_index = std::min(total_cache / 8, args.GetBoolArg("-txindex", DEFAULT_TXINDEX) ? MAX_TX_INDEX_CACHE : 0); + index_sizes.bip352_index = std::min(total_cache / 8, args.GetBoolArg("-bip352index", DEFAULT_BIP352_INDEX) ? MAX_BIP352_INDEX_CACHE : 0); + index_sizes.bip352_ct_index = std::min(total_cache / 8, args.GetBoolArg("-bip352ctindex", DEFAULT_BIP352_CT_INDEX) ? MAX_BIP352_INDEX_CACHE << 20 : 0); + total_cache -= index_sizes.tx_index; if (n_indexes > 0) { size_t max_cache = std::min(total_cache / 8, MAX_FILTER_INDEX_CACHE); index_sizes.filter_index = max_cache / n_indexes; total_cache -= index_sizes.filter_index * n_indexes; } + return {index_sizes, kernel::CacheSizes{total_cache}}; } } // namespace node diff --git a/src/node/caches.h b/src/node/caches.h index f24e9cc910304..722774b1f0008 100644 --- a/src/node/caches.h +++ b/src/node/caches.h @@ -19,8 +19,10 @@ static constexpr size_t DEFAULT_DB_CACHE{DEFAULT_KERNEL_CACHE}; namespace node { struct IndexCacheSizes { + size_t bip352_index{0}; size_t tx_index{0}; size_t filter_index{0}; + size_t bip352_ct_index{0}; }; struct CacheSizes { IndexCacheSizes index; diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp index cfc0379f68304..d93fd74c50127 100644 --- a/src/rpc/blockchain.cpp +++ b/src/rpc/blockchain.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -737,6 +738,73 @@ const RPCResult getblock_vin{ } }; +static RPCHelpMan getsilentpaymentblockdata() +{ + return RPCHelpMan{"getsilentpaymentblockdata", + "Returns an array of hex-encoded strings data for the tweaked public key sum of candidate silent transaction inputs in each transaction.\n", + { + {"block_hash", RPCArg::Type::STR_HEX, RPCArg::Optional::NO, "The block hash"}, + {"dust", RPCArg::Type::AMOUNT, RPCArg::Default{CAmount(0)}, strprintf("Dust threshold in satoshi (max %d): all outputs must be greater or equal. This filter has limited precision.", BIP352Index::max_dust_threshold)}, + }, + { + RPCResult{ + RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::ARR, "bip352_tweaks", "The tweaked public key for each transaction that could be a silent payment", + {{RPCResult::Type::STR_HEX, "tweak", "Hex-encoded data for the public key sum of candidate silent transaction inputs in the transaction"}} + } + }}, + }, + RPCExamples{ + HelpExampleCli("getsilentpaymentblockdata", "\"00000000000000000002cbdf64ae445b53b545ba1e960f9e83787130d1530484\"") + + HelpExampleRpc("getsilentpaymentblockdata", "\"00000000000000000002cbdf64ae445b53b545ba1e960f9e83787130d1530484\"") + }, + [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue +{ + // TODO: add cut-through argument, check which index to use here + if (!g_bip352_index) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Requires bip352index"); + } + + uint256 block_hash(ParseHashV(request.params[0], "block_hash")); + { + ChainstateManager& chainman = EnsureAnyChainman(request.context); + const CBlockIndex* block_index; + LOCK(cs_main); + block_index = chainman.m_blockman.LookupBlockIndex(block_hash); + if (!block_index) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Block not found"); + } + } + + CAmount dust_threshold{0}; + if (!request.params[1].isNull()) { + dust_threshold = AmountFromValue(request.params[1], 0); + if (dust_threshold > BIP352Index::max_dust_threshold) { + throw JSONRPCError(RPC_INVALID_PARAMETER, strprintf("dust argument must be <= %d", BIP352Index::max_dust_threshold)); + } + } + + BIP352Index::tweak_index_entry tweak_index_entry; + if (!g_bip352_index->FindSilentPayment(block_hash, tweak_index_entry)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, "Block has not been indexed yet"); + } + + UniValue ret(UniValue::VOBJ); + UniValue tweaks_res(UniValue::VARR); + + for (const auto& entry : tweak_index_entry) { + if ((CAmount(entry.second) << BIP352Index::dust_shift) >= dust_threshold) { + tweaks_res.push_back(HexStr(entry.first)); + } + } + + ret.pushKV("bip352_tweaks", tweaks_res); + return ret; +}, + }; +} + static RPCHelpMan getblock() { return RPCHelpMan{ @@ -3457,6 +3525,7 @@ void RegisterBlockchainRPCCommands(CRPCTable& t) {"blockchain", &getblockfrompeer}, {"blockchain", &getblockhash}, {"blockchain", &getblockheader}, + {"blockchain", &getsilentpaymentblockdata}, {"blockchain", &getchaintips}, {"blockchain", &getdifficulty}, {"blockchain", &getdeploymentinfo}, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 1fbe62d3d389c..d2105519f54cd 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -314,6 +314,7 @@ static const CRPCConvertParam vRPCConvertParams[] = { "stop", 0, "wait" }, { "addnode", 2, "v2transport" }, { "addconnection", 2, "v2transport" }, + { "getsilentpaymentblockdata", 1, "dust"}, }; // clang-format on diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index a5da8785df46a..68a10ac990a70 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -395,6 +396,14 @@ static RPCHelpMan getindexinfo() result.pushKVs(SummaryToJSON(g_coin_stats_index->GetSummary(), index_name)); } + if (g_bip352_index) { + result.pushKVs(SummaryToJSON(g_bip352_index->GetSummary(), index_name)); + } + + if (g_bip352_ct_index) { + result.pushKVs(SummaryToJSON(g_bip352_ct_index->GetSummary(), index_name)); + } + ForEachBlockFilterIndex([&result, &index_name](const BlockFilterIndex& index) { result.pushKVs(SummaryToJSON(index.GetSummary(), index_name)); }); diff --git a/src/test/fuzz/rpc.cpp b/src/test/fuzz/rpc.cpp index 580a6338a849d..f463d7d14ca1c 100644 --- a/src/test/fuzz/rpc.cpp +++ b/src/test/fuzz/rpc.cpp @@ -185,6 +185,7 @@ const std::vector RPC_COMMANDS_SAFE_FOR_FUZZING{ "waitforblock", "waitforblockheight", "waitfornewblock", + "getsilentpaymentblockdata" }; std::string ConsumeScalarRPCArgument(FuzzedDataProvider& fuzzed_data_provider, bool& good_data) diff --git a/test/functional/rpc_getsilentpaymentblockdata.py b/test/functional/rpc_getsilentpaymentblockdata.py new file mode 100755 index 0000000000000..03db19979f625 --- /dev/null +++ b/test/functional/rpc_getsilentpaymentblockdata.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# Copyright (c) 2023-present The Bitcoin Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. + +# +# Test getsilentpaymentblockdata rpc call +# + +from test_framework.test_framework import BitcoinTestFramework +from test_framework.util import ( + assert_equal, +) +from test_framework.blocktools import ( + TIME_GENESIS_BLOCK, +) +from test_framework.descriptors import ( + descsum_create, +) + +class GetsilentpaymentblockdataTest(BitcoinTestFramework): + def skip_test_if_missing_module(self): + self.skip_if_no_wallet() + + def set_test_params(self): + self.num_nodes = 1 + + self.extra_args = [[ + '-bip352index', + ]] + + self.setup_clean_chain = True + + def run_test(self): + node = self.nodes[0] + mocktime = 1702294115 + node.setmocktime(mocktime) + node.createwallet(wallet_name="w", blank=True) + wallet = self.nodes[0].get_wallet_rpc("w") + res = wallet.importdescriptors([{ + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/84h/1h/0h/0/*)'), + 'timestamp': TIME_GENESIS_BLOCK, + 'active': True, + }, + { + 'desc': descsum_create('wpkh(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/84h/1h/0h/1/*)'), + 'timestamp': TIME_GENESIS_BLOCK, + 'active': True, + 'internal': True, + }, + { + 'desc': descsum_create('tr(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/86h/1h/0h/0/*)'), + 'timestamp': TIME_GENESIS_BLOCK, + 'active': True, + }, + { + 'desc': descsum_create('tr(tprv8ZgxMBicQKsPeuVhWwi6wuMQGfPKi9Li5GtX35jVNknACgqe3CY4g5xgkfDDJcmtF7o1QnxWDRYw4H5P26PXq7sbcUkEqeR4fg3Kxp2tigg/86h/1h/0h/1/*)'), + 'timestamp': TIME_GENESIS_BLOCK, + 'active': True, + 'internal': True, + } + ]) + assert all([r["success"] for r in res]) + self.log.info("Mine fresh coins to a taproot addresses") + mine_tr = wallet.getnewaddress(address_type="bech32m") + self.generatetoaddress(node, 1, mine_tr) + self.generate(node, 100) + + self.log.info("Blocks with only a coinbase won't have any silent payment data") + silent_data = node.getsilentpaymentblockdata(node.getbestblockhash()) + assert_equal(silent_data['bip352_tweaks'], []) + + self.log.info("Spending from taproot to segwit won't result in silent payment data") + dest_sw = wallet.getnewaddress(address_type="bech32") + txid = wallet.send(outputs={dest_sw: 1}, options={'change_position': 1})['txid'] + assert_equal(txid, '2c09a66f51a40888e1a3ab287d96fef5da1f6d7914b570d80e88631c8b823dd2') + self.generate(node, 1) + + silent_data = node.getsilentpaymentblockdata(node.getbestblockhash()) + assert_equal(silent_data['bip352_tweaks'], []) + + self.log.info("Spending (from taproot) to taproot results in silent payment data") + dest_tr = wallet.getnewaddress(address_type="bech32m") + wallet.send(outputs={dest_tr: 1}, options={'inputs': [{'txid': txid, 'vout': 1}], 'add_inputs': False, 'change_position': 1}) + self.generate(node, 1) + silent_data = node.getsilentpaymentblockdata(node.getbestblockhash()) + assert_equal(silent_data['bip352_tweaks'], ['0292d9514e20f2eb852e904b122c3fbcb20004dceb129f1013feab3b44240202da']) + +if __name__ == '__main__': + GetsilentpaymentblockdataTest(__file__).main() diff --git a/test/functional/test_runner.py b/test/functional/test_runner.py index f2e514a7a4658..a4563d876a075 100755 --- a/test/functional/test_runner.py +++ b/test/functional/test_runner.py @@ -345,6 +345,7 @@ 'wallet_orphanedreward.py', 'wallet_timelock.py', 'p2p_permissions.py', + 'rpc_getsilentpaymentblockdata.py', 'feature_blocksdir.py', 'wallet_startup.py', 'feature_remove_pruned_files_on_startup.py',