kernel: Add function to read block undo data from disk to C header

This adds functions for reading the undo data from disk with a retrieved
block tree entry. The undo data of a block contains all the spent
script pubkeys of all the transactions in a block. For ease of
understanding the undo data is renamed to spent outputs with seperate
data structures exposed for a block's and a transaction's spent outputs.

In normal operations undo data is used during re-orgs. This data might
also be useful for building external indexes, or to scan for silent
payment transactions.

Internally the block undo data contains a vector of transaction undo
data which contains a vector of the coins consumed. The coins are all
int the order of the transaction inputs of the consuming transactions.
Each coin can be used to retrieve a transaction output and in turn a
script pubkey and amount.

This translates to the three-level hierarchy the api provides: Block
spent outputs contain transaction spent outputs, which contain
individual coins. Each coin includes the associated output, the height
of the block is contained in, and whether it is from a coinbase
transaction.
This commit is contained in:
TheCharlatan 2024-06-01 14:47:56 +02:00
parent 3a2b8d855d
commit d9d040fd16
No known key found for this signature in database
GPG Key ID: 9B79B45691DB4173
4 changed files with 417 additions and 4 deletions

View File

@ -7,6 +7,7 @@
#include <kernel/bitcoinkernel.h>
#include <chain.h>
#include <coins.h>
#include <consensus/amount.h>
#include <consensus/validation.h>
#include <kernel/caches.h>
@ -28,6 +29,7 @@
#include <sync.h>
#include <tinyformat.h>
#include <uint256.h>
#include <undo.h>
#include <util/fs.h>
#include <util/result.h>
#include <util/signalinterrupt.h>
@ -492,6 +494,9 @@ struct btck_ChainParameters : Handle<btck_ChainParameters, std::unique_ptr<const
struct btck_ChainstateManagerOptions : Handle<btck_ChainstateManagerOptions, ChainstateManagerOptions> {};
struct btck_ChainstateManager : Handle<btck_ChainstateManager, ChainMan> {};
struct btck_Chain : Handle<btck_Chain, CChain> {};
struct btck_BlockSpentOutputs : Handle<btck_BlockSpentOutputs, std::shared_ptr<CBlockUndo>> {};
struct btck_TransactionSpentOutputs : Handle<btck_TransactionSpentOutputs, CTxUndo> {};
struct btck_Coin : Handle<btck_Coin, Coin> {};
btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len)
{
@ -999,6 +1004,89 @@ btck_Block* btck_block_read(const btck_ChainstateManager* chainman, const btck_B
return btck_Block::create(block);
}
btck_BlockSpentOutputs* btck_block_spent_outputs_read(const btck_ChainstateManager* chainman, const btck_BlockTreeEntry* entry)
{
auto block_undo{std::make_shared<CBlockUndo>()};
if (btck_BlockTreeEntry::get(entry).nHeight < 1) {
LogDebug(BCLog::KERNEL, "The genesis block does not have any spent outputs.");
return btck_BlockSpentOutputs::create(block_undo);
}
if (!btck_ChainstateManager::get(chainman).m_chainman->m_blockman.ReadBlockUndo(*block_undo, btck_BlockTreeEntry::get(entry))) {
LogError("Failed to read block spent outputs data.");
return nullptr;
}
return btck_BlockSpentOutputs::create(block_undo);
}
btck_BlockSpentOutputs* btck_block_spent_outputs_copy(const btck_BlockSpentOutputs* block_spent_outputs)
{
return btck_BlockSpentOutputs::copy(block_spent_outputs);
}
size_t btck_block_spent_outputs_count(const btck_BlockSpentOutputs* block_spent_outputs)
{
return btck_BlockSpentOutputs::get(block_spent_outputs)->vtxundo.size();
}
const btck_TransactionSpentOutputs* btck_block_spent_outputs_get_transaction_spent_outputs_at(const btck_BlockSpentOutputs* block_spent_outputs, size_t transaction_index)
{
assert(transaction_index < btck_BlockSpentOutputs::get(block_spent_outputs)->vtxundo.size());
const auto* tx_undo{&btck_BlockSpentOutputs::get(block_spent_outputs)->vtxundo.at(transaction_index)};
return btck_TransactionSpentOutputs::ref(tx_undo);
}
void btck_block_spent_outputs_destroy(btck_BlockSpentOutputs* block_spent_outputs)
{
delete block_spent_outputs;
}
btck_TransactionSpentOutputs* btck_transaction_spent_outputs_copy(const btck_TransactionSpentOutputs* transaction_spent_outputs)
{
return btck_TransactionSpentOutputs::copy(transaction_spent_outputs);
}
size_t btck_transaction_spent_outputs_count(const btck_TransactionSpentOutputs* transaction_spent_outputs)
{
return btck_TransactionSpentOutputs::get(transaction_spent_outputs).vprevout.size();
}
void btck_transaction_spent_outputs_destroy(btck_TransactionSpentOutputs* transaction_spent_outputs)
{
delete transaction_spent_outputs;
}
const btck_Coin* btck_transaction_spent_outputs_get_coin_at(const btck_TransactionSpentOutputs* transaction_spent_outputs, size_t coin_index)
{
assert(coin_index < btck_TransactionSpentOutputs::get(transaction_spent_outputs).vprevout.size());
const Coin* coin{&btck_TransactionSpentOutputs::get(transaction_spent_outputs).vprevout.at(coin_index)};
return btck_Coin::ref(coin);
}
btck_Coin* btck_coin_copy(const btck_Coin* coin)
{
return btck_Coin::copy(coin);
}
uint32_t btck_coin_confirmation_height(const btck_Coin* coin)
{
return btck_Coin::get(coin).nHeight;
}
int btck_coin_is_coinbase(const btck_Coin* coin)
{
return btck_Coin::get(coin).IsCoinBase() ? 1 : 0;
}
const btck_TransactionOutput* btck_coin_get_output(const btck_Coin* coin)
{
return btck_TransactionOutput::ref(&btck_Coin::get(coin).out);
}
void btck_coin_destroy(btck_Coin* coin)
{
delete coin;
}
int btck_chainstate_manager_process_block(
btck_ChainstateManager* chainman,
const btck_Block* block,

View File

@ -211,6 +211,36 @@ typedef struct btck_BlockValidationState btck_BlockValidationState;
*/
typedef struct btck_Chain btck_Chain;
/**
* Opaque data structure for holding a block's spent outputs.
*
* Contains all the previous outputs consumed by all transactions in a specific
* block. Internally it holds a nested vector. The top level vector has an
* entry for each transaction in a block (in order of the actual transactions
* of the block and without the coinbase transaction). This is exposed through
* @ref btck_TransactionSpentOutputs. Each btck_TransactionSpentOutputs is in
* turn a vector of all the previous outputs of a transaction (in order of
* their corresponding inputs).
*/
typedef struct btck_BlockSpentOutputs btck_BlockSpentOutputs;
/**
* Opaque data structure for holding a transaction's spent outputs.
*
* Holds the coins consumed by a certain transaction. Retrieved through the
* @ref btck_BlockSpentOutputs. The coins are in the same order as the
* transaction's inputs consuming them.
*/
typedef struct btck_TransactionSpentOutputs btck_TransactionSpentOutputs;
/**
* Opaque data structure for holding a coin.
*
* Holds information on the @ref btck_TransactionOutput held within,
* including the height it was spent at and whether it is a coinbase output.
*/
typedef struct btck_Coin btck_Coin;
/** Current sync state passed to tip changed callbacks. */
typedef uint8_t btck_SynchronizationState;
#define btck_SynchronizationState_INIT_REINDEX ((btck_SynchronizationState)(0))
@ -1081,6 +1111,156 @@ BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_chain_get_height(
///@}
/** @name BlockSpentOutputs
* Functions for working with block spent outputs.
*/
///@{
/**
* @brief Reads the block spent coins data the passed in block tree entry points to from
* disk and returns it.
*
* @param[in] chainstate_manager Non-null.
* @param[in] block_tree_entry Non-null.
* @return The read out block spent outputs, or null on error.
*/
BITCOINKERNEL_API btck_BlockSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_read(
const btck_ChainstateManager* chainstate_manager,
const btck_BlockTreeEntry* block_tree_entry) BITCOINKERNEL_ARG_NONNULL(1, 2);
/**
* @brief Copy a block's spent outputs.
*
* @param[in] block_spent_outputs Non-null.
* @return The copied block spent outputs.
*/
BITCOINKERNEL_API btck_BlockSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_copy(
const btck_BlockSpentOutputs* block_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Returns the number of transaction spent outputs whose data is contained in
* block spent outputs.
*
* @param[in] block_spent_outputs Non-null.
* @return The number of transaction spent outputs data in the block spent outputs.
*/
BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_count(
const btck_BlockSpentOutputs* block_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Returns a transaction spent outputs contained in the block spent
* outputs at a certain index. The returned pointer is unowned and only valid
* for the lifetime of block_spent_outputs.
*
* @param[in] block_spent_outputs Non-null.
* @param[in] transaction_spent_outputs_index The index of the transaction spent outputs within the block spent outputs.
* @return A transaction spent outputs pointer.
*/
BITCOINKERNEL_API const btck_TransactionSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_spent_outputs_get_transaction_spent_outputs_at(
const btck_BlockSpentOutputs* block_spent_outputs,
size_t transaction_spent_outputs_index) BITCOINKERNEL_ARG_NONNULL(1);
/**
* Destroy the block spent outputs.
*/
BITCOINKERNEL_API void btck_block_spent_outputs_destroy(btck_BlockSpentOutputs* block_spent_outputs);
///@}
/** @name TransactionSpentOutputs
* Functions for working with the spent coins of a transaction
*/
///@{
/**
* @brief Copy a transaction's spent outputs.
*
* @param[in] transaction_spent_outputs Non-null.
* @return The copied transaction spent outputs.
*/
BITCOINKERNEL_API btck_TransactionSpentOutputs* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_spent_outputs_copy(
const btck_TransactionSpentOutputs* transaction_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Returns the number of previous transaction outputs contained in the
* transaction spent outputs data.
*
* @param[in] transaction_spent_outputs Non-null
* @return The number of spent transaction outputs for the transaction.
*/
BITCOINKERNEL_API size_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_spent_outputs_count(
const btck_TransactionSpentOutputs* transaction_spent_outputs) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Returns a coin contained in the transaction spent outputs at a
* certain index. The returned pointer is unowned and only valid for the
* lifetime of transaction_spent_outputs.
*
* @param[in] transaction_spent_outputs Non-null.
* @param[in] coin_index The index of the to be retrieved coin within the
* transaction spent outputs.
* @return A coin pointer.
*/
BITCOINKERNEL_API const btck_Coin* BITCOINKERNEL_WARN_UNUSED_RESULT btck_transaction_spent_outputs_get_coin_at(
const btck_TransactionSpentOutputs* transaction_spent_outputs,
size_t coin_index) BITCOINKERNEL_ARG_NONNULL(1);
/**
* Destroy the transaction spent outputs.
*/
BITCOINKERNEL_API void btck_transaction_spent_outputs_destroy(btck_TransactionSpentOutputs* transaction_spent_outputs);
///@}
/** @name Coin
* Functions for working with coins.
*/
///@{
/**
* @brief Copy a coin.
*
* @param[in] coin Non-null.
* @return The copied coin.
*/
BITCOINKERNEL_API btck_Coin* BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_copy(
const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Returns the height of the block that contains the coin's prevout.
*
* @param[in] coin Non-null.
* @return The block height of the coin.
*/
BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_confirmation_height(
const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Returns whether the containing transaction was a coinbase.
*
* @param[in] coin Non-null.
* @return 1 if the coin is a coinbase coin, 0 otherwise.
*/
BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_is_coinbase(
const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1);
/**
* @brief Return the transaction output of a coin. The returned pointer is
* unowned and only valid for the lifetime of the coin.
*
* @param[in] coin Non-null.
* @return A transaction output pointer.
*/
BITCOINKERNEL_API const btck_TransactionOutput* BITCOINKERNEL_WARN_UNUSED_RESULT btck_coin_get_output(
const btck_Coin* coin) BITCOINKERNEL_ARG_NONNULL(1);
/**
* Destroy the coin.
*/
BITCOINKERNEL_API void btck_coin_destroy(btck_Coin* coin);
///@}
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus

View File

@ -802,6 +802,111 @@ public:
}
};
template <typename Derived>
class CoinApi
{
private:
auto impl() const
{
return static_cast<const Derived*>(this)->get();
}
friend Derived;
CoinApi() = default;
public:
uint32_t GetConfirmationHeight() const { return btck_coin_confirmation_height(impl()); }
bool IsCoinbase() const { return btck_coin_is_coinbase(impl()) == 1; }
TransactionOutputView GetOutput() const
{
return TransactionOutputView{btck_coin_get_output(impl())};
}
};
class CoinView : public View<btck_Coin>, public CoinApi<CoinView>
{
public:
explicit CoinView(const btck_Coin* ptr) : View{ptr} {}
};
class Coin : public Handle<btck_Coin, btck_coin_copy, btck_coin_destroy>, public CoinApi<Coin>
{
public:
Coin(btck_Coin* coin) : Handle{coin} {}
Coin(const CoinView& view) : Handle{view} {}
};
template <typename Derived>
class TransactionSpentOutputsApi
{
private:
auto impl() const
{
return static_cast<const Derived*>(this)->get();
}
friend Derived;
TransactionSpentOutputsApi() = default;
public:
size_t Count() const
{
return btck_transaction_spent_outputs_count(impl());
}
CoinView GetCoin(size_t index) const
{
return CoinView{btck_transaction_spent_outputs_get_coin_at(impl(), index)};
}
auto Coins() const
{
return Range<Derived, &TransactionSpentOutputsApi<Derived>::Count, &TransactionSpentOutputsApi<Derived>::GetCoin>{*static_cast<const Derived*>(this)};
}
};
class TransactionSpentOutputsView : public View<btck_TransactionSpentOutputs>, public TransactionSpentOutputsApi<TransactionSpentOutputsView>
{
public:
explicit TransactionSpentOutputsView(const btck_TransactionSpentOutputs* ptr) : View{ptr} {}
};
class TransactionSpentOutputs : public Handle<btck_TransactionSpentOutputs, btck_transaction_spent_outputs_copy, btck_transaction_spent_outputs_destroy>,
public TransactionSpentOutputsApi<TransactionSpentOutputs>
{
public:
TransactionSpentOutputs(btck_TransactionSpentOutputs* transaction_spent_outputs) : Handle{transaction_spent_outputs} {}
TransactionSpentOutputs(const TransactionSpentOutputsView& view) : Handle{view} {}
};
class BlockSpentOutputs : public Handle<btck_BlockSpentOutputs, btck_block_spent_outputs_copy, btck_block_spent_outputs_destroy>
{
public:
BlockSpentOutputs(btck_BlockSpentOutputs* block_spent_outputs)
: Handle{block_spent_outputs}
{
}
size_t Count() const
{
return btck_block_spent_outputs_count(get());
}
TransactionSpentOutputsView GetTxSpentOutputs(size_t tx_undo_index) const
{
return TransactionSpentOutputsView{btck_block_spent_outputs_get_transaction_spent_outputs_at(get(), tx_undo_index)};
}
auto TxsSpentOutputs() const
{
return Range<BlockSpentOutputs, &BlockSpentOutputs::Count, &BlockSpentOutputs::GetTxSpentOutputs>{*this};
}
};
class ChainMan : UniqueHandle<btck_ChainstateManager, btck_chainstate_manager_destroy>
{
public:
@ -843,6 +948,11 @@ public:
if (!block) return std::nullopt;
return block;
}
BlockSpentOutputs ReadBlockSpentOutputs(const BlockTreeEntry& entry) const
{
return btck_block_spent_outputs_read(get(), entry.get());
}
};
} // namespace btck

View File

@ -731,11 +731,13 @@ void chainman_mainnet_validation_test(TestDirectory& test_directory)
check_equal(read_block.value().ToBytes(), raw_block);
// Check that we can read the previous block
auto tip_2{tip.GetPrevious()};
auto read_block_2{chainman->ReadBlock(tip_2.value())};
BlockTreeEntry tip_2{*tip.GetPrevious()};
Block read_block_2{*chainman->ReadBlock(tip_2)};
BOOST_CHECK_EQUAL(chainman->ReadBlockSpentOutputs(tip_2).Count(), 0);
BOOST_CHECK_EQUAL(chainman->ReadBlockSpentOutputs(tip).Count(), 0);
// It should be an error if we go another block back, since the genesis has no ancestor
BOOST_CHECK(!tip_2.value().GetPrevious());
BOOST_CHECK(!tip_2.GetPrevious());
// If we try to validate it again, it should be a duplicate
BOOST_CHECK(chainman->ProcessBlock(block, &new_block));
@ -820,6 +822,39 @@ BOOST_AUTO_TEST_CASE(btck_chainman_regtest_tests)
auto read_block_2 = chainman->ReadBlock(tip_2).value();
check_equal(read_block_2.ToBytes(), as_bytes(REGTEST_BLOCK_DATA[REGTEST_BLOCK_DATA.size() - 2]));
BlockSpentOutputs block_spent_outputs{chainman->ReadBlockSpentOutputs(tip)};
BlockSpentOutputs block_spent_outputs_prev{chainman->ReadBlockSpentOutputs(*tip.GetPrevious())};
CheckHandle(block_spent_outputs, block_spent_outputs_prev);
CheckRange(block_spent_outputs_prev.TxsSpentOutputs(), block_spent_outputs_prev.Count());
BOOST_CHECK_EQUAL(block_spent_outputs.Count(), 1);
TransactionSpentOutputsView transaction_spent_outputs{block_spent_outputs.GetTxSpentOutputs(block_spent_outputs.Count() - 1)};
TransactionSpentOutputs owned_transaction_spent_outputs{transaction_spent_outputs};
TransactionSpentOutputs owned_transaction_spent_outputs_prev{block_spent_outputs_prev.GetTxSpentOutputs(block_spent_outputs_prev.Count() - 1)};
CheckHandle(owned_transaction_spent_outputs, owned_transaction_spent_outputs_prev);
CheckRange(transaction_spent_outputs.Coins(), transaction_spent_outputs.Count());
CoinView coin{transaction_spent_outputs.GetCoin(transaction_spent_outputs.Count() - 1)};
BOOST_CHECK(!coin.IsCoinbase());
Coin owned_coin{coin};
Coin owned_coin_prev{owned_transaction_spent_outputs_prev.GetCoin(owned_transaction_spent_outputs_prev.Count() - 1)};
CheckHandle(owned_coin, owned_coin_prev);
TransactionOutputView output = coin.GetOutput();
uint32_t coin_height = coin.GetConfirmationHeight();
BOOST_CHECK_EQUAL(coin_height, 205);
BOOST_CHECK_EQUAL(output.Amount(), 100000000);
auto script_pubkey = output.GetScriptPubkey();
auto script_pubkey_bytes{script_pubkey.ToBytes()};
BOOST_CHECK_EQUAL(script_pubkey_bytes.size(), 22);
auto round_trip_script_pubkey{ScriptPubkey(script_pubkey_bytes)};
BOOST_CHECK_EQUAL(round_trip_script_pubkey.ToBytes().size(), 22);
for (const auto tx_spent_outputs : block_spent_outputs.TxsSpentOutputs()) {
for (const auto coins : tx_spent_outputs.Coins()) {
BOOST_CHECK_GT(coins.GetOutput().Amount(), 1);
}
}
std::filesystem::remove_all(test_directory.m_directory / "blocks" / "blk00000.dat");
BOOST_CHECK(!chainman->ReadBlock(tip_2));
BOOST_CHECK(!chainman->ReadBlock(tip_2).has_value());
std::filesystem::remove_all(test_directory.m_directory / "blocks" / "rev00000.dat");
BOOST_CHECK_THROW(chainman->ReadBlockSpentOutputs(tip), std::runtime_error);
}