diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index fa3433f3cc..acf133ea9d 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -46,6 +46,7 @@ QT_MOC_CPP = \ qt/moc_bitcoinamountfield.cpp \ qt/moc_bitcoingui.cpp \ qt/moc_bitcoinunits.cpp \ + qt/moc_blockview.cpp \ qt/moc_clientmodel.cpp \ qt/moc_coincontroldialog.cpp \ qt/moc_coincontroltreewidget.cpp \ @@ -119,6 +120,7 @@ BITCOIN_QT_H = \ qt/bitcoinamountfield.h \ qt/bitcoingui.h \ qt/bitcoinunits.h \ + qt/blockview.h \ qt/clientmodel.h \ qt/coincontroldialog.h \ qt/coincontroltreewidget.h \ @@ -234,6 +236,7 @@ BITCOIN_QT_BASE_CPP = \ qt/bitcoinamountfield.cpp \ qt/bitcoingui.cpp \ qt/bitcoinunits.cpp \ + qt/blockview.cpp \ qt/clientmodel.cpp \ qt/csvmodelwriter.cpp \ qt/guiutil.cpp \ diff --git a/src/interfaces/mining.h b/src/interfaces/mining.h index 9d98a3919e..1577a4cc50 100644 --- a/src/interfaces/mining.h +++ b/src/interfaces/mining.h @@ -46,8 +46,8 @@ public: * @param[in] options options for creating the block * @returns a block template */ - virtual std::unique_ptr createNewBlock(const CScript& script_pub_key, const node::BlockCreateOptions& options={}) = 0; - virtual std::unique_ptr createNewBlock2(const CScript& script_pub_key, const node::BlockCreateOptions& assemble_options) = 0; + virtual std::shared_ptr createNewBlock(const CScript& script_pub_key, const node::BlockCreateOptions& options={}) = 0; + virtual std::shared_ptr createNewBlock2(const CScript& script_pub_key, const node::BlockCreateOptions& assemble_options) = 0; /** * Processes new block. A valid new block is automatically relayed to peers. diff --git a/src/node/interfaces.cpp b/src/node/interfaces.cpp index cae7d8905b..6fc3962d9f 100644 --- a/src/node/interfaces.cpp +++ b/src/node/interfaces.cpp @@ -935,16 +935,16 @@ public: return TestBlockValidity(state, chainman().GetParams(), chainman().ActiveChainstate(), block, tip, /*fCheckPOW=*/false, check_merkle_root); } - std::unique_ptr createNewBlock(const CScript& script_pub_key, const BlockCreateOptions& options) override + std::shared_ptr createNewBlock(const CScript& script_pub_key, const BlockCreateOptions& options) override { BlockAssembler::Options assemble_options{options}; ApplyArgsManOptions(*Assert(m_node.args), assemble_options); return createNewBlock2(script_pub_key, assemble_options); } - std::unique_ptr createNewBlock2(const CScript& script_pub_key, const BlockCreateOptions& assemble_options) override + std::shared_ptr createNewBlock2(const CScript& script_pub_key, const BlockCreateOptions& assemble_options) override { - return BlockAssembler{chainman().ActiveChainstate(), context()->mempool.get(), assemble_options}.CreateNewBlock(script_pub_key); + return BlockAssembler{chainman().ActiveChainstate(), context()->mempool.get(), assemble_options, m_node}.CreateNewBlock(script_pub_key); } NodeContext* context() override { return &m_node; } diff --git a/src/node/miner.cpp b/src/node/miner.cpp index fcacd0b01c..33c3ce0a1a 100644 --- a/src/node/miner.cpp +++ b/src/node/miner.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -23,6 +24,7 @@ #include #include #include +#include #include #include @@ -82,10 +84,11 @@ BlockCreateOptions BlockCreateOptions::Clamped() const return options; } -BlockAssembler::BlockAssembler(Chainstate& chainstate, const CTxMemPool* mempool, const Options& options) +BlockAssembler::BlockAssembler(Chainstate& chainstate, const CTxMemPool* mempool, const Options& options, const NodeContext& node) : chainparams{chainstate.m_chainman.GetParams()}, m_mempool{options.use_mempool ? mempool : nullptr}, m_chainstate{chainstate}, + m_node{node}, m_options{options.Clamped()} { // Whether we need to account for byte usage (in addition to weight usage) @@ -133,7 +136,7 @@ void BlockAssembler::resetBlock() blockFinished = false; } -std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn) +std::shared_ptr BlockAssembler::CreateNewBlock(const CScript& scriptPubKeyIn) { const auto time_start{SteadyClock::now()}; @@ -221,6 +224,8 @@ std::unique_ptr BlockAssembler::CreateNewBlock(const CScript& sc Ticks(time_2 - time_1), Ticks(time_2 - time_start)); + if (m_node.validation_signals) m_node.validation_signals->NewBlockTemplate(pblocktemplate); + return std::move(pblocktemplate); } diff --git a/src/node/miner.h b/src/node/miner.h index 8b4ddcd9de..0b79bc0af2 100644 --- a/src/node/miner.h +++ b/src/node/miner.h @@ -29,6 +29,7 @@ class Chainstate; class ChainstateManager; namespace Consensus { struct Params; }; +namespace node { struct NodeContext; }; namespace node { @@ -140,7 +141,7 @@ class BlockAssembler { private: // The constructed block template - std::unique_ptr pblocktemplate; + std::shared_ptr pblocktemplate; bool fNeedSizeAccounting; @@ -159,6 +160,7 @@ private: const CChainParams& chainparams; const CTxMemPool* const m_mempool; Chainstate& m_chainstate; + const NodeContext& m_node; // Variables used for addPriorityTxs int lastFewTxs; @@ -167,10 +169,10 @@ private: public: using Options = BlockCreateOptions; - explicit BlockAssembler(Chainstate& chainstate, const CTxMemPool* mempool, const Options& options); + explicit BlockAssembler(Chainstate& chainstate, const CTxMemPool* mempool, const Options& options, const NodeContext& node); /** Construct a new block template with coinbase to scriptPubKeyIn */ - std::unique_ptr CreateNewBlock(const CScript& scriptPubKeyIn); + std::shared_ptr CreateNewBlock(const CScript& scriptPubKeyIn); inline static std::optional m_last_block_num_txs{}; inline static std::optional m_last_block_weight{}; diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index dbf2cc95f9..f90536614e 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -603,6 +604,14 @@ void BitcoinGUI::createMenuBar() window_menu->addAction(m_show_netwatch_action); window_menu->addAction(showMempoolStatsAction); + auto show_blockview_action = new QAction(tr("Block &Visualizer"), this); + window_menu->addAction(show_blockview_action); + connect(show_blockview_action, &QAction::triggered, [this] { + auto blockview = new GuiBlockView(platformStyle, m_network_style); + blockview->setClientModel(clientModel); + GUIUtil::bringToFront(blockview); + }); + window_menu->addSeparator(); for (RPCConsole::TabTypes tab_type : rpcConsole->tabs()) { QAction* tab_action = window_menu->addAction(rpcConsole->tabTitle(tab_type)); diff --git a/src/qt/blockview.cpp b/src/qt/blockview.cpp new file mode 100644 index 0000000000..309e30fbf5 --- /dev/null +++ b/src/qt/blockview.cpp @@ -0,0 +1,513 @@ +// Copyright (c) 2024 Luke Dashjr +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#if defined(HAVE_CONFIG_H) +#include +#endif + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +static constexpr qreal TX_PADDING_NEXT{4}; +static constexpr qreal TX_PADDING_NEARBY{2}; +static constexpr qreal EXPECTED_WHITESPACE_PERCENT{1.5}; +static constexpr auto RADIAN_DIVISOR{8}; + +void ScalingGraphicsView::resizeEvent(QResizeEvent * const event) +{ + fitInView(scene()->sceneRect(), Qt::KeepAspectRatio); + QGraphicsView::resizeEvent(event); +} + +class BlockViewValidationInterface final : public CValidationInterface +{ +private: + GuiBlockView& m_bv; + +public: + explicit BlockViewValidationInterface(GuiBlockView& bv) : m_bv(bv) {} + + void BlockConnected(ChainstateRole role, const std::shared_ptr& block_cached, const CBlockIndex* pblockindex) override { + m_bv.updateBestBlock(pblockindex->nHeight); + + if (!m_bv.m_follow_tip) return; + + std::shared_ptr block = block_cached; + auto chainman = m_bv.getChainstateManager(); + Assert(chainman); + if (!block) { + std::shared_ptr pblock = std::make_shared(); + if (!chainman->m_blockman.ReadBlockFromDisk(*pblock, *pblockindex)) { + // Indicate error somehow? + return; + } + block = pblock; + } + + const auto block_subsidy = GetBlockSubsidy(pblockindex->nHeight, chainman->GetParams().GetConsensus()); + + m_bv.setBlock(block, block_subsidy); + } + + void NewBlockTemplate(const std::shared_ptr& blocktemplate) override { + { + LOCK(m_bv.m_mutex); + if (m_bv.m_block) { + // Update cached template, but don't render it + m_bv.m_block_template = blocktemplate; + return; + } + } + + m_bv.setBlock(blocktemplate); + } +}; + +void GuiBlockView::updateBestBlock(const int height) +{ + m_block_chooser->setItemText(1, tr("Newest block (%1)").arg(height)); +} + +GuiBlockView::GuiBlockView(const PlatformStyle *platformStyle, const NetworkStyle *networkStyle, QWidget *parent) : + QDialog(parent, GUIUtil::dialog_flags | Qt::WindowMaximizeButtonHint) +{ + setWindowTitle(tr(PACKAGE_NAME) + " - " + tr("Block View") + " " + networkStyle->getTitleAddText()); + setWindowIcon(networkStyle->getTrayAndWindowIcon()); + resize(640, 640); + + QVBoxLayout * const layout = new QVBoxLayout(this); + setLayout(layout); + + auto hlayout = new QHBoxLayout; + layout->addLayout(hlayout); + hlayout->addWidget(new QLabel(tr("Displayed block: "))); + m_block_chooser = new QComboBox(this); + hlayout->addWidget(m_block_chooser, 1); + connect(m_block_chooser, QOverload::of(&QComboBox::currentIndexChanged), [=, this](const int index){ + m_follow_tip = false; + auto ud = m_block_chooser->itemData(index).toInt(); + if (ud == -3) { + m_block_chooser->setEditable(false); + auto block_template = WITH_LOCK(m_mutex, return m_block_template); + if (block_template) { + setBlock(block_template); + } else { + clear(); + } + return; + } + + auto chainman = getChainstateManager(); + if (!chainman) { + clear(); + return; + } + auto& blockman = chainman->m_blockman; + + CBlockIndex *pblockindex; + if (ud == -2) { + m_follow_tip = true; + pblockindex = WITH_LOCK(::cs_main, return chainman->ActiveChain().Tip()); + if (!pblockindex) { + clear(); + return; + } + } else if (ud == -1) { + m_block_chooser->setEditable(true); + m_block_chooser->clearEditText(); + return; + } else { + auto qtxt = m_block_chooser->itemText(index); + auto txt = qtxt.toStdString(); + auto blockhash{uint256::FromHex(txt)}; + if (blockhash) { + LOCK(cs_main); + pblockindex = blockman.LookupBlockIndex(*blockhash); + } else if (auto height = ToIntegral(txt)) { + LOCK(cs_main); + pblockindex = chainman->ActiveChain()[*height]; + } else { + pblockindex = nullptr; + } + if (!pblockindex) { + clear(); + QMessageBox::critical(this, tr("Invalid block"), tr("\"%1\" is not a valid block height or hash!").arg(qtxt)); + m_block_chooser->removeItem(index); + return; + } + } + + std::shared_ptr block = std::make_shared(); + if ((!blockman.ReadBlockFromDisk(*block, *pblockindex)) || block->vtx.empty()) { + clear(); + const bool is_pruned = WITH_LOCK(::cs_main, return blockman.IsBlockPruned(*pblockindex)); + if (is_pruned) { + QMessageBox::critical(this, tr("Pruned block"), tr("Block %1 (%2) is pruned.").arg(pblockindex->nHeight).arg(QString::fromStdString(pblockindex->GetBlockHash().ToString()))); + } else { + QMessageBox::critical(this, tr("Error reading block"), tr("Block %1 (%2) could not be loaded.").arg(pblockindex->nHeight).arg(QString::fromStdString(pblockindex->GetBlockHash().ToString()))); + } + m_block_chooser->removeItem(index); + return; + } + + m_block_chooser->setEditable(false); + + const auto block_subsidy = GetBlockSubsidy(pblockindex->nHeight, chainman->GetParams().GetConsensus()); + + setBlock(block, block_subsidy); + }); + // Items initialized later, after ClientModel is available + + m_scene = new QGraphicsScene(this); + m_scene->setSceneRect(0, 0, 1, 1); + auto view = new ScalingGraphicsView(m_scene, this); + layout->addWidget(view); + view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + view->setStyleSheet("QGraphicsView { background: transparent; }"); + view->setAlignment(Qt::AlignHCenter | Qt::AlignBottom); + connect(m_scene, &QGraphicsScene::sceneRectChanged, [view](const QRectF& rect){ + view->fitInView(rect, Qt::KeepAspectRatio); + }); + + hlayout = new QHBoxLayout; + layout->addLayout(hlayout); + hlayout->addWidget(new QLabel(tr("Transactions"), this)); + m_lbl_tx_count = new QLabel(this); + m_lbl_tx_count->setAlignment(Qt::AlignRight); + hlayout->addWidget(m_lbl_tx_count); + + hlayout = new QHBoxLayout; + layout->addLayout(hlayout); + hlayout->addWidget(new QLabel(tr("Txn Fees"), this)); + m_lbl_tx_fees = new QLabel(this); + m_lbl_tx_fees->setAlignment(Qt::AlignRight); + hlayout->addWidget(m_lbl_tx_fees); + + connect(&m_timer, &QTimer::timeout, this, &GuiBlockView::updateScene); + + m_validation_interface = new BlockViewValidationInterface(*this); +} + +GuiBlockView::~GuiBlockView() +{ + if (m_validation_interface) { + setClientModel(nullptr); + delete m_validation_interface; + m_validation_interface = nullptr; + } +} + +void GuiBlockView::setClientModel(ClientModel *model) +{ + if (m_client_model) { + auto& validation_signals = m_client_model->node().context()->validation_signals; + if (validation_signals) { + validation_signals->UnregisterValidationInterface(m_validation_interface); + } + disconnect(m_client_model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &GuiBlockView::updateDisplayUnit); + } + m_client_model = model; + if (model) { + connect(model->getOptionsModel(), &OptionsModel::displayUnitChanged, this, &GuiBlockView::updateDisplayUnit); + updateDisplayUnit(); + + if (m_block_chooser->count() == 0) { + m_block_chooser->addItem(tr("This node's preferred block template"), -3); + m_block_chooser->addItem("", -2); + m_block_chooser->addItem(tr("Specific block"), -1); + m_block_chooser->setCurrentIndex(1); + } + + auto chainman = getChainstateManager(); + if (chainman) { + const auto pblockindex = WITH_LOCK(::cs_main, return chainman->ActiveChain().Tip()); + updateBestBlock(pblockindex->nHeight); + } + auto& validation_signals = model->node().context()->validation_signals; + if (validation_signals) { + validation_signals->RegisterValidationInterface(m_validation_interface); + } + } +} + +ChainstateManager* GuiBlockView::getChainstateManager() const +{ + if (!m_client_model) return nullptr; + auto node_ctx = m_client_model->node().context(); + if (!node_ctx) return nullptr; + auto& chainman = node_ctx->chainman; + if (!chainman) return nullptr; + return &(*chainman); +} + +void GuiBlockView::clear() +{ + LOCK(m_mutex); + m_block_fees = -1; + m_lbl_tx_count->setText(""); + m_block.reset(); + m_block_template.reset(); + for (auto& [wtxid, elem] : m_elements) { + const auto gi = elem.gi; + m_scene->removeItem(gi); + delete gi; + } + m_elements.clear(); +} + +bool GuiBlockView::any_overlap(const Bubble& proposed, const std::vector& others) +{ + for (const auto& other : others) { + const auto x_dist = std::abs(other.pos.x() - proposed.pos.x()); + const auto y_dist = std::abs(other.pos.y() - proposed.pos.y()); + const auto dist = std::sqrt((x_dist * x_dist) + (y_dist * y_dist)); + if (dist < proposed.radius + other.radius + TX_PADDING_NEARBY) { + return true; + } + } + return false; +} + +void GuiBlockView::setBlock(std::shared_ptr block, const CAmount block_subsidy) +{ + LOCK(m_mutex); + m_block_fees = [&] { + CAmount total{0}; + Assert(!block->vtx.empty()); + for (const auto& outp : block->vtx[0]->vout) { + total += outp.nValue; + } + return total - block_subsidy; + }(); + m_block = block; + m_block_template.reset(); + m_block_changed = true; + updateElements(/*instant=*/ true); +} + +void GuiBlockView::setBlock(std::shared_ptr blocktemplate) +{ + LOCK(m_mutex); + const bool instant = (bool)m_block; // force instant if changing from real block to template + m_block_fees = -blocktemplate->vTxFees.front(); + m_block.reset(); + m_block_template = blocktemplate; + m_block_changed = true; + updateElements(/*instant=*/ instant); +} + +void GuiBlockView::updateBlockFees(CAmount block_fees) +{ + if (block_fees < 0) { + m_lbl_tx_fees->setText(""); + return; + } + const auto unit = m_client_model ? m_client_model->getOptionsModel()->getDisplayUnit() : BitcoinUnit::BTC; + m_lbl_tx_fees->setText(BitcoinUnits::formatWithUnit(unit, block_fees)); +} + +void GuiBlockView::updateDisplayUnit() +{ + const auto block_fees = WITH_LOCK(m_mutex, return m_block_fees); + updateBlockFees(block_fees); +} + +constexpr qreal offscreen{99}; + +void GuiBlockView::updateElements(bool instant) +{ + if (!m_block_changed) return; + + m_timer.stop(); + m_block_changed = false; + auto pblocktemplate = m_block_template; + auto pblock = m_block; + auto& block = pblock ? *pblock : pblocktemplate->block; + + instant |= m_elements.empty(); + for (auto& el : m_elements) { + el.second.target_loc.setY(offscreen); + } + m_bubblegraph = std::make_unique(); + auto& bubbles = m_bubblegraph->bubbles; + size_t total_txs_size{0}; + qreal limit_halfwidth{std::sqrt(::GetSerializeSize(TX_WITH_WITNESS(block))) * EXPECTED_WHITESPACE_PERCENT / 2}; + for (size_t i = 1; i < block.vtx.size(); ++i) { + auto& tx = *block.vtx[i]; + auto& el = m_elements[tx.GetWitnessHash()]; + QPointF preferred_loc; + double diameter; + const auto tx_size = tx.GetTotalSize(); + total_txs_size += tx_size; + const bool fresh_bubble = !el.gi; + if (fresh_bubble) { + diameter = 2 * std::sqrt(tx_size / std::numbers::pi); + } else { + // preferred_loc = el.gi->pos(); + diameter = el.gi->boundingRect().height(); + } + Bubble proposed{ .pos = {}, .radius = diameter / 2, .el = &el, }; + qreal x_extremity{proposed.radius}; + if (bubbles.empty()) { + proposed.pos.setY(-proposed.radius); + } + for (auto bubble_it = bubbles.rbegin(); bubble_it != bubbles.rend(); ++bubble_it) { + const auto& centre = bubble_it->pos; + QPointF preferred_loc_rel(preferred_loc.x() - centre.x(), preferred_loc.y() - centre.y()); + double preferred_angle; + if (preferred_loc_rel.isNull()) { + preferred_angle = std::numbers::pi / 2; + } else { + preferred_angle = std::atan2(preferred_loc.y() - centre.y(), preferred_loc.x() - centre.x()); + } + const auto distance = bubble_it->radius + proposed.radius + TX_PADDING_NEXT; + double angle = preferred_angle; + bool found{false}; + while (true) { + proposed.pos = QPointF(centre.x() + (distance * std::cos(angle)), centre.y() + (distance * std::sin(angle))); + + x_extremity = std::abs(proposed.pos.x()) + proposed.radius; + if (proposed.pos.y() < -proposed.radius && x_extremity <= limit_halfwidth && !any_overlap(proposed, bubbles)) { + found = true; + break; + } + + if (angle < preferred_angle) { + angle = preferred_angle + (preferred_angle - angle); + } else { + angle = preferred_angle - (angle - preferred_angle) - (std::numbers::pi / RADIAN_DIVISOR); + } + if (angle > preferred_angle + std::numbers::pi) { + break; + } + } + if (found) break; + } + m_bubblegraph->min_x = std::min(m_bubblegraph->min_x, proposed.pos.x() - proposed.radius); + m_bubblegraph->max_x = std::max(m_bubblegraph->max_x, proposed.pos.x() + proposed.radius); + m_bubblegraph->min_y = std::min(m_bubblegraph->min_y, proposed.pos.y() - proposed.radius); + bubbles.push_back(proposed); + el.target_loc = proposed.pos; + } + m_lbl_tx_count->setText(tr("%1 (%2)").arg(block.vtx.size() - 1).arg(tr("%1 kB").arg(total_txs_size / 1000.0, 0, 'f', 1))); + updateBlockFees(m_block_fees); + m_bubblegraph->instant = instant; + QMetaObject::invokeMethod(this, "updateSceneInit", Qt::QueuedConnection); +} + +void GuiBlockView::updateSceneInit() +{ + LOCK(m_mutex); + if (!m_bubblegraph) return; + for (auto& bubble : m_bubblegraph->bubbles) { + auto& el = *bubble.el; + if (!el.gi) { + const auto diameter = bubble.radius * 2; + auto gi = m_scene->addEllipse(0, 0, diameter, diameter, QPen(palette().window(), TX_PADDING_NEARBY)); + el.gi = gi; + gi->setBrush(QColor(Qt::blue)); + gi->setPos(bubble.pos.x() - bubble.radius, m_bubblegraph->instant ? (bubble.pos.y() - bubble.radius) : offscreen); + } + } + for (auto it = m_elements.begin(); it != m_elements.end(); ) { + const auto& target_loc = it->second.target_loc; + const auto gi = it->second.gi; + bool delete_el{false}; + if (target_loc.y() == offscreen || !gi /* never got a chance to exist */) { + delete_el = true; + // TODO: if confirmed, slide it off the bottom + // TODO: if conflicted, pop the bubble? + // TODO: if delayed, move off the top + } else { + if (gi->y() == offscreen) { + gi->setY(m_bubblegraph->min_y - gi->boundingRect().height()); + } + } + if (delete_el) { + if (gi) { + m_scene->removeItem(gi); + delete gi; + } + it = m_elements.erase(it); + } else { + ++it; + } + } + m_scene->setSceneRect(m_bubblegraph->min_x, m_bubblegraph->min_y, m_bubblegraph->max_x - m_bubblegraph->min_x, -m_bubblegraph->min_y); + if (!m_bubblegraph->instant) { + m_frame_div = 4; + updateScene(); + m_timer.start(100); + } + m_bubblegraph.reset(); +} + +void GuiBlockView::updateScene() +{ + LOCK(m_mutex); + bool all_completed{true}; + for (auto it = m_elements.begin(); it != m_elements.end(); ) { + const auto& target_loc = it->second.target_loc; + QGraphicsItem* gi = it->second.gi; + const auto radius = gi->boundingRect().width() / 2; + const QPointF current_loc(gi->pos().x() + radius, gi->pos().y() + radius); + bool delete_el{false}; + if (target_loc != current_loc) { + // Get 25% closer each tick + QPointF new_loc(current_loc.x() + ((target_loc.x() - current_loc.x()) / m_frame_div), + current_loc.y() + ((target_loc.y() - current_loc.y()) / m_frame_div)); + if (std::abs(new_loc.x() - target_loc.x()) < TX_PADDING_NEXT) { + new_loc.setX(target_loc.x()); + } + if (std::abs(new_loc.y() - target_loc.y()) < TX_PADDING_NEXT) { + new_loc.setY(target_loc.y()); + } + gi->setPos(new_loc.x() - radius, new_loc.y() - radius); + if (new_loc == target_loc) { + if (target_loc.y() + radius < m_scene->sceneRect().y() || target_loc.y() - radius > 0) { + delete_el = true; + } + } else { + all_completed = false; + } + } + if (delete_el) { + m_scene->removeItem(gi); + delete gi; + it = m_elements.erase(it); + } else { + ++it; + } + } + --m_frame_div; + if (all_completed) { + m_timer.stop(); + } +} diff --git a/src/qt/blockview.h b/src/qt/blockview.h new file mode 100644 index 0000000000..fe1159a144 --- /dev/null +++ b/src/qt/blockview.h @@ -0,0 +1,120 @@ +// Copyright (c) 2024 Luke Dashjr +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QT_BLOCKVIEW_H +#define BITCOIN_QT_BLOCKVIEW_H + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QComboBox; +class QGraphicsItem; +class QGraphicsScene; +class QLabel; +class QWidget; +QT_END_NAMESPACE + +class CBlock; +namespace node { struct CBlockTemplate; } +class ChainstateManager; +class ClientModel; +class CValidationInterface; +class NetworkStyle; +class PlatformStyle; + +class ScalingGraphicsView : public QGraphicsView +{ + Q_OBJECT + +public: + using QGraphicsView::QGraphicsView; + + void resizeEvent(QResizeEvent *event) override; +}; + +class BlockViewValidationInterface; + +class GuiBlockView : public QDialog +{ + Q_OBJECT + +public: + RecursiveMutex m_mutex; + +private: + struct SceneElement { + QGraphicsItem* gi; + QPointF target_loc; + }; + struct Bubble { + QPointF pos; + double radius; + SceneElement *el; + }; + struct BubbleGraph { + std::vector bubbles; + qreal min_x{0}; + qreal max_x{0}; + qreal min_y{0}; + bool instant; + }; + std::map m_elements GUARDED_BY(m_mutex); + std::unique_ptr m_bubblegraph GUARDED_BY(m_mutex); + QGraphicsScene *m_scene; + QTimer m_timer; + unsigned int m_frame_div; + + QComboBox *m_block_chooser; + QLabel *m_lbl_tx_count; + QLabel *m_lbl_tx_fees; + + BlockViewValidationInterface *m_validation_interface; + + static bool any_overlap(const Bubble& proposed, const std::vector& others); + +protected: + void updateElements(bool instant) EXCLUSIVE_LOCKS_REQUIRED(m_mutex); + void updateBlockFees(CAmount block_fees); + +protected Q_SLOTS: + void updateDisplayUnit(); + void updateSceneInit(); + void updateScene(); + +public: + ClientModel *m_client_model{nullptr}; + + bool m_block_changed GUARDED_BY(m_mutex); + CAmount m_block_fees GUARDED_BY(m_mutex) {-1}; + std::shared_ptr m_block_template GUARDED_BY(m_mutex); + std::shared_ptr m_block GUARDED_BY(m_mutex); + std::atomic m_follow_tip; + + GuiBlockView(const PlatformStyle *, const NetworkStyle *, QWidget * parent = nullptr); + ~GuiBlockView(); + + void setClientModel(ClientModel *model); + ChainstateManager* getChainstateManager() const; + + void updateBestBlock(int height); + + void clear(); + void setBlock(std::shared_ptr block, CAmount block_subsidy); + void setBlock(std::shared_ptr blocktemplate); +}; + +#endif // BITCOIN_QT_BLOCKVIEW_H diff --git a/src/rpc/mining.cpp b/src/rpc/mining.cpp index 66bffd42eb..f4d28b438c 100644 --- a/src/rpc/mining.cpp +++ b/src/rpc/mining.cpp @@ -162,7 +162,7 @@ static UniValue generateBlocks(ChainstateManager& chainman, Mining& miner, const { UniValue blockHashes(UniValue::VARR); while (nGenerate > 0 && !chainman.m_interrupt) { - std::unique_ptr pblocktemplate(miner.createNewBlock(coinbase_script)); + auto pblocktemplate = miner.createNewBlock(coinbase_script); if (!pblocktemplate.get()) throw JSONRPCError(RPC_INTERNAL_ERROR, "Couldn't create new block"); @@ -373,7 +373,7 @@ static RPCHelpMan generateblock() { LOCK(chainman.GetMutex()); { - std::unique_ptr blocktemplate{miner.createNewBlock(coinbase_script, {.use_mempool = false})}; + auto blocktemplate = miner.createNewBlock(coinbase_script, {.use_mempool = false}); if (!blocktemplate) { throw JSONRPCError(RPC_INTERNAL_ERROR, "Couldn't create new block"); } @@ -865,7 +865,7 @@ static RPCHelpMan getblocktemplate() // Update block static CBlockIndex* pindexPrev; static int64_t time_start; - static std::unique_ptr pblocktemplate; + static std::shared_ptr pblocktemplate; if (!pindexPrev || pindexPrev->GetBlockHash() != tip || bypass_cache || (miner.getTransactionsUpdated() != nTransactionsUpdatedLast && GetTime() - time_start > 5)) diff --git a/src/test/blockfilter_index_tests.cpp b/src/test/blockfilter_index_tests.cpp index 067a32d6a4..8e2ccc2721 100644 --- a/src/test/blockfilter_index_tests.cpp +++ b/src/test/blockfilter_index_tests.cpp @@ -68,7 +68,7 @@ CBlock BuildChainTestingSetup::CreateBlock(const CBlockIndex* prev, const CScript& scriptPubKey) { BlockAssembler::Options options; - std::unique_ptr pblocktemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock(scriptPubKey); + std::shared_ptr pblocktemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options, m_node}.CreateNewBlock(scriptPubKey); CBlock& block = pblocktemplate->block; block.hashPrevBlock = prev->GetBlockHash(); block.nTime = prev->nTime + 1; diff --git a/src/test/fuzz/mini_miner.cpp b/src/test/fuzz/mini_miner.cpp index d972f9a6bb..24cee28112 100644 --- a/src/test/fuzz/mini_miner.cpp +++ b/src/test/fuzz/mini_miner.cpp @@ -176,7 +176,7 @@ FUZZ_TARGET(mini_miner_selection, .init = initialize_miner) miner_options.nBlockMaxWeight = DEFAULT_BLOCK_MAX_WEIGHT; miner_options.test_block_validity = false; - node::BlockAssembler miner{g_setup->m_node.chainman->ActiveChainstate(), &pool, miner_options}; + node::BlockAssembler miner{g_setup->m_node.chainman->ActiveChainstate(), &pool, miner_options, g_setup->m_node}; node::MiniMiner mini_miner{pool, outpoints}; assert(mini_miner.IsReadyToCalculate()); diff --git a/src/test/fuzz/tx_pool.cpp b/src/test/fuzz/tx_pool.cpp index 64861311db..524d4a0538 100644 --- a/src/test/fuzz/tx_pool.cpp +++ b/src/test/fuzz/tx_pool.cpp @@ -97,7 +97,7 @@ void Finish(FuzzedDataProvider& fuzzed_data_provider, MockedTxPool& tx_pool, Cha BlockAssembler::Options options; options.nBlockMaxWeight = fuzzed_data_provider.ConsumeIntegralInRange(0U, MAX_BLOCK_WEIGHT); options.blockMinFeeRate = CFeeRate{ConsumeMoney(fuzzed_data_provider, /*max=*/COIN)}; - auto assembler = BlockAssembler{chainstate, &tx_pool, options}; + auto assembler = BlockAssembler{chainstate, &tx_pool, options, g_setup->m_node}; auto block_template = assembler.CreateNewBlock(CScript{} << OP_TRUE); Assert(block_template->block.vtx.size() >= 1); } diff --git a/src/test/miner_tests.cpp b/src/test/miner_tests.cpp index 5819a0340f..6ff08aec86 100644 --- a/src/test/miner_tests.cpp +++ b/src/test/miner_tests.cpp @@ -68,7 +68,7 @@ BlockAssembler MinerTestingSetup::AssemblerForTest(CTxMemPool& tx_mempool) options.nBlockMaxWeight = MAX_BLOCK_WEIGHT; options.nBlockMaxSize = MAX_BLOCK_SERIALIZED_SIZE; options.blockMinFeeRate = blockMinFeeRate; - return BlockAssembler{m_node.chainman->ActiveChainstate(), &tx_mempool, options}; + return BlockAssembler{m_node.chainman->ActiveChainstate(), &tx_mempool, options, m_node}; } constexpr static struct { @@ -137,7 +137,7 @@ void MinerTestingSetup::TestPackageSelection(const CScript& scriptPubKey, const Txid hashHighFeeTx = tx.GetHash(); tx_mempool.addUnchecked(entry.Fee(50000).Time(Now()).SpendsCoinbase(false).FromTx(tx)); - std::unique_ptr pblocktemplate = AssemblerForTest(tx_mempool).CreateNewBlock(scriptPubKey); + auto pblocktemplate = AssemblerForTest(tx_mempool).CreateNewBlock(scriptPubKey); BOOST_REQUIRE_EQUAL(pblocktemplate->block.vtx.size(), 4U); BOOST_CHECK(pblocktemplate->block.vtx[1]->GetHash() == hashParentTx); BOOST_CHECK(pblocktemplate->block.vtx[2]->GetHash() == hashHighFeeTx); @@ -609,7 +609,7 @@ BOOST_AUTO_TEST_CASE(CreateNewBlock_validity) { // Note that by default, these tests run with size accounting enabled. CScript scriptPubKey = CScript() << ParseHex("04678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5f") << OP_CHECKSIG; - std::unique_ptr pblocktemplate; + std::shared_ptr pblocktemplate; CTxMemPool& tx_mempool{*m_node.mempool}; // Simple block creation, nothing special yet: diff --git a/src/test/peerman_tests.cpp b/src/test/peerman_tests.cpp index 6de373eef2..2923c1eb76 100644 --- a/src/test/peerman_tests.cpp +++ b/src/test/peerman_tests.cpp @@ -21,7 +21,7 @@ static void mineBlock(const node::NodeContext& node, std::chrono::seconds block_ auto curr_time = GetTime(); SetMockTime(block_time); // update time so the block is created with it node::BlockAssembler::Options options; - CBlock block = node::BlockAssembler{node.chainman->ActiveChainstate(), nullptr, options}.CreateNewBlock(CScript() << OP_TRUE)->block; + CBlock block = node::BlockAssembler{node.chainman->ActiveChainstate(), nullptr, options, node}.CreateNewBlock(CScript() << OP_TRUE)->block; while (!CheckProofOfWork(block.GetHash(), block.nBits, node.chainman->GetConsensus())) ++block.nNonce; block.fChecked = true; // little speedup SetMockTime(curr_time); // process block at current time diff --git a/src/test/util/mining.cpp b/src/test/util/mining.cpp index ad7a38d3fe..5152f2d6cc 100644 --- a/src/test/util/mining.cpp +++ b/src/test/util/mining.cpp @@ -112,7 +112,7 @@ std::shared_ptr PrepareBlock(const NodeContext& node, const CScript& coi const BlockAssembler::Options& assembler_options) { auto block = std::make_shared( - BlockAssembler{Assert(node.chainman)->ActiveChainstate(), Assert(node.mempool.get()), assembler_options} + BlockAssembler{Assert(node.chainman)->ActiveChainstate(), Assert(node.mempool.get()), assembler_options, node} .CreateNewBlock(coinbase_scriptPubKey) ->block); diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 383a4c1fc9..053540098d 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -390,7 +390,7 @@ CBlock TestChain100Setup::CreateBlock( Chainstate& chainstate) { BlockAssembler::Options options; - CBlock block = BlockAssembler{chainstate, nullptr, options}.CreateNewBlock(scriptPubKey)->block; + CBlock block = BlockAssembler{chainstate, nullptr, options, m_node}.CreateNewBlock(scriptPubKey)->block; Assert(block.vtx.size() == 1); for (const CMutableTransaction& tx : txns) { diff --git a/src/test/validation_block_tests.cpp b/src/test/validation_block_tests.cpp index 0b3964d859..628bfd6182 100644 --- a/src/test/validation_block_tests.cpp +++ b/src/test/validation_block_tests.cpp @@ -66,7 +66,7 @@ std::shared_ptr MinerTestingSetup::Block(const uint256& prev_hash) static uint64_t time = Params().GenesisBlock().nTime; BlockAssembler::Options options; - auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock(CScript{} << i++ << OP_TRUE); + auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options, m_node}.CreateNewBlock(CScript{} << i++ << OP_TRUE); auto pblock = std::make_shared(ptemplate->block); pblock->hashPrevBlock = prev_hash; pblock->nTime = ++time; @@ -331,7 +331,7 @@ BOOST_AUTO_TEST_CASE(witness_commitment_index) CScript pubKey; pubKey << 1 << OP_TRUE; BlockAssembler::Options options; - auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options}.CreateNewBlock(pubKey); + auto ptemplate = BlockAssembler{m_node.chainman->ActiveChainstate(), m_node.mempool.get(), options, m_node}.CreateNewBlock(pubKey); CBlock pblock = ptemplate->block; CTxOut witness; diff --git a/src/validationinterface.cpp b/src/validationinterface.cpp index 7a58fe553a..03b4db5c52 100644 --- a/src/validationinterface.cpp +++ b/src/validationinterface.cpp @@ -260,3 +260,8 @@ void ValidationSignals::NewPoWValidBlock(const CBlockIndex *pindex, const std::s LOG_EVENT("%s: block hash=%s", __func__, block->GetHash().ToString()); m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.NewPoWValidBlock(pindex, block); }); } + +void ValidationSignals::NewBlockTemplate(const std::shared_ptr& blocktemplate) { + LOG_EVENT("%s", __func__); + m_internals->Iterate([&](CValidationInterface& callbacks) { callbacks.NewBlockTemplate(blocktemplate); }); +} diff --git a/src/validationinterface.h b/src/validationinterface.h index 06aa99bd53..994b97c708 100644 --- a/src/validationinterface.h +++ b/src/validationinterface.h @@ -25,6 +25,7 @@ class BlockValidationState; class CBlock; class CBlockIndex; struct CBlockLocator; +namespace node { struct CBlockTemplate; } enum class MemPoolRemovalReason; struct RemovedMempoolTransactionInfo; struct NewMempoolTransactionInfo; @@ -157,6 +158,8 @@ protected: * has been received and connected to the headers tree, though not validated yet. */ virtual void NewPoWValidBlock(const CBlockIndex *pindex, const std::shared_ptr& block) {}; + + virtual void NewBlockTemplate(const std::shared_ptr& blocktemplate) {} /** * Notifies the validation interface that it is being unregistered */ @@ -234,6 +237,7 @@ public: void ChainStateFlushed(ChainstateRole, const CBlockLocator &); void BlockChecked(const CBlock&, const BlockValidationState&); void NewPoWValidBlock(const CBlockIndex *, const std::shared_ptr&); + void NewBlockTemplate(const std::shared_ptr& blocktemplate); }; #endif // BITCOIN_VALIDATIONINTERFACE_H