I believe as things are currently implemented this is an empty array, but this would be useful information IMO. I would assume the data serialization format of the undo data is not widely known outside of this project, and I have spent more than a couple hours looking into it in the past. Could we consider creating a more detailed type here? I would suggest a "Coin" that contains the script and amount after disk deserialization, then defining a list of coins as the undo data. For users that are uninterested, the default behavior could omit the disk read and leave this empty.
<details>
<summary>AI generated patch suggestion</summary>
diff --git a/src/interfaces/chain.h b/src/interfaces/chain.h
index a0e8a146e7..b5ca6a1242 100644
--- a/src/interfaces/chain.h
+++ b/src/interfaces/chain.h
@@ -330,7 +330,12 @@ public:
//! Some notifications are asynchronous and may still execute after the handler is disconnected.
//! Use waitForNotifications() after the handler is disconnected to ensure all pending notifications
//! have been processed.
- virtual std::unique_ptr<Handler> handleNotifications(std::shared_ptr<Notifications> notifications) = 0;
+ //! If include_undo_data is true, blockConnected and blockDisconnected notifications include the
+ //! block's undo data (spent coins and their amounts), which lets clients maintain their own
+ //! UTXO/index state. It is disabled by default because reading undo data from disk adds
+ //! overhead to every notification.
+ virtual std::unique_ptr<Handler> handleNotifications(std::shared_ptr<Notifications> notifications,
+ bool include_undo_data = false) = 0;
//! Wait for pending notifications to be processed unless block hash points to the current
//! chain tip.
diff --git a/src/ipc/capnp/chain.capnp b/src/ipc/capnp/chain.capnp
index 7351a3c9b9..8208318ee7 100644
--- a/src/ipc/capnp/chain.capnp
+++ b/src/ipc/capnp/chain.capnp
@@ -71,7 +71,7 @@ interface Chain $Proxy.wrap("interfaces::Chain") {
initWarning [@36](/bitcoin-bitcoin/contributor/36/) (context :Proxy.Context, message :Common.BilingualStr) -> ();
initError [@37](/bitcoin-bitcoin/contributor/37/) (context :Proxy.Context, message :Common.BilingualStr) -> ();
showProgress [@38](/bitcoin-bitcoin/contributor/38/) (context :Proxy.Context, title :Text, progress :Int32, resumePossible :Bool) -> ();
- handleNotifications [@39](/bitcoin-bitcoin/contributor/39/) (context :Proxy.Context, notifications :ChainNotifications) -> (result :Handler.Handler);
+ handleNotifications [@39](/bitcoin-bitcoin/contributor/39/) (context :Proxy.Context, notifications :ChainNotifications, includeUndoData :Bool = false) -> (result :Handler.Handler);
waitForNotificationsIfTipChanged [@40](/bitcoin-bitcoin/contributor/40/) (context :Proxy.Context, oldTip :Data) -> ();
waitForNotifications [@41](/bitcoin-bitcoin/contributor/41/) (context :Proxy.Context) -> ();
handleRpc [@42](/bitcoin-bitcoin/contributor/42/) (context :Proxy.Context, command :RPCCommand) -> (result :Handler.Handler);
@@ -165,6 +165,14 @@ struct FoundBlockResult {
found [@9](/bitcoin-bitcoin/contributor/9/) :Bool;
}
+struct Coin {
+ # Consumer-friendly representation of a spent coin in a block's undo data.
+ amount [@0](/bitcoin-bitcoin/contributor/0/) :Int64;
+ isCoinbase [@1](/bitcoin-bitcoin/contributor/1/) :Bool;
+ script [@2](/bitcoin-bitcoin/contributor/2/) :Data;
+ height [@3](/bitcoin-bitcoin/contributor/3/) :UInt32;
+}
+
struct BlockInfo $Proxy.wrap("interfaces::BlockInfo") {
# Fields skipped below with Proxy.skip are pointer fields manually handled
# by CustomBuildMessage / CustomPassMessage overloads.
@@ -174,7 +182,7 @@ struct BlockInfo $Proxy.wrap("interfaces::BlockInfo") {
fileNumber [@3](/bitcoin-bitcoin/contributor/3/) :Int32 = -1 $Proxy.name("file_number");
dataPos [@4](/bitcoin-bitcoin/contributor/4/) :UInt32 = 0 $Proxy.name("data_pos");
data [@5](/bitcoin-bitcoin/contributor/5/) :Data $Proxy.skip;
- undoData [@6](/bitcoin-bitcoin/contributor/6/) :Data $Proxy.skip;
+ undoCoins [@6](/bitcoin-bitcoin/contributor/6/) :List(Coin) $Proxy.skip;
chainTimeMax [@7](/bitcoin-bitcoin/contributor/7/) :UInt32 = 0 $Proxy.name("chain_time_max");
}
diff --git a/src/ipc/capnp/chain.cpp b/src/ipc/capnp/chain.cpp
index 9b19d4bc7a..f77a2d3c12 100644
--- a/src/ipc/capnp/chain.cpp
+++ b/src/ipc/capnp/chain.cpp
@@ -29,6 +29,7 @@
#include <cstdint>
#include <functional>
#include <memory>
+#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
@@ -129,7 +130,23 @@ void CustomBuildMessage(InvokeContext& invoke_context,
builder.setHash(ipc::capnp::ToArray(block.hash));
if (block.prev_hash) builder.setPrevHash(ipc::capnp::ToArray(*block.prev_hash));
if (block.data) builder.setData(ipc::capnp::ToArray(ipc::capnp::Serialize(*block.data)));
- if (block.undo_data) builder.setUndoData(ipc::capnp::ToArray(ipc::capnp::Serialize(*block.undo_data)));
+ if (block.undo_data) {
+ size_t coin_count{0};
+ for (const CTxUndo& tx_undo : block.undo_data->vtxundo) {
+ coin_count += tx_undo.vprevout.size();
+ }
+ auto coins = builder.initUndoCoins(coin_count);
+ size_t i{0};
+ for (const CTxUndo& tx_undo : block.undo_data->vtxundo) {
+ for (const Coin& coin : tx_undo.vprevout) {
+ auto entry = coins[i++];
+ entry.setAmount(coin.out.nValue);
+ entry.setIsCoinbase(coin.IsCoinBase());
+ entry.setScript(ipc::capnp::ToArray(coin.out.scriptPubKey));
+ entry.setHeight(coin.nHeight);
+ }
+ }
+ }
// Copy the remaining fields using the code generated by Proxy.wrap.
mp::BuildOne<0>(mp::TypeList<interfaces::BlockInfo>(), invoke_context, builder, block);
}
@@ -162,8 +179,41 @@ void CustomPassMessage(InvokeContext& invoke_context,
data.emplace(ipc::capnp::Unserialize<CBlock>(reader.getData()));
block.data = &*data;
}
- if (reader.hasUndoData()) {
- undo_data.emplace(ipc::capnp::Unserialize<CBlockUndo>(reader.getUndoData()));
+ if (reader.hasUndoCoins()) {
+ auto coins = reader.getUndoCoins();
+ auto make_coin = [](const ipc::capnp::messages::Coin::Reader& entry) {
+ const auto script{entry.getScript()};
+ return Coin{CTxOut{entry.getAmount(), CScript(script.begin(), script.end())},
+ static_cast<int>(entry.getHeight()), entry.getIsCoinbase()};
+ };
+ CBlockUndo undo;
+ if (data) {
+ size_t input_count{0};
+ for (const auto& tx : data->vtx) {
+ if (!tx->IsCoinBase()) input_count += tx->vin.size();
+ }
+ if (input_count != coins.size()) throw std::runtime_error("BlockInfo undo coin count does not match block inputs");
+ // Reconstruct the per-transaction grouping of Core's undo data
+ // using the input counts from the block data.
+ size_t coin_index{0};
+ for (const auto& tx : data->vtx) {
+ if (tx->IsCoinBase()) continue;
+ CTxUndo tx_undo;
+ tx_undo.vprevout.reserve(tx->vin.size());
+ for (size_t j = 0; j < tx->vin.size(); ++j) {
+ tx_undo.vprevout.push_back(make_coin(coins[coin_index++]));
+ }
+ undo.vtxundo.push_back(std::move(tx_undo));
+ }
+ } else {
+ CTxUndo tx_undo;
+ tx_undo.vprevout.reserve(coins.size());
+ for (const auto entry : coins) {
+ tx_undo.vprevout.push_back(make_coin(entry));
+ }
+ undo.vtxundo.push_back(std::move(tx_undo));
+ }
+ undo_data.emplace(std::move(undo));
block.undo_data = &*undo_data;
}
mp::ReadField(mp::TypeList<interfaces::BlockInfo>(), invoke_context,
diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp
index 6bd3818070..fdb31ab0bd 100644
--- a/src/node/interfaces.cpp
+++ b/src/node/interfaces.cpp
@@ -58,6 +58,7 @@
#include <sync.h>
#include <txmempool.h>
#include <uint256.h>
+#include <undo.h>
#include <univalue.h>
#include <util/btcsignals.h>
#include <util/check.h>
@@ -462,8 +463,8 @@ bool FillBlock(const CBlockIndex* index, const FoundBlock& block, UniqueLock<Rec
class NotificationsProxy : public CValidationInterface
{
public:
- explicit NotificationsProxy(std::shared_ptr<Chain::Notifications> notifications)
- : m_notifications(std::move(notifications)) {}
+ NotificationsProxy(std::shared_ptr<Chain::Notifications> notifications, ChainstateManager& chainman, bool include_undo_data)
+ : m_notifications(std::move(notifications)), m_chainman(chainman), m_include_undo_data(include_undo_data) {}
virtual ~NotificationsProxy() = default;
void TransactionAddedToMempool(const NewMempoolTransactionInfo& tx, uint64_t mempool_sequence) override
{
@@ -475,11 +476,13 @@ public:
}
void BlockConnected(const ChainstateRole& role, const std::shared_ptr<const CBlock>& block, const CBlockIndex* index) override
{
- m_notifications->blockConnected(role, kernel::MakeBlockInfo(index, block.get()));
+ CBlockUndo undo;
+ m_notifications->blockConnected(role, make_block_info(index, block.get(), undo));
}
void BlockDisconnected(const std::shared_ptr<const CBlock>& block, const CBlockIndex* index) override
{
- m_notifications->blockDisconnected(kernel::MakeBlockInfo(index, block.get()));
+ CBlockUndo undo;
+ m_notifications->blockDisconnected(make_block_info(index, block.get(), undo));
}
void UpdatedBlockTip(const CBlockIndex* index, const CBlockIndex* fork_index, bool is_ibd) override
{
@@ -489,14 +492,30 @@ public:
{
m_notifications->chainStateFlushed(role, locator);
}
+ interfaces::BlockInfo make_block_info(const CBlockIndex* index, const CBlock* block, CBlockUndo& undo) const
+ {
+ interfaces::BlockInfo info{kernel::MakeBlockInfo(index, block)};
+ if (m_include_undo_data && index->nHeight > 0) {
+ if (m_chainman.m_blockman.ReadBlockUndo(undo, *index)) {
+ info.undo_data = &undo;
+ } else {
+ m_chainman.GetNotifications().fatalError(Untranslated(strprintf("Failed to read undo data for block %s", index->Get
BlockHash().ToString())));
+ }
+ }
+ return info;
+ }
std::shared_ptr<Chain::Notifications> m_notifications;
+ ChainstateManager& m_chainman;
+ bool m_include_undo_data;
};
class NotificationsHandlerImpl : public Handler
{
public:
- explicit NotificationsHandlerImpl(ValidationSignals& signals, std::shared_ptr<Chain::Notifications> notifications)
- : m_signals{signals}, m_proxy{std::make_shared<NotificationsProxy>(std::move(notifications))}
+ NotificationsHandlerImpl(ValidationSignals& signals, std::shared_ptr<Chain::Notifications> notifications,
+ ChainstateManager& chainman, bool include_undo_data)
+ : m_signals{signals},
+ m_proxy{std::make_shared<NotificationsProxy>(std::move(notifications), chainman, include_undo_data)}
{
m_signals.RegisterSharedValidationInterface(m_proxy);
}
@@ -786,9 +805,11 @@ public:
{
::uiInterface.ShowProgress(title, progress, resume_possible);
}
- std::unique_ptr<Handler> handleNotifications(std::shared_ptr<Notifications> notifications) override
+ std::unique_ptr<Handler> handleNotifications(std::shared_ptr<Notifications> notifications,
+ bool include_undo_data) override
{
- return std::make_unique<NotificationsHandlerImpl>(validation_signals(), std::move(notifications));
+ return std::make_unique<NotificationsHandlerImpl>(
+ validation_signals(), std::move(notifications), chainman(), include_undo_data);
}
</details>
Also small naming suggestion, maybe spentCoins instead?