Skip to content

Commit eb0594e

Browse files
committed
Merge bitcoin/bitcoin#33891: kernel: Expose reusable PrecomputedTransactionData in script validation
44e006d [kernel] Expose reusable PrecomputedTransactionData in script valid (Josh Doman) Pull request description: This PR exposes a reusable `PrecomputedTransactionData` object in script validation using libkernel. Currently, libkernel computes `PrecomputedTransactionData` each time `btck_script_pubkey_verify` is called, exposing clients to quadratic hashing when validating a transaction with multiple inputs. By externalizing `PrecomputedTransactionData` and making it reusable, libkernel can eliminate this attack vector. I discussed this problem in [this issue](sedited/rust-bitcoinkernel#46). The design of this PR is inspired by @sedited's comments. The PR introduces three new APIs for managing the `btck_PrecomputedTransactionData` object: ```c /** * @brief Create precomputed transaction data for script verification. * * @param[in] tx_to Non-null. * @param[in] spent_outputs Nullable for non-taproot verification. Points to an array of * outputs spent by the transaction. * @param[in] spent_outputs_len Length of the spent_outputs array. * @return The precomputed data, or null on error. */ btck_PrecomputedTransactionData* btck_precomputed_transaction_data_create( const btck_Transaction* tx_to, const btck_TransactionOutput** spent_outputs, size_t spent_outputs_len) BITCOINKERNEL_ARG_NONNULL(1); /** * @brief Copy precomputed transaction data. * * @param[in] precomputed_txdata Non-null. * @return The copied precomputed transaction data. */ btck_PrecomputedTransactionData* btck_precomputed_transaction_data_copy( const btck_PrecomputedTransactionData* precomputed_txdata) BITCOINKERNEL_ARG_NONNULL(1); /** * Destroy the precomputed transaction data. */ void btck_precomputed_transaction_data_destroy(btck_PrecomputedTransactionData* precomputed_txdata); ``` The PR also modifies `btck_script_pubkey_verify` so that it accepts `precomputed_txdata` instead of `spent_outputs`: ```c /** * @brief Verify if the input at input_index of tx_to spends the script pubkey * under the constraints specified by flags. If the * `btck_ScriptVerificationFlags_WITNESS` flag is set in the flags bitfield, the * amount parameter is used. If the taproot flag is set, the precomputed data * must contain the spent outputs. * * @param[in] script_pubkey Non-null, script pubkey to be spent. * @param[in] amount Amount of the script pubkey's associated output. May be zero if * the witness flag is not set. * @param[in] tx_to Non-null, transaction spending the script_pubkey. * @param[in] precomputed_txdata Nullable if the taproot flag is not set. Otherwise, precomputed data * for tx_to with the spent outputs must be provided. * @param[in] input_index Index of the input in tx_to spending the script_pubkey. * @param[in] flags Bitfield of btck_ScriptVerificationFlags controlling validation constraints. * @param[out] status Nullable, will be set to an error code if the operation fails, or OK otherwise. * @return 1 if the script is valid, 0 otherwise. */ int btck_script_pubkey_verify( const btck_ScriptPubkey* script_pubkey, int64_t amount, const btck_Transaction* tx_to, const btck_PrecomputedTransactionData* precomputed_txdata, unsigned int input_index, btck_ScriptVerificationFlags flags, btck_ScriptVerifyStatus* status) BITCOINKERNEL_ARG_NONNULL(1, 3); ``` As before, an error is thrown if the taproot flag is set and `spent_outputs` is not provided in `precomputed_txdata` (or `precomputed_txdata` is null). For simple single-input non-taproot verification, `precomputed_txdata` may be null, and the kernel will construct the precomputed data on-the-fly. Both the C++ wrapper and the test suite are updated with the new API. Tests cover both `precomputed_txdata` reuse and nullability. Appreciate feedback on this concept / approach! ACKs for top commit: sedited: Re-ACK 44e006d stringintech: ACK 44e006d Tree-SHA512: 1ed435173e6ff4ec82bc603194cf182c685cb79f167439a442b9b179a32f6c189c358f04d4cb56d153fab04e3424a11b73c31680e42b87b8a6efcc3ccefc366c
2 parents e703360 + 44e006d commit eb0594e

File tree

4 files changed

+221
-74
lines changed

4 files changed

+221
-74
lines changed

src/kernel/bitcoinkernel.cpp

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,7 @@ struct btck_BlockHash : Handle<btck_BlockHash, uint256> {};
495495
struct btck_TransactionInput : Handle<btck_TransactionInput, CTxIn> {};
496496
struct btck_TransactionOutPoint: Handle<btck_TransactionOutPoint, COutPoint> {};
497497
struct btck_Txid: Handle<btck_Txid, Txid> {};
498+
struct btck_PrecomputedTransactionData : Handle<btck_PrecomputedTransactionData, PrecomputedTransactionData> {};
498499

499500
btck_Transaction* btck_transaction_create(const void* raw_transaction, size_t raw_transaction_len)
500501
{
@@ -608,10 +609,46 @@ void btck_transaction_output_destroy(btck_TransactionOutput* output)
608609
delete output;
609610
}
610611

612+
btck_PrecomputedTransactionData* btck_precomputed_transaction_data_create(
613+
const btck_Transaction* tx_to,
614+
const btck_TransactionOutput** spent_outputs_, size_t spent_outputs_len)
615+
{
616+
try {
617+
const CTransaction& tx{*btck_Transaction::get(tx_to)};
618+
auto txdata{btck_PrecomputedTransactionData::create()};
619+
if (spent_outputs_ != nullptr && spent_outputs_len > 0) {
620+
assert(spent_outputs_len == tx.vin.size());
621+
std::vector<CTxOut> spent_outputs;
622+
spent_outputs.reserve(spent_outputs_len);
623+
for (size_t i = 0; i < spent_outputs_len; i++) {
624+
const CTxOut& tx_out{btck_TransactionOutput::get(spent_outputs_[i])};
625+
spent_outputs.push_back(tx_out);
626+
}
627+
btck_PrecomputedTransactionData::get(txdata).Init(tx, std::move(spent_outputs));
628+
} else {
629+
btck_PrecomputedTransactionData::get(txdata).Init(tx, {});
630+
}
631+
632+
return txdata;
633+
} catch (...) {
634+
return nullptr;
635+
}
636+
}
637+
638+
btck_PrecomputedTransactionData* btck_precomputed_transaction_data_copy(const btck_PrecomputedTransactionData* precomputed_txdata)
639+
{
640+
return btck_PrecomputedTransactionData::copy(precomputed_txdata);
641+
}
642+
643+
void btck_precomputed_transaction_data_destroy(btck_PrecomputedTransactionData* precomputed_txdata)
644+
{
645+
delete precomputed_txdata;
646+
}
647+
611648
int btck_script_pubkey_verify(const btck_ScriptPubkey* script_pubkey,
612649
const int64_t amount,
613650
const btck_Transaction* tx_to,
614-
const btck_TransactionOutput** spent_outputs_, size_t spent_outputs_len,
651+
const btck_PrecomputedTransactionData* precomputed_txdata,
615652
const unsigned int input_index,
616653
const btck_ScriptVerificationFlags flags,
617654
btck_ScriptVerifyStatus* status)
@@ -624,31 +661,18 @@ int btck_script_pubkey_verify(const btck_ScriptPubkey* script_pubkey,
624661
return 0;
625662
}
626663

627-
if (flags & btck_ScriptVerificationFlags_TAPROOT && spent_outputs_ == nullptr) {
664+
const CTransaction& tx{*btck_Transaction::get(tx_to)};
665+
assert(input_index < tx.vin.size());
666+
667+
const PrecomputedTransactionData& txdata{precomputed_txdata ? btck_PrecomputedTransactionData::get(precomputed_txdata) : PrecomputedTransactionData(tx)};
668+
669+
if (flags & btck_ScriptVerificationFlags_TAPROOT && txdata.m_spent_outputs.empty()) {
628670
if (status) *status = btck_ScriptVerifyStatus_ERROR_SPENT_OUTPUTS_REQUIRED;
629671
return 0;
630672
}
631673

632674
if (status) *status = btck_ScriptVerifyStatus_OK;
633675

634-
const CTransaction& tx{*btck_Transaction::get(tx_to)};
635-
std::vector<CTxOut> spent_outputs;
636-
if (spent_outputs_ != nullptr) {
637-
assert(spent_outputs_len == tx.vin.size());
638-
spent_outputs.reserve(spent_outputs_len);
639-
for (size_t i = 0; i < spent_outputs_len; i++) {
640-
const CTxOut& tx_out{btck_TransactionOutput::get(spent_outputs_[i])};
641-
spent_outputs.push_back(tx_out);
642-
}
643-
}
644-
645-
assert(input_index < tx.vin.size());
646-
PrecomputedTransactionData txdata{tx};
647-
648-
if (spent_outputs_ != nullptr && flags & btck_ScriptVerificationFlags_TAPROOT) {
649-
txdata.Init(tx, std::move(spent_outputs));
650-
}
651-
652676
bool result = VerifyScript(tx.vin[input_index].scriptSig,
653677
btck_ScriptPubkey::get(script_pubkey),
654678
&tx.vin[input_index].scriptWitness,

src/kernel/bitcoinkernel.h

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,16 @@ typedef struct btck_TransactionInput btck_TransactionInput;
282282
*/
283283
typedef struct btck_TransactionOutPoint btck_TransactionOutPoint;
284284

285+
/**
286+
* Opaque data structure for holding precomputed transaction data.
287+
*
288+
* Reusable when verifying multiple inputs of the same transaction.
289+
* This avoids recomputing transaction hashes for each input.
290+
*
291+
* Required when verifying a taproot input.
292+
*/
293+
typedef struct btck_PrecomputedTransactionData btck_PrecomputedTransactionData;
294+
285295
typedef struct btck_Txid btck_Txid;
286296

287297
/** Current sync state passed to tip changed callbacks. */
@@ -571,6 +581,40 @@ BITCOINKERNEL_API void btck_transaction_destroy(btck_Transaction* transaction);
571581

572582
///@}
573583

584+
/** @name PrecomputedTransactionData
585+
* Functions for working with precomputed transaction data.
586+
*/
587+
///@{
588+
589+
/**
590+
* @brief Create precomputed transaction data for script verification.
591+
*
592+
* @param[in] tx_to Non-null.
593+
* @param[in] spent_outputs Nullable for non-taproot verification. Points to an array of
594+
* outputs spent by the transaction.
595+
* @param[in] spent_outputs_len Length of the spent_outputs array.
596+
* @return The precomputed data, or null on error.
597+
*/
598+
BITCOINKERNEL_API btck_PrecomputedTransactionData* BITCOINKERNEL_WARN_UNUSED_RESULT btck_precomputed_transaction_data_create(
599+
const btck_Transaction* tx_to,
600+
const btck_TransactionOutput** spent_outputs, size_t spent_outputs_len) BITCOINKERNEL_ARG_NONNULL(1);
601+
602+
/**
603+
* @brief Copy precomputed transaction data.
604+
*
605+
* @param[in] precomputed_txdata Non-null.
606+
* @return The copied precomputed transaction data.
607+
*/
608+
BITCOINKERNEL_API btck_PrecomputedTransactionData* BITCOINKERNEL_WARN_UNUSED_RESULT btck_precomputed_transaction_data_copy(
609+
const btck_PrecomputedTransactionData* precomputed_txdata) BITCOINKERNEL_ARG_NONNULL(1);
610+
611+
/**
612+
* Destroy the precomputed transaction data.
613+
*/
614+
BITCOINKERNEL_API void btck_precomputed_transaction_data_destroy(btck_PrecomputedTransactionData* precomputed_txdata);
615+
616+
///@}
617+
574618
/** @name ScriptPubkey
575619
* Functions for working with script pubkeys.
576620
*/
@@ -598,26 +642,25 @@ BITCOINKERNEL_API btck_ScriptPubkey* BITCOINKERNEL_WARN_UNUSED_RESULT btck_scrip
598642
* @brief Verify if the input at input_index of tx_to spends the script pubkey
599643
* under the constraints specified by flags. If the
600644
* `btck_ScriptVerificationFlags_WITNESS` flag is set in the flags bitfield, the
601-
* amount parameter is used. If the taproot flag is set, the spent outputs
602-
* parameter is used to validate taproot transactions.
603-
*
604-
* @param[in] script_pubkey Non-null, script pubkey to be spent.
605-
* @param[in] amount Amount of the script pubkey's associated output. May be zero if
606-
* the witness flag is not set.
607-
* @param[in] tx_to Non-null, transaction spending the script_pubkey.
608-
* @param[in] spent_outputs Nullable if the taproot flag is not set. Points to an array of
609-
* outputs spent by the transaction.
610-
* @param[in] spent_outputs_len Length of the spent_outputs array.
611-
* @param[in] input_index Index of the input in tx_to spending the script_pubkey.
612-
* @param[in] flags Bitfield of btck_ScriptVerificationFlags controlling validation constraints.
613-
* @param[out] status Nullable, will be set to an error code if the operation fails, or OK otherwise.
614-
* @return 1 if the script is valid, 0 otherwise.
645+
* amount parameter is used. If the taproot flag is set, the precomputed data
646+
* must contain the spent outputs.
647+
*
648+
* @param[in] script_pubkey Non-null, script pubkey to be spent.
649+
* @param[in] amount Amount of the script pubkey's associated output. May be zero if
650+
* the witness flag is not set.
651+
* @param[in] tx_to Non-null, transaction spending the script_pubkey.
652+
* @param[in] precomputed_txdata Nullable if the taproot flag is not set. Otherwise, precomputed data
653+
* for tx_to with the spent outputs must be provided.
654+
* @param[in] input_index Index of the input in tx_to spending the script_pubkey.
655+
* @param[in] flags Bitfield of btck_ScriptVerificationFlags controlling validation constraints.
656+
* @param[out] status Nullable, will be set to an error code if the operation fails, or OK otherwise.
657+
* @return 1 if the script is valid, 0 otherwise.
615658
*/
616659
BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_script_pubkey_verify(
617660
const btck_ScriptPubkey* script_pubkey,
618661
int64_t amount,
619662
const btck_Transaction* tx_to,
620-
const btck_TransactionOutput** spent_outputs, size_t spent_outputs_len,
663+
const btck_PrecomputedTransactionData* precomputed_txdata,
621664
unsigned int input_index,
622665
btck_ScriptVerificationFlags flags,
623666
btck_ScriptVerifyStatus* status) BITCOINKERNEL_ARG_NONNULL(1, 3);

src/kernel/bitcoinkernel_wrapper.h

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ class UniqueHandle
367367
const CType* get() const { return m_ptr.get(); }
368368
};
369369

370+
class PrecomputedTransactionData;
370371
class Transaction;
371372
class TransactionOutput;
372373

@@ -385,7 +386,7 @@ class ScriptPubkeyApi
385386
public:
386387
bool Verify(int64_t amount,
387388
const Transaction& tx_to,
388-
std::span<const TransactionOutput> spent_outputs,
389+
const PrecomputedTransactionData* precomputed_txdata,
389390
unsigned int input_index,
390391
ScriptVerificationFlags flags,
391392
ScriptVerifyStatus& status) const;
@@ -626,29 +627,30 @@ class Transaction : public Handle<btck_Transaction, btck_transaction_copy, btck_
626627
: Handle{view} {}
627628
};
628629

630+
class PrecomputedTransactionData : public Handle<btck_PrecomputedTransactionData, btck_precomputed_transaction_data_copy, btck_precomputed_transaction_data_destroy>
631+
{
632+
public:
633+
explicit PrecomputedTransactionData(const Transaction& tx_to, std::span<const TransactionOutput> spent_outputs)
634+
: Handle{btck_precomputed_transaction_data_create(
635+
tx_to.get(),
636+
reinterpret_cast<const btck_TransactionOutput**>(
637+
const_cast<TransactionOutput*>(spent_outputs.data())),
638+
spent_outputs.size())} {}
639+
};
640+
629641
template <typename Derived>
630642
bool ScriptPubkeyApi<Derived>::Verify(int64_t amount,
631643
const Transaction& tx_to,
632-
const std::span<const TransactionOutput> spent_outputs,
644+
const PrecomputedTransactionData* precomputed_txdata,
633645
unsigned int input_index,
634646
ScriptVerificationFlags flags,
635647
ScriptVerifyStatus& status) const
636648
{
637-
const btck_TransactionOutput** spent_outputs_ptr = nullptr;
638-
std::vector<const btck_TransactionOutput*> raw_spent_outputs;
639-
if (spent_outputs.size() > 0) {
640-
raw_spent_outputs.reserve(spent_outputs.size());
641-
642-
for (const auto& output : spent_outputs) {
643-
raw_spent_outputs.push_back(output.get());
644-
}
645-
spent_outputs_ptr = raw_spent_outputs.data();
646-
}
647649
auto result = btck_script_pubkey_verify(
648650
impl(),
649651
amount,
650652
tx_to.get(),
651-
spent_outputs_ptr, spent_outputs.size(),
653+
precomputed_txdata ? precomputed_txdata->get() : nullptr,
652654
input_index,
653655
static_cast<btck_ScriptVerificationFlags>(flags),
654656
reinterpret_cast<btck_ScriptVerifyStatus*>(&status));

0 commit comments

Comments
 (0)