From aa262da7bcfa9bf3d0105e6f689eae7c6e95a0e5 Mon Sep 17 00:00:00 2001 From: TheCharlatan Date: Thu, 30 May 2024 22:59:48 +0200 Subject: [PATCH] kernel: Add validation interface to C header This adds the infrastructure required to process validation events. For now the external validation interface only has support for the `BlockChecked` , `NewPoWValidBlock`, `BlockConnected`, and `BlockDisconnected` callback. Support for the other internal validation interface methods can be added in the future. The validation interface follows an architecture for defining its callbacks and ownership that is similar to the notifications. The task runner is created internally with a context, which itself internally creates a unique ValidationSignals object. When the user creates a new chainstate manager the validation signals are internally passed to the chainstate manager through the context. A validation interface can register for validation events with a context. Internally the passed in validation interface is registerd with the validation signals of a context. The callbacks block any further validation execution when they are called. It is up to the user to either multiplex them, or use them otherwise in a multithreaded mechanism to make processing the validation events non-blocking. I.e. for a synchronous mechanism, the user executes instructions directly at the end of the callback function: ```mermaid sequenceDiagram participant V as Validation participant C as Callback V->>C: Call callback Note over C: Process event (blocks) C-->>V: Return Note over V: Validation resumes ``` To avoid blocking, the user can submit the data to e.g. a worker thread or event manager, so processing happens asynchronously: ```mermaid sequenceDiagram participant V as Validation participant C as Callback participant W as Worker Thread V->>C: Call callback C->>W: Submit to worker thread C-->>V: Return immediately Note over V: Validation continues Note over W: Process event async ``` --- src/kernel/bitcoinkernel.cpp | 85 +++++++++++++++++++++++++++++- src/kernel/bitcoinkernel.h | 60 ++++++++++++++++++++- src/kernel/bitcoinkernel_wrapper.h | 46 ++++++++++++++++ src/test/kernel/test_kernel.cpp | 32 ++++++++++- 4 files changed, 219 insertions(+), 4 deletions(-) diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp index 1c146df0cb0..9c96a1c4900 100644 --- a/src/kernel/bitcoinkernel.cpp +++ b/src/kernel/bitcoinkernel.cpp @@ -31,8 +31,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -47,6 +49,8 @@ #include #include +using util::ImmediateTaskRunner; + // Define G_TRANSLATION_FUN symbol in libbitcoinkernel library so users of the // library aren't required to export this symbol extern const std::function G_TRANSLATION_FUN{nullptr}; @@ -135,6 +139,7 @@ struct Handle { struct btck_BlockTreeEntry: Handle {}; struct btck_Block : Handle> {}; +struct btck_BlockValidationState : Handle {}; namespace { @@ -317,10 +322,65 @@ public: } }; +class KernelValidationInterface final : public CValidationInterface +{ +public: + btck_ValidationInterfaceCallbacks m_cbs; + + explicit KernelValidationInterface(const btck_ValidationInterfaceCallbacks vi_cbs) : m_cbs{vi_cbs} {} + + ~KernelValidationInterface() + { + if (m_cbs.user_data && m_cbs.user_data_destroy) { + m_cbs.user_data_destroy(m_cbs.user_data); + } + m_cbs.user_data = nullptr; + m_cbs.user_data_destroy = nullptr; + } + +protected: + void BlockChecked(const std::shared_ptr& block, const BlockValidationState& stateIn) override + { + if (m_cbs.block_checked) { + m_cbs.block_checked(m_cbs.user_data, + btck_Block::copy(btck_Block::ref(&block)), + btck_BlockValidationState::ref(&stateIn)); + } + } + + void NewPoWValidBlock(const CBlockIndex* pindex, const std::shared_ptr& block) override + { + if (m_cbs.pow_valid_block) { + m_cbs.pow_valid_block(m_cbs.user_data, + btck_Block::copy(btck_Block::ref(&block)), + btck_BlockTreeEntry::ref(pindex)); + } + } + + void BlockConnected(ChainstateRole role, const std::shared_ptr& block, const CBlockIndex* pindex) override + { + if (m_cbs.block_connected) { + m_cbs.block_connected(m_cbs.user_data, + btck_Block::copy(btck_Block::ref(&block)), + btck_BlockTreeEntry::ref(pindex)); + } + } + + void BlockDisconnected(const std::shared_ptr& block, const CBlockIndex* pindex) override + { + if (m_cbs.block_disconnected) { + m_cbs.block_disconnected(m_cbs.user_data, + btck_Block::copy(btck_Block::ref(&block)), + btck_BlockTreeEntry::ref(pindex)); + } + } +}; + struct ContextOptions { mutable Mutex m_mutex; std::unique_ptr m_chainparams GUARDED_BY(m_mutex); std::shared_ptr m_notifications GUARDED_BY(m_mutex); + std::shared_ptr m_validation_interface GUARDED_BY(m_mutex); }; class Context @@ -332,8 +392,12 @@ public: std::unique_ptr m_interrupt; + std::unique_ptr m_signals; + std::unique_ptr m_chainparams; + std::shared_ptr m_validation_interface; + Context(const ContextOptions* options, bool& sane) : m_context{std::make_unique()}, m_interrupt{std::make_unique()} @@ -346,6 +410,11 @@ public: if (options->m_notifications) { m_notifications = options->m_notifications; } + if (options->m_validation_interface) { + m_signals = std::make_unique(std::make_unique()); + m_validation_interface = options->m_validation_interface; + m_signals->RegisterSharedValidationInterface(m_validation_interface); + } } if (!m_chainparams) { @@ -360,6 +429,13 @@ public: sane = false; } } + + ~Context() + { + if (m_signals) { + m_signals->UnregisterSharedValidationInterface(m_validation_interface); + } + } }; //! Helper struct to wrap the ChainstateManager-related Options @@ -374,7 +450,8 @@ struct ChainstateManagerOptions { : m_chainman_options{ChainstateManager::Options{ .chainparams = *context->m_chainparams, .datadir = data_dir, - .notifications = *context->m_notifications}}, + .notifications = *context->m_notifications, + .signals = context->m_signals.get()}}, m_blockman_options{node::BlockManager::Options{ .chainparams = *context->m_chainparams, .blocks_dir = blocks_dir, @@ -654,6 +731,12 @@ void btck_context_options_set_notifications(btck_ContextOptions* options, btck_N btck_ContextOptions::get(options).m_notifications = std::make_shared(notifications); } +void btck_context_options_set_validation_interface(btck_ContextOptions* options, btck_ValidationInterfaceCallbacks vi_cbs) +{ + LOCK(btck_ContextOptions::get(options).m_mutex); + btck_ContextOptions::get(options).m_validation_interface = std::make_shared(vi_cbs); +} + void btck_context_options_destroy(btck_ContextOptions* options) { delete options; diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h index 8d7b2898251..b41ff90e744 100644 --- a/src/kernel/bitcoinkernel.h +++ b/src/kernel/bitcoinkernel.h @@ -67,6 +67,9 @@ extern "C" { * functions, e.g. for scripts, may communicate more detailed error information * through status code out parameters. * + * Fine-grained validation information is communicated through the validation + * interface. + * * The kernel notifications issue callbacks for errors. These are usually * indicative of a system error. If such an error is issued, it is recommended * to halt and tear down the existing kernel objects. Remediating the error may @@ -149,6 +152,10 @@ typedef struct btck_ContextOptions btck_ContextOptions; * other validation objects are instantiated from it, the context is kept in * memory for the duration of their lifetimes. * + * The processing of validation events is done through an internal task runner + * owned by the context. It passes events through the registered validation + * interface callbacks. + * * A constructed context can be safely used from multiple threads. */ typedef struct btck_Context btck_Context; @@ -191,6 +198,14 @@ typedef struct btck_ChainstateManager btck_ChainstateManager; */ typedef struct btck_Block btck_Block; +/** + * Opaque data structure for holding the state of a block during validation. + * + * Contains information indicating whether validation was successful, and if not + * which step during block validation failed. + */ +typedef struct btck_BlockValidationState btck_BlockValidationState; + /** Current sync state passed to tip changed callbacks. */ typedef uint8_t btck_SynchronizationState; #define btck_SynchronizationState_INIT_REINDEX ((btck_SynchronizationState)(0)) @@ -226,6 +241,33 @@ typedef void (*btck_NotifyWarningUnset)(void* user_data, btck_Warning warning); typedef void (*btck_NotifyFlushError)(void* user_data, const char* message, size_t message_len); typedef void (*btck_NotifyFatalError)(void* user_data, const char* message, size_t message_len); +/** + * Function signatures for the validation interface. + */ +typedef void (*btck_ValidationInterfaceBlockChecked)(void* user_data, btck_Block* block, const btck_BlockValidationState* state); +typedef void (*btck_ValidationInterfacePoWValidBlock)(void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry); +typedef void (*btck_ValidationInterfaceBlockConnected)(void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry); +typedef void (*btck_ValidationInterfaceBlockDisconnected)(void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry); + +/** + * Holds the validation interface callbacks. The user data pointer may be used + * to point to user-defined structures to make processing the validation + * callbacks easier. Note that these callbacks block any further validation + * execution when they are called. + */ +typedef struct { + void* user_data; //!< Holds a user-defined opaque structure that is passed to the validation + //!< interface callbacks. If user_data_destroy is also defined ownership of the + //!< user_data is passed to the created context options and subsequently context. + btck_DestroyCallback user_data_destroy; //!< Frees the provided user data structure. + btck_ValidationInterfaceBlockChecked block_checked; //!< Called when a new block has been fully validated. Contains the + //!< result of its validation. + btck_ValidationInterfacePoWValidBlock pow_valid_block; //!< Called when a new block extends the header chain and has a valid transaction + //!< and segwit merkle root. + btck_ValidationInterfaceBlockConnected block_connected; //!< Called when a block is valid and has now been connected to the best chain. + btck_ValidationInterfaceBlockDisconnected block_disconnected; //!< Called during a re-org when a block has been removed from the best chain. +} btck_ValidationInterfaceCallbacks; + /** * A struct for holding the kernel notification callbacks. The user data * pointer may be used to point to user-defined structures to make processing @@ -674,6 +716,20 @@ BITCOINKERNEL_API void btck_context_options_set_notifications( btck_ContextOptions* context_options, btck_NotificationInterfaceCallbacks notifications) BITCOINKERNEL_ARG_NONNULL(1); +/** + * @brief Set the validation interface callbacks for the context options. The + * context created with the options will be configured for these validation + * interface callbacks. The callbacks will then be triggered from validation + * events issued by the chainstate manager created from the same context. + * + * @param[in] context_options Non-null, previously created with btck_context_options_create. + * @param[in] validation_interface_callbacks The callbacks used for passing validation information to the + * user. + */ +BITCOINKERNEL_API void btck_context_options_set_validation_interface( + btck_ContextOptions* context_options, + btck_ValidationInterfaceCallbacks validation_interface_callbacks) BITCOINKERNEL_ARG_NONNULL(1); + /** * Destroy the context options. */ @@ -835,7 +891,9 @@ BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_chainstate_manager_i * manager. Processing first does checks on the block, and if these passed, * saves it to disk. It then validates the block against the utxo set. If it is * valid, the chain is extended with it. The return value is not indicative of - * the block's validity. + * the block's validity. Detailed information on the validity of the block can + * be retrieved by registering the `block_checked` callback in the validation + * interface. * * @param[in] chainstate_manager Non-null. * @param[in] block Non-null, block to be validated. diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h index 4c378d3a8fa..7105f9e95d6 100644 --- a/src/kernel/bitcoinkernel_wrapper.h +++ b/src/kernel/bitcoinkernel_wrapper.h @@ -604,6 +604,34 @@ public: virtual void FatalErrorHandler(std::string_view error) {} }; +class BlockValidationState +{ +private: + const btck_BlockValidationState* m_state; + +public: + BlockValidationState(const btck_BlockValidationState* state) : m_state{state} {} + + BlockValidationState(const BlockValidationState&) = delete; + BlockValidationState& operator=(const BlockValidationState&) = delete; + BlockValidationState(BlockValidationState&&) = delete; + BlockValidationState& operator=(BlockValidationState&&) = delete; +}; + +class ValidationInterface +{ +public: + virtual ~ValidationInterface() = default; + + virtual void BlockChecked(Block block, const BlockValidationState state) {} + + virtual void PowValidBlock(BlockTreeEntry entry, Block block) {} + + virtual void BlockConnected(Block block, BlockTreeEntry entry) {} + + virtual void BlockDisconnected(Block block, BlockTreeEntry entry) {} +}; + class ChainParams : public Handle { public: @@ -641,6 +669,24 @@ public: .fatal_error = +[](void* user_data, const char* error, size_t error_len) { (*static_cast(user_data))->FatalErrorHandler({error, error_len}); }, }); } + + template + void SetValidationInterface(std::shared_ptr validation_interface) + { + static_assert(std::is_base_of_v); + auto heap_vi = std::make_unique>(std::move(validation_interface)); + using user_type = std::shared_ptr*; + btck_context_options_set_validation_interface( + get(), + btck_ValidationInterfaceCallbacks{ + .user_data = heap_vi.release(), + .user_data_destroy = +[](void* user_data) { delete static_cast(user_data); }, + .block_checked = +[](void* user_data, btck_Block* block, const btck_BlockValidationState* state) { (*static_cast(user_data))->BlockChecked(Block{block}, BlockValidationState{state}); }, + .pow_valid_block = +[](void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry) { (*static_cast(user_data))->PowValidBlock(BlockTreeEntry{entry}, Block{block}); }, + .block_connected = +[](void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry) { (*static_cast(user_data))->BlockConnected(Block{block}, BlockTreeEntry{entry}); }, + .block_disconnected = +[](void* user_data, btck_Block* block, const btck_BlockTreeEntry* entry) { (*static_cast(user_data))->BlockDisconnected(Block{block}, BlockTreeEntry{entry}); }, + }); + } }; class Context : public Handle diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp index cfe3b2d2d4a..4cf3df7f57e 100644 --- a/src/test/kernel/test_kernel.cpp +++ b/src/test/kernel/test_kernel.cpp @@ -126,6 +126,30 @@ public: } }; +class TestValidationInterface : public ValidationInterface +{ +public: + void BlockChecked(Block block, const BlockValidationState state) override + { + std::cout << "Block checked." << std::endl; + } + + void BlockConnected(Block block, BlockTreeEntry entry) override + { + std::cout << "Block connected." << std::endl; + } + + void PowValidBlock(BlockTreeEntry entry, Block block) override + { + std::cout << "Block passed pow verification" << std::endl; + } + + void BlockDisconnected(Block block, BlockTreeEntry entry) override + { + std::cout << "Block disconnected." << std::endl; + } +}; + void run_verify_test( const ScriptPubkey& spent_script_pubkey, const Transaction& spending_tx, @@ -481,12 +505,15 @@ BOOST_AUTO_TEST_CASE(btck_block) CheckRange(block_tx.Transactions(), block_tx.CountTransactions()); } -Context create_context(std::shared_ptr notifications, ChainType chain_type) +Context create_context(std::shared_ptr notifications, ChainType chain_type, std::shared_ptr validation_interface = nullptr) { ContextOptions options{}; ChainParams params{chain_type}; options.SetChainParams(params); options.SetNotifications(notifications); + if (validation_interface) { + options.SetValidationInterface(validation_interface); + } auto context{Context{options}}; return context; } @@ -571,7 +598,8 @@ void chainman_reindex_chainstate_test(TestDirectory& test_directory) void chainman_mainnet_validation_test(TestDirectory& test_directory) { auto notifications{std::make_shared()}; - auto context{create_context(notifications, ChainType::MAINNET)}; + auto validation_interface{std::make_shared()}; + auto context{create_context(notifications, ChainType::MAINNET, validation_interface)}; auto chainman{create_chainman(test_directory, false, false, false, false, context)}; {