kernel: Expose reusable PrecomputedTransactionData in script validation #33891

pull joshdoman wants to merge 1 commits into bitcoin:master from joshdoman:kernel/precomputed-txdata changing 4 files +186 −62
  1. joshdoman commented at 7:58 pm on November 17, 2025: none

    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. The design of this PR is inspired by @sedited’s comments.

    The PR introduces three new APIs for managing the btck_PrecomputedTransactionData object:

     0/**
     1 * [@brief](/bitcoin-bitcoin/contributor/brief/) Create precomputed transaction data for efficient script verification.
     2 *
     3 * [@param](/bitcoin-bitcoin/contributor/param/)[in] tx_to             Non-null.
     4 * [@param](/bitcoin-bitcoin/contributor/param/)[in] spent_outputs     Nullable for non-taproot verification. Points to an array of
     5 *                              outputs spent by the transaction.
     6 * [@param](/bitcoin-bitcoin/contributor/param/)[in] spent_outputs_len Length of the spent_outputs array.
     7 * [@return](/bitcoin-bitcoin/contributor/return/)                      The precomputed data, or null on error.
     8 */
     9btck_PrecomputedTransactionData* btck_precomputed_transaction_data_create(
    10    const btck_Transaction* tx_to,
    11    const btck_TransactionOutput** spent_outputs, size_t spent_outputs_len) BITCOINKERNEL_ARG_NONNULL(1);
    12
    13/**
    14 * [@brief](/bitcoin-bitcoin/contributor/brief/) Copy precomputed transaction data.
    15 *
    16 * [@param](/bitcoin-bitcoin/contributor/param/)[in] precomputed_txdata  Non-null.
    17 * [@return](/bitcoin-bitcoin/contributor/return/)                      The copied precomputed transaction data.
    18 */
    19btck_PrecomputedTransactionData* btck_precomputed_transaction_data_copy(
    20    const btck_PrecomputedTransactionData* precomputed_txdata) BITCOINKERNEL_ARG_NONNULL(1);
    21
    22/**
    23 * Destroy the precomputed transaction data.
    24 */
    25void 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:

     0/**
     1 * [@brief](/bitcoin-bitcoin/contributor/brief/) Verify if the input at input_index of tx_to spends the script pubkey
     2 * under the constraints specified by flags. If the
     3 * `btck_ScriptVerificationFlags_WITNESS` flag is set in the flags bitfield, the
     4 * amount parameter is used. If the taproot flag is set, the precomputed data
     5 * must contain the spent outputs.
     6 *
     7 * [@param](/bitcoin-bitcoin/contributor/param/)[in] script_pubkey      Non-null, script pubkey to be spent.
     8 * [@param](/bitcoin-bitcoin/contributor/param/)[in] amount             Amount of the script pubkey's associated output. May be zero if
     9 *                               the witness flag is not set.
    10 * [@param](/bitcoin-bitcoin/contributor/param/)[in] tx_to              Non-null, transaction spending the script_pubkey.
    11 * [@param](/bitcoin-bitcoin/contributor/param/)[in] precomputed_txdata Nullable if the taproot flag is not set. Otherwise, precomputed data
    12 *                               for tx_to with the spent outputs must be provided.
    13 * [@param](/bitcoin-bitcoin/contributor/param/)[in] input_index        Index of the input in tx_to spending the script_pubkey.
    14 * [@param](/bitcoin-bitcoin/contributor/param/)[in] flags              Bitfield of btck_ScriptVerificationFlags controlling validation constraints.
    15 * [@param](/bitcoin-bitcoin/contributor/param/)[out] status            Nullable, will be set to an error code if the operation fails, or OK otherwise.
    16 * [@return](/bitcoin-bitcoin/contributor/return/)                       1 if the script is valid, 0 otherwise.
    17 */
    18int btck_script_pubkey_verify(
    19    const btck_ScriptPubkey* script_pubkey,
    20    int64_t amount,
    21    const btck_Transaction* tx_to,
    22    const btck_PrecomputedTransactionData* precomputed_txdata,
    23    unsigned int input_index,
    24    btck_ScriptVerificationFlags flags,
    25    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!

  2. DrahtBot commented at 7:58 pm on November 17, 2025: contributor

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33891.

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK sedited
    Concept ACK yuvicc, w0xlt

    If your review is incorrectly listed, please copy-paste <!–meta-tag:bot-skip–> into the comment that the bot should ignore.

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #33943 (kernel: don’t use assert to handle invalid user input by stickies-v)
    • #33908 (kernel: add context‑free block validation API (btck_check_block_context_free) with POW/Merkle flags by w0xlt)
    • #33822 (kernel: Add block header support and validation by yuvicc)
    • #33796 (kernel: Expose CheckTransaction consensus validation function by w0xlt)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    LLM Linter (✨ experimental)

    Possible places where named args may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

    • run_verify_test(/spent_script_pubkey/ legacy_spent_script_pubkey, /spending_tx/ legacy_spending_tx, /precomputed_txdata/ nullptr, /amount/ 0, /input_index/ 0, /is_taproot/ false) in src/test/kernel/test_kernel.cpp
    • run_verify_test(/spent_script_pubkey/ legacy_spent_script_pubkey, /spending_tx/ legacy_spending_tx, /precomputed_txdata/ &legacy_precomputed_txdata, /amount/ 0, /input_index/ 0, /is_taproot/ false) in src/test/kernel/test_kernel.cpp
    • run_verify_test(/spent_script_pubkey/ segwit_spent_script_pubkey, /spending_tx/ segwit_spending_tx, /precomputed_txdata/ nullptr, /amount/ 18393430, /input_index/ 0, /is_taproot/ false) in src/test/kernel/test_kernel.cpp
    • run_verify_test(/spent_script_pubkey/ segwit_spent_script_pubkey, /spending_tx/ segwit_spending_tx, /precomputed_txdata/ &segwit_precomputed_txdata, /amount/ 18393430, /input_index/ 0, /is_taproot/ false) in src/test/kernel/test_kernel.cpp
    • run_verify_test(/spent_script_pubkey/ taproot_spent_script_pubkey, /spending_tx/ taproot_spending_tx, /precomputed_txdata/ &taproot_precomputed_txdata, /amount/ 88480, /input_index/ 0, /is_taproot/ true) in src/test/kernel/test_kernel.cpp
    • run_verify_test(/spent_script_pubkey/ taproot2_spent_script_pubkey0, /spending_tx/ taproot2_spending_tx, /precomputed_txdata/ &taproot2_precomputed_txdata, /amount/ 546, /input_index/ 0, /is_taproot/ true) in src/test/kernel/test_kernel.cpp
    • run_verify_test(/spent_script_pubkey/ taproot2_spent_script_pubkey1, /spending_tx/ taproot2_spending_tx, /precomputed_txdata/ &taproot2_precomputed_txdata, /amount/ 135125, /input_index/ 1, /is_taproot/ true) in src/test/kernel/test_kernel.cpp

    2025-11-27

  3. joshdoman renamed this:
    [kernel] Expose reusable `PrecomputedTransactionData` in script validation
    kernel: Expose reusable `PrecomputedTransactionData` in script validation
    on Nov 17, 2025
  4. DrahtBot added the label Validation on Nov 17, 2025
  5. yuvicc commented at 6:41 am on November 18, 2025: contributor

    Concept ACK

    Wrote a small program to test btck_PrecomputedTransactionData.

  6. w0xlt commented at 0:45 am on November 19, 2025: contributor
    Concept ACK
  7. sedited commented at 10:33 am on November 26, 2025: contributor
    This looks reasonable, Concept ACK
  8. in src/test/kernel/test_kernel.cpp:523 in 0dadeb2e06
    519         /*amount*/ 18393430,
    520         /*input_index*/ 0,
    521         /*is_taproot*/ false);
    522 
    523     // Taproot transaction 33e794d097969002ee05d336686fc03c9e15a597c1b9827669460fac98799036
    524+    auto spending_tx{Transaction{hex_string_to_byte_vec("01000000000101d1f1c1f8cdf6759167b90f52c9ad358a369f95284e841d7a2536cef31c0549580100000000fdffffff020000000000000000316a2f49206c696b65205363686e6f7272207369677320616e6420492063616e6e6f74206c69652e204062697462756734329e06010000000000225120a37c3903c8d0db6512e2b40b0dffa05e5a3ab73603ce8c9c4b7771e5412328f90140a60c383f71bac0ec919b1d7dbc3eb72dd56e7aa99583615564f9f99b8ae4e837b758773a5b2e4c51348854c8389f008e05029db7f464a5ff2e01d5e6e626174affd30a00")}};
    


    sedited commented at 10:54 am on November 27, 2025:
    It would be nice to get a test where we verify both inputs of a two-input taproot transaction with the same data. I think we currently do that implicitly in the full-chain script verification test further down, but I’d like an explicit test too.

    joshdoman commented at 8:57 pm on November 27, 2025:
    An explicit test makes sense. Just added one.
  9. in src/kernel/bitcoinkernel.cpp:619 in 0dadeb2e06
    614+{
    615+    try {
    616+        const CTransaction& tx{*btck_Transaction::get(tx_to)};
    617+        PrecomputedTransactionData txdata{tx};
    618+        std::vector<CTxOut> spent_outputs;
    619+        if (spent_outputs_ != nullptr) {
    


    sedited commented at 10:58 am on November 27, 2025:
    Can you also check that spent_outputs_len is 0 here? It might be the case that the passed in pointer is not null by chance, or UB for that matter, but the intent is still clearly communicated by passing a zero size.

    joshdoman commented at 8:59 pm on November 27, 2025:
    No problem.
  10. in src/kernel/bitcoinkernel.cpp:629 in 0dadeb2e06
    624+                spent_outputs.push_back(tx_out);
    625+            }
    626+            txdata.Init(tx, std::move(spent_outputs));
    627+        }
    628+
    629+        return btck_PrecomputedTransactionData::create(std::move(txdata));
    


    sedited commented at 11:21 am on November 27, 2025:

    I realize you are immitating my own patterns here, but I think it would be better to not rely too heaviliy on the move constructor here, and instead do the following:

     0diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
     1index 4102ee9305..2e86c3d8df 100644
     2--- a/src/kernel/bitcoinkernel.cpp
     3+++ b/src/kernel/bitcoinkernel.cpp
     4@@ -617 +617 @@ btck_PrecomputedTransactionData* btck_precomputed_transaction_data_create(
     5-        PrecomputedTransactionData txdata{tx};
     6+        auto txdata{btck_PrecomputedTransactionData::create(tx)};
     7@@ -626 +626 @@ btck_PrecomputedTransactionData* btck_precomputed_transaction_data_create(
     8-            txdata.Init(tx, std::move(spent_outputs));
     9+            btck_PrecomputedTransactionData::get(txdata).Init(tx, std::move(spent_outputs));
    10@@ -629 +629 @@ btck_PrecomputedTransactionData* btck_precomputed_transaction_data_create(
    11-        return btck_PrecomputedTransactionData::create(std::move(txdata));
    12+        return txdata;
    

    There some other cases in this file where this pattern might be better, but I don’t think it is worth fixing here.


    joshdoman commented at 8:58 pm on November 27, 2025:
    Good suggestion, made that change.
  11. in src/kernel/bitcoinkernel.h:646 in 0dadeb2e06 outdated
    643@@ -606,7 +644,7 @@ BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_script_pubkey_verify
    644     const btck_ScriptPubkey* script_pubkey,
    645     int64_t amount,
    646     const btck_Transaction* tx_to,
    


    sedited commented at 11:29 am on November 27, 2025:
    The reason I was hesitant to submit this patch earlier is that the developer needs to associate the precomputed data with this transaction, and we don’t have a straight forward semantic mechanism to enforce that. While I don’t think this is particularly problematic, since the same can also be said for the outputs currently, it should be clearly communicated in the documentation for this function and I think also deserves a test case with such a mismatch.

    joshdoman commented at 9:05 pm on November 27, 2025:

    To try to mitigate this concern, I renamed the parameter precomputed_txdata, to make it more clear that it pertains to the actual transaction being verified (previously the parameter was more vaguely named precomputed_data).

    I also updated the parameter description in the documentation for btck_script_pubkey_verify, to make it explicit that the parameter is derived from tx_to and the spent outputs.

    Hopefully these changes help, but open to other suggestions!

  12. [kernel] Expose reusable `PrecomputedTransactionData` in script validation 17d3575cfc
  13. in src/test/kernel/test_kernel.cpp:517 in 0dadeb2e06
    513@@ -514,19 +514,21 @@ BOOST_AUTO_TEST_CASE(btck_script_verify_tests)
    514     run_verify_test(
    515         /*spent_script_pubkey*/ ScriptPubkey{hex_string_to_byte_vec("0020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d")},
    516         /*spending_tx*/ Transaction{hex_string_to_byte_vec("010000000001011f97548fbbe7a0db7588a66e18d803d0089315aa7d4cc28360b6ec50ef36718a0100000000ffffffff02df1776000000000017a9146c002a686959067f4866b8fb493ad7970290ab728757d29f0000000000220020701a8d401c84fb13e6baf169d59684e17abd9fa216c8cc5b9fc63d622ff8c58d04004730440220565d170eed95ff95027a69b313758450ba84a01224e1f7f130dda46e94d13f8602207bdd20e307f062594022f12ed5017bbf4a055a06aea91c10110a0e3bb23117fc014730440220647d2dc5b15f60bc37dc42618a370b2a1490293f9e5c8464f53ec4fe1dfe067302203598773895b4b16d37485cbe21b337f4e4b650739880098c592553add7dd4355016952210375e00eb72e29da82b89367947f29ef34afb75e8654f6ea368e0acdfd92976b7c2103a1b26313f430c4b15bb1fdce663207659d8cac749a0e53d70eff01874496feff2103c96d495bfdd5ba4145e3e046fee45e84a8a48ad05bd8dbb395c011a32cf9f88053ae00000000")},
    517-        /*spent_outputs*/ {},
    518+        /*precomputed_data*/ nullptr,
    


    sedited commented at 11:39 am on November 27, 2025:
    Can you also add a case for passing in precomputed data for both the segwit and legacy transaction?

    joshdoman commented at 9:05 pm on November 27, 2025:
    Done!
  14. joshdoman force-pushed on Nov 27, 2025
  15. sedited approved
  16. sedited commented at 9:00 pm on November 28, 2025: contributor
    ACK 17d3575cfcb41adfd364e7a1912a4701786f7455
  17. DrahtBot requested review from w0xlt on Nov 28, 2025

github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2025-11-30 00:13 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me