kernel: add serialization method for btck_BlockHeader API #34401

pull yuvicc wants to merge 3 commits into bitcoin:master from yuvicc:2026-11-followup_33822 changing 6 files +97 −1
  1. yuvicc commented at 6:50 AM on January 25, 2026: contributor

    This adds serialization for btck_BlockHeader API. Also, updated the CheckHandle to compare the byte content instead of size.

    The changes here is done in two commits. First commit adds the SpanWriter class and next one moves the block header serialization to SpanWriter. See commit message for more details.

    Follow-up to #33822 .

  2. DrahtBot added the label Validation on Jan 25, 2026
  3. DrahtBot commented at 6:50 AM on January 25, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

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

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

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

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline for information on the review process.

    Type Reviewers
    ACK stickies-v, theStack, alexanderwiederin, w0xlt
    Concept ACK musaHaruna

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    No conflicts as of last run.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

    LLM Linter (✨ experimental)

    Possible places where comparison-specific test macros should replace generic comparisons:

    • [src/test/streams_tests.cpp] BOOST_CHECK_THROW(SpanWriter(std::span{small}, a, b), std::ios_base::failure); -> Prefer BOOST_CHECK_EXCEPTION(..., std::ios_base::failure, HasReason("SpanWriter::write(): exceeded buffer size")) (or the exact thrown message), instead of only checking the exception type.
    • [src/test/streams_tests.cpp] BOOST_CHECK_THROW(SpanWriter(std::span{small}) << a << b, std::ios_base::failure); -> Prefer BOOST_CHECK_EXCEPTION(..., std::ios_base::failure, HasReason(...)) to assert the precise failure reason/message.

    <sup>2026-04-14 13:50:26</sup>

  4. yuvicc renamed this:
    kernel: add serialization for btck_BlockHeader API
    kernel: add serialization method for btck_BlockHeader API
    on Jan 25, 2026
  5. in src/kernel/bitcoinkernel.h:1772 in d14cdea5cf outdated
    1767 | @@ -1768,6 +1768,20 @@ BITCOINKERNEL_API int32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_get
    1768 |  BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_get_nonce(
    1769 |      const btck_BlockHeader* header) BITCOINKERNEL_ARG_NONNULL(1);
    1770 |  
    1771 | +/**
    1772 | + * @brief Serializes the btck_BlockHeader through the passed in callback to bytes.
    


    stickies-v commented at 3:58 PM on January 27, 2026:

    For consistency with other serialization function documentation, would be good to add

    This is consensus serialization that is also used for the P2P network.

  6. in src/kernel/bitcoinkernel.h:1780 in d14cdea5cf
    1775 | + * @param[in] writer    Non-null, callback to a write bytes function.
    1776 | + * @param[in] user_data Holds a user-defined opaque structure that will be
    1777 | + *                      passed back through the writer callback.
    1778 | + * @return              0 on success.
    1779 | + */
    1780 | +BITCOINKERNEL_API int btck_block_header_to_bytes(
    


    stickies-v commented at 3:59 PM on January 27, 2026:

    Headers are fixed 80 byte structures, so I think we can just return a fixed-length buffer instead of using a writer - see e.g. btck_block_hash_to_bytes.

    <details> <summary>git diff on d14cdea5cf</summary>

    diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
    index 525d861827..2fd5a51873 100644
    --- a/src/kernel/bitcoinkernel.cpp
    +++ b/src/kernel/bitcoinkernel.cpp
    @@ -1389,15 +1389,11 @@ uint32_t btck_block_header_get_nonce(const btck_BlockHeader* header)
         return btck_BlockHeader::get(header).nNonce;
     }
     
    -int btck_block_header_to_bytes(const btck_BlockHeader* header, btck_WriteBytes writer, void* user_data)
    +void btck_block_header_to_bytes(const btck_BlockHeader* header, unsigned char output[80])
     {
    -    try {
    -        WriterStream ws{writer, user_data};
    -        ws << btck_BlockHeader::get(header);
    -        return 0;
    -    } catch (...) {
    -        return -1;
    -    }
    +    DataStream stream{};
    +    stream << btck_BlockHeader::get(header);
    +    std::memcpy(output, stream.data(), 80);
     }
     
     void btck_block_header_destroy(btck_BlockHeader* header)
    diff --git a/src/kernel/bitcoinkernel.h b/src/kernel/bitcoinkernel.h
    index 53491ea69e..4f6ae41c61 100644
    --- a/src/kernel/bitcoinkernel.h
    +++ b/src/kernel/bitcoinkernel.h
    @@ -1769,18 +1769,13 @@ BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_ge
         const btck_BlockHeader* header) BITCOINKERNEL_ARG_NONNULL(1);
     
     /**
    - * [@brief](/bitcoin-bitcoin/contributor/brief/) Serializes the btck_BlockHeader through the passed in callback to bytes.
    + * [@brief](/bitcoin-bitcoin/contributor/brief/) Serializes the btck_BlockHeader to bytes.
      *
      * [@param](/bitcoin-bitcoin/contributor/param/)[in] header    Non-null.
    - * [@param](/bitcoin-bitcoin/contributor/param/)[in] writer    Non-null, callback to a write bytes function.
    - * [@param](/bitcoin-bitcoin/contributor/param/)[in] user_data Holds a user-defined opaque structure that will be
    - *                      passed back through the writer callback.
    - * [@return](/bitcoin-bitcoin/contributor/return/)              0 on success.
    + * [@param](/bitcoin-bitcoin/contributor/param/)[out] output   The serialized block header (80 bytes).
      */
    -BITCOINKERNEL_API int btck_block_header_to_bytes(
    -    const btck_BlockHeader* header,
    -    btck_WriteBytes writer,
    -    void* user_data) BITCOINKERNEL_ARG_NONNULL(1, 2);
    +BITCOINKERNEL_API void btck_block_header_to_bytes(
    +    const btck_BlockHeader* header, unsigned char output[80]) BITCOINKERNEL_ARG_NONNULL(1, 2);
     
     /**
      * Destroy the btck_BlockHeader.
    diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h
    index c23ad8ba3b..95afd90480 100644
    --- a/src/kernel/bitcoinkernel_wrapper.h
    +++ b/src/kernel/bitcoinkernel_wrapper.h
    @@ -747,9 +747,11 @@ public:
             return btck_block_header_get_nonce(impl());
         }
     
    -    std::vector<std::byte> ToBytes() const
    +    std::array<std::byte, 80> ToBytes() const
         {
    -        return write_bytes(impl(), btck_block_header_to_bytes);
    +        std::array<std::byte, 80> header;
    +        btck_block_header_to_bytes(impl(), reinterpret_cast<unsigned char*>(header.data()));
    +        return header;
         }
     };
     
    diff --git a/src/test/kernel/test_kernel.cpp b/src/test/kernel/test_kernel.cpp
    index 86c2e25057..79c88c94c7 100644
    --- a/src/test/kernel/test_kernel.cpp
    +++ b/src/test/kernel/test_kernel.cpp
    @@ -266,7 +266,7 @@ void run_verify_test(
     
     template <typename T>
     concept HasToBytes = requires(T t) {
    -    { t.ToBytes() } -> std::convertible_to<std::vector<std::byte>>;
    +    { t.ToBytes() } -> std::convertible_to<std::span<const std::byte>>;
     };
     
     template <typename T>
    
    

    </details>


    yuvicc commented at 7:47 AM on January 31, 2026:

    Makes sense, thanks.

  7. stickies-v commented at 4:47 PM on January 27, 2026: contributor

    Concept ACK, definitely useful to have.

  8. yuvicc force-pushed on Jan 31, 2026
  9. DrahtBot added the label CI failed on Jan 31, 2026
  10. DrahtBot commented at 9:13 AM on January 31, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/21541544208/job/62076715283</sub> <sub>LLM reason (✨ experimental): Linting failed due to include-usage warnings (lint-includes.py) causing the all_python_linters check to fail.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  11. yuvicc force-pushed on Jan 31, 2026
  12. DrahtBot removed the label CI failed on Jan 31, 2026
  13. yuvicc commented at 3:38 PM on January 31, 2026: contributor

    Thanks for review @stickies-v.

    • return fixed length buffer instead of writer comment
    • Addressed nit
  14. in src/kernel/bitcoinkernel.cpp:1399 in ebc8433637
    1390 | @@ -1391,6 +1391,13 @@ uint32_t btck_block_header_get_nonce(const btck_BlockHeader* header)
    1391 |      return btck_BlockHeader::get(header).nNonce;
    1392 |  }
    1393 |  
    1394 | +void btck_block_header_to_bytes(const btck_BlockHeader* header, unsigned char output[80])
    1395 | +{
    1396 | +    DataStream stream{};
    1397 | +    stream << btck_BlockHeader::get(header);
    1398 | +    std::memcpy(output, stream.data(), 80);
    1399 | +}
    


    stickies-v commented at 12:03 PM on February 2, 2026:

    Sorry, my previous suggestion wasn't entirely sound. This function can throw an exception that crosses the C boundary (e.g. if memory allocation fails), so we should better handle this.

    Some options are:

    1. Wrapping this in a try/catch and returning an int status code
    2. Reverting to the WriterStream approach, which adds some unnecessary overhead
    3. Implement a SpanWriter to avoid allocating memory. I'm not sure about the guarantees we have that serializing the block header will never throw, though, so this might be in addition to Option 1 (but still has the benefit of avoiding allocation)

    Possible SpanWriter approach:

    <details> <summary>git diff on ebc8433637</summary>

    diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
    index 2b129f2952..cdac10a05d 100644
    --- a/src/kernel/bitcoinkernel.cpp
    +++ b/src/kernel/bitcoinkernel.cpp
    @@ -1393,9 +1393,7 @@ uint32_t btck_block_header_get_nonce(const btck_BlockHeader* header)
     
     void btck_block_header_to_bytes(const btck_BlockHeader* header, unsigned char output[80])
     {
    -    DataStream stream{};
    -    stream << btck_BlockHeader::get(header);
    -    std::memcpy(output, stream.data(), 80);
    +    SpanWriter{std::span{output, 80}} << btck_BlockHeader::get(header);
     }
     
     void btck_block_header_destroy(btck_BlockHeader* header)
    diff --git a/src/streams.h b/src/streams.h
    index 466084e9fa..3358fa994a 100644
    --- a/src/streams.h
    +++ b/src/streams.h
    @@ -77,6 +77,38 @@ private:
         size_t nPos;
     };
     
    +/** Minimal stream for writing to an existing span of bytes.
    + */
    +class SpanWriter
    +{
    +private:
    +    std::span<std::byte> m_data;
    +    size_t m_pos{0};
    +
    +public:
    +    explicit SpanWriter(std::span<unsigned char> data) : m_data{std::as_writable_bytes(data)} {}
    +    explicit SpanWriter(std::span<std::byte> data) : m_data{data} {}
    +    template <typename... Args>
    +    SpanWriter(std::span<unsigned char> data, Args&&... args) : SpanWriter{data}
    +    {
    +        ::SerializeMany(*this, std::forward<Args>(args)...);
    +    }
    +
    +    void write(std::span<const std::byte> src)
    +    {
    +        assert(m_pos + src.size() <= m_data.size());
    +        memcpy(m_data.data() + m_pos, src.data(), src.size());
    +        m_pos += src.size();
    +    }
    +
    +    template <typename T>
    +    SpanWriter& operator<<(const T& obj)
    +    {
    +        ::Serialize(*this, obj);
    +        return *this;
    +    }
    +};
    +
     /** Minimal stream for reading from an existing byte array by std::span.
      */
     class SpanReader
    diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp
    index af75ee987a..29fce0612d 100644
    --- a/src/test/streams_tests.cpp
    +++ b/src/test/streams_tests.cpp
    @@ -207,6 +207,31 @@ BOOST_AUTO_TEST_CASE(streams_vector_writer)
         vch.clear();
     }
     
    +BOOST_AUTO_TEST_CASE(streams_span_writer)
    +{
    +    unsigned char a(1);
    +    unsigned char b(2);
    +    unsigned char bytes[] = {3, 4, 5, 6};
    +    std::array<unsigned char, 8> arr{};
    +
    +    // Test operator<<.
    +    SpanWriter writer{arr};
    +    writer << a << b;
    +    std::array<unsigned char, 8> expected1{{1, 2, 0, 0, 0, 0, 0, 0}};
    +    BOOST_CHECK_EQUAL_COLLECTIONS(arr.begin(), arr.end(), expected1.begin(), expected1.end());
    +
    +    // Use variadic constructor and write to subspan.
    +    SpanWriter{std::span{arr}.subspan(2), a, bytes, b};
    +    std::array<unsigned char, 8> expected2{{1, 2, 1, 3, 4, 5, 6, 2}};
    +    BOOST_CHECK_EQUAL_COLLECTIONS(arr.begin(), arr.end(), expected2.begin(), expected2.end());
    +
    +    // Test std::byte span constructor.
    +    std::array<std::byte, 2> byte_arr{};
    +    SpanWriter{std::span{byte_arr}} << a << b;
    +    std::array<std::byte, 2> expected3{{std::byte{1}, std::byte{2}}};
    +    BOOST_CHECK_EQUAL_COLLECTIONS(byte_arr.begin(), byte_arr.end(), expected3.begin(), expected3.end());
    +}
    +
     BOOST_AUTO_TEST_CASE(streams_vector_reader)
     {
         std::vector<unsigned char> vch = {1, 255, 3, 4, 5, 6};
    
    

    </details> [@sedited](/bitcoin-bitcoin/contributor/sedited/) do you have any preferences here?


    yuvicc commented at 5:41 PM on February 2, 2026:

    I think as a defensive mechanism, we should still use try-catch here to ensure exceptions never cross the C API boundary, even though header serialization shouldn't fail under normal circumstances.

  15. DrahtBot added the label Needs rebase on Feb 4, 2026
  16. yuvicc force-pushed on Feb 21, 2026
  17. DrahtBot removed the label Needs rebase on Feb 21, 2026
  18. yuvicc commented at 4:01 PM on February 21, 2026: contributor

    Added SpanWriter approach as suggested by @stickies-v and also split into two commits. First commit adds the SpanWriter class and next one moves the block header serialization to SpanWriter. See commit message for more details. Also updated the PR description.

  19. yuvicc requested review from stickies-v on Feb 21, 2026
  20. ?
    added_to_project_v2 sedited
  21. ?
    project_v2_item_status_changed github-project-automation[bot]
  22. ?
    project_v2_item_status_changed sedited
  23. sedited requested review from alexanderwiederin on Mar 3, 2026
  24. alexanderwiederin commented at 2:32 PM on March 4, 2026: contributor

    Concept ACK.

    I recommend we wrap the span serialisation in a try/catch, as you suggested earlier, and return an int like the other to_bytes methods.

    The commit messages can be more descriptive and I would also split the second commit into two: one for the method and the other for the HasToBytes change.

  25. ?
    project_v2_item_status_changed alexanderwiederin
  26. ?
    project_v2_item_status_changed alexanderwiederin
  27. musaHaruna commented at 11:52 AM on March 6, 2026: contributor

    Concept ACK Still familiarizing myself with the Kernel API, but still looked at the code through a newbie lens. In 77c36df, the SpanWriter class provides a minimal serialization stream that writes bytes into a pre-allocated memory buffer represented by a std::span. It maintains an internal write position (m_pos) and appends serialized data sequentially into the span.

    One design detail I noticed is the bounds check inside write():

    assert(m_pos + src.size() <= m_data.size());

    Based on my understanding, the use of assert here instead of throwing an exception means that this condition is a programming invariant rather than a recoverable runtime error. The class assumes that callers are responsible for providing a buffer that is large enough for the serialized data.

    So in case of an incorrect input, the caller will be the one to handle the error at runtime? IIUC, this is what this comment below is suggesting if such case of incorrect inputs occurs.

    I recommend we wrap the span serialisation in a try/catch, as you suggested earlier, and return an int like the other to_bytes methods.

  28. yuvicc force-pushed on Mar 9, 2026
  29. yuvicc force-pushed on Mar 9, 2026
  30. DrahtBot added the label CI failed on Mar 9, 2026
  31. DrahtBot commented at 1:38 PM on March 9, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task test ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/22855674050/job/66295237030</sub> <sub>LLM reason (✨ experimental): CI failure due to a build error: the cmake/gmake compilation of src/kernel/bitcoinkernel.cpp failed with a non-zero exit status.</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  32. DrahtBot removed the label CI failed on Mar 9, 2026
  33. yuvicc commented at 4:14 PM on March 9, 2026: contributor

    Added try-catch in the serialization part. Also, split the second commit into two and made the commit message more descriptive.

  34. in src/kernel/bitcoinkernel.h:1772 in d00950235d
    1767 | @@ -1768,6 +1768,16 @@ BITCOINKERNEL_API int32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_get
    1768 |  BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_get_nonce(
    1769 |      const btck_BlockHeader* header) BITCOINKERNEL_ARG_NONNULL(1);
    1770 |  
    1771 | +/**
    1772 | + * @brief Serializes the btck_BlockHeader through the passed in callback to bytes.
    


    alexanderwiederin commented at 10:58 AM on March 10, 2026:

    I would suggest @brief Serializes the block header to bytes. - or similar.

    there is no callback.


    yuvicc commented at 2:00 PM on March 16, 2026:

    Correct will update that. Also, as discussed in WG call, doc naming convention: always use struct names instead of natural names.

  35. in src/kernel/bitcoinkernel.h:1776 in d00950235d outdated
    1767 | @@ -1768,6 +1768,16 @@ BITCOINKERNEL_API int32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_get
    1768 |  BITCOINKERNEL_API uint32_t BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_get_nonce(
    1769 |      const btck_BlockHeader* header) BITCOINKERNEL_ARG_NONNULL(1);
    1770 |  
    1771 | +/**
    1772 | + * @brief Serializes the btck_BlockHeader through the passed in callback to bytes.
    1773 | + * This is consensus serialization that is also used for the P2P network.
    1774 | + *
    1775 | + * @param[in] header    Non-null.
    1776 | + * @param[out] output   The serialized block header (80 bytes).
    


    alexanderwiederin commented at 11:15 AM on March 10, 2026:

    To align with the others:

     * [@param](/bitcoin-bitcoin/contributor/param/)[out] output   Non-null, 80-byte buffer to write the serialized header into.
     * [@return](/bitcoin-bitcoin/contributor/return/)              0 on success.
    
  36. alexanderwiederin commented at 11:20 AM on March 10, 2026: contributor

    Thanks.

    I still think we can improve the commit messages. I use this here for guidance: https://cbea.ms/git-commit/

  37. yuvicc force-pushed on Mar 16, 2026
  38. yuvicc commented at 2:15 PM on March 16, 2026: contributor

    Improved the commits messages and fixed some nits.

  39. theStack commented at 2:51 AM on March 18, 2026: contributor

    Concept ACK

  40. yuvicc force-pushed on Mar 25, 2026
  41. yuvicc commented at 7:19 AM on March 25, 2026: contributor

    updated commit messages

  42. w0xlt commented at 12:21 AM on March 27, 2026: contributor

    Concept ACK

  43. in src/streams.h:143 in 80e98e44b7
     141 | +        ::SerializeMany(*this, std::forward<Args>(args)...);
     142 | +    }
     143 | +
     144 | +    void write(std::span<const std::byte> src)
     145 | +    {
     146 | +        assert(m_pos + src.size() <= m_data.size());
    


    stickies-v commented at 12:23 PM on March 27, 2026:

    Throwing is probably more robust than asserting here (which e.g. wouldn't be handled in btck_block_header_to_bytes), mirroring SpanReader::read. Also (m_pos + src.size() can overflow, so could address both as:

    <details> <summary>git diff on 80e98e44b7</summary>

    diff --git a/src/streams.h b/src/streams.h
    index fcaf835768..d390b712a0 100644
    --- a/src/streams.h
    +++ b/src/streams.h
    @@ -140,7 +140,9 @@ public:
     
         void write(std::span<const std::byte> src)
         {
    -        assert(m_pos + src.size() <= m_data.size());
    +        if (src.size() > m_data.size() - m_pos) {
    +            throw std::ios_base::failure("SpanWriter::write(): exceeded buffer size");
    +        }
             memcpy(m_data.data() + m_pos, src.data(), src.size());
             m_pos += src.size();
         }
    
    

    </details>

    We could then also add a test case:

    <details> <summary>git diff on 80e98e44b7</summary>

    diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp
    index ef3b04e2e8..c5ee057d60 100644
    --- a/src/test/streams_tests.cpp
    +++ b/src/test/streams_tests.cpp
    @@ -230,6 +230,10 @@ BOOST_AUTO_TEST_CASE(streams_span_writer)
         SpanWriter{std::span{byte_arr}} << a << b;
         std::array<std::byte, 2> expected3{std::byte{1}, std::byte{2}};
         BOOST_CHECK_EQUAL_COLLECTIONS(byte_arr.begin(), byte_arr.end(), expected3.begin(), expected3.end());
    +
    +    // Writing past the end throws
    +    std::array<unsigned char, 1> small{};
    +    BOOST_CHECK_THROW(SpanWriter(small, a, b), std::ios_base::failure);
     }
     
     BOOST_AUTO_TEST_CASE(streams_vector_reader)
    
    

    </details>


    yuvicc commented at 11:55 AM on April 1, 2026:

    Done in f615589f2e9dd41f54c95da6c2f728bbf027d852. Thanks

  44. in src/streams.h:126 in 80e98e44b7 outdated
     124 |      }
     125 |  };
     126 |  
     127 | +/** Minimal stream for writing to an existing span of bytes.
     128 | + */
     129 | +class SpanWriter
    


    stickies-v commented at 12:29 PM on March 27, 2026:

    In commit d45a84b250ca57d3da1996a999e89d3a0b342201 message:

    ... instead of using writerstream, which incurs unnecessary overhead through dynamic memory allocations.

    This is a conversation about the best block header serialization approach, and unrelated to the SpanWriter commit. I think just the commit title is fine, here.


    yuvicc commented at 11:55 AM on April 1, 2026:

    Done.

  45. in src/kernel/bitcoinkernel_wrapper.h:753 in 80e98e44b7
     745 | @@ -756,6 +746,13 @@ class BlockHeaderApi
     746 |      {
     747 |          return btck_block_header_get_nonce(impl());
     748 |      }
     749 | +
     750 | +    std::array<std::byte, 80> ToBytes() const
     751 | +    {
     752 | +        std::array<std::byte, 80> header;
     753 | +        btck_block_header_to_bytes(impl(),  reinterpret_cast<unsigned char*>(header.data()));
    


    stickies-v commented at 12:46 PM on March 27, 2026:

    Probably shouldn't ignore errors here? Also, double space before reinterpret_cast


    yuvicc commented at 11:58 AM on April 1, 2026:

    Agree, the BlockHeader construction for the C++ wrapper should probably throw for invalid data and I have also added a check in the ToBytes() method. f798ece7a72aa025c79ad906b2b7ea7e2fdfa885

  46. in src/kernel/bitcoinkernel.h:1779 in 80e98e44b7
    1774 | + *
    1775 | + * @param[in] header    Non-null.
    1776 | + * @param[out] output   The serialized block header (80 bytes).
    1777 | + * @return              0 on success.
    1778 | + */
    1779 | +BITCOINKERNEL_API int btck_block_header_to_bytes(
    


    stickies-v commented at 12:47 PM on March 27, 2026:
    BITCOINKERNEL_API int BITCOINKERNEL_WARN_UNUSED_RESULT btck_block_header_to_bytes(
    

    yuvicc commented at 11:56 AM on April 1, 2026:

    Done in f798ece7a72aa025c79ad906b2b7ea7e2fdfa885. Thanks

  47. in src/test/kernel/test_kernel.cpp:1 in 80e98e44b7 outdated


    stickies-v commented at 4:08 PM on March 27, 2026:

    b76bcbd12e2202a5cf5ee5bb7c837c1667f12c7a commit message still mentions CheckHandle, but I think that's no longer in this commit.


    yuvicc commented at 11:56 AM on April 1, 2026:

    Done.

  48. stickies-v commented at 4:09 PM on March 27, 2026: contributor

    Reviewed 80e98e44b735da2e3b80073042b08014199b0e01 - LGTM once comments are addressed.

  49. yuvicc force-pushed on Apr 1, 2026
  50. yuvicc commented at 12:00 PM on April 1, 2026: contributor
  51. in src/kernel/bitcoinkernel_wrapper.h:757 in bb92d5fa65
     752 | +        std::array<std::byte, 80> header;
     753 | +        auto res = btck_block_header_to_bytes(impl(), reinterpret_cast<unsigned char*>(header.data()));
     754 | +        if (res == 0) {
     755 | +            return header;
     756 | +        }
     757 | +        throw std::runtime_error("Failed to serialize block header");
    


    stickies-v commented at 12:39 PM on April 1, 2026:

    nit: stylistic, but we would typically check for the error condition to throw, and then return. Also I think it's better to specify the int type here and use brace init to prevent narrowing conversion.

    <details> <summary>git diff on bb92d5fa65</summary>

    diff --git a/src/kernel/bitcoinkernel_wrapper.h b/src/kernel/bitcoinkernel_wrapper.h
    index 5a56cbf732..63ca62b0ca 100644
    --- a/src/kernel/bitcoinkernel_wrapper.h
    +++ b/src/kernel/bitcoinkernel_wrapper.h
    @@ -750,11 +750,11 @@ public:
         std::array<std::byte, 80> ToBytes() const
         {
             std::array<std::byte, 80> header;
    -        auto res = btck_block_header_to_bytes(impl(), reinterpret_cast<unsigned char*>(header.data()));
    -        if (res == 0) {
    -            return header;
    +        int res{btck_block_header_to_bytes(impl(), reinterpret_cast<unsigned char*>(header.data()))};
    +        if (res != 0) {
    +            throw std::runtime_error("Failed to serialize block header");
             }
    -        throw std::runtime_error("Failed to serialize block header");
    +        return header;
         }
     };
     
    
    

    </details>


    yuvicc commented at 2:54 PM on April 1, 2026:

    Done in c7fec6c91f7a79ba5fc1cf8866fbab5b8da43b26

  52. in src/streams.h:144 in bb92d5fa65
     142 | +    }
     143 | +
     144 | +    void write(std::span<const std::byte> src)
     145 | +    {
     146 | +        if (src.size() > m_data.size() - m_pos) {
     147 | +            throw std::ios_base::failure("Spanwriter::write(): exceeded buffer size");
    


    stickies-v commented at 12:39 PM on April 1, 2026:
                throw std::ios_base::failure("SpanWriter::write(): exceeded buffer size");
    

    yuvicc commented at 2:53 PM on April 1, 2026:

    done in fdf49462af08dc99a7317d199c7b60b42317f393

  53. in src/streams.h:130 in bb92d5fa65
     128 | + */
     129 | +class SpanWriter
     130 | +{
     131 | +private:
     132 | +    std::span<std::byte> m_data;
     133 | +    size_t m_pos{0};
    


    stickies-v commented at 1:06 PM on April 1, 2026:

    Sorry, another approach suggestion. Since this class doesn't allowing seeking/rewind etc, we don't actually need the position tracking. We can also improve naming a bit, with dest better reflecting that we're writing into this span that the generic data.

    <details> <summary>git diff on bb92d5fa65</summary>

    diff --git a/src/streams.h b/src/streams.h
    index 78c820e066..b57c443287 100644
    --- a/src/streams.h
    +++ b/src/streams.h
    @@ -126,25 +126,24 @@ public:
     class SpanWriter
     {
     private:
    -    std::span<std::byte> m_data;
    -    size_t m_pos{0};
    +    std::span<std::byte> m_dest;
     
     public:
    -    explicit SpanWriter(std::span<unsigned char> data) : m_data{std::as_writable_bytes(data)} {}
    -    explicit SpanWriter(std::span<std::byte> data) : m_data{data} {}
    +    explicit SpanWriter(std::span<unsigned char> dest) : m_dest{std::as_writable_bytes(dest)} {}
    +    explicit SpanWriter(std::span<std::byte> dest) : m_dest{dest} {}
         template <typename... Args>
    -    SpanWriter(std::span<unsigned char> data, Args&&... args) : SpanWriter{data}
    +    SpanWriter(std::span<unsigned char> dest, Args&&... args) : SpanWriter{dest}
         {
             ::SerializeMany(*this, std::forward<Args>(args)...);
         }
     
         void write(std::span<const std::byte> src)
         {
    -        if (src.size() > m_data.size() - m_pos) {
    -            throw std::ios_base::failure("Spanwriter::write(): exceeded buffer size");
    +        if (src.size() > m_dest.size()) {
    +            throw std::ios_base::failure("SpanWriter::write(): exceeded buffer size");
             }
    -        memcpy(m_data.data() + m_pos, src.data(), src.size());
    -        m_pos += src.size();
    +        memcpy(m_dest.data(), src.data(), src.size());
    +        m_dest = m_dest.subspan(src.size());
         }
     
         template<typename T>
    
    

    </details>

    If you prefer the current approach, we should probably add a m_pos <= m_data.size() at the end of write().


    yuvicc commented at 2:08 PM on April 1, 2026:

    Alright, this approach looks fine to me.


    yuvicc commented at 2:54 PM on April 1, 2026:

    Done. Thanks.

  54. stickies-v approved
  55. stickies-v commented at 1:06 PM on April 1, 2026: contributor

    re-ACK bb92d5fa6576e8ff97c2a3afe38622052ec0b540

  56. DrahtBot requested review from theStack on Apr 1, 2026
  57. DrahtBot requested review from alexanderwiederin on Apr 1, 2026
  58. DrahtBot requested review from musaHaruna on Apr 1, 2026
  59. yuvicc force-pushed on Apr 1, 2026
  60. yuvicc commented at 2:56 PM on April 1, 2026: contributor

    Implemented some suggestions by @stickies-v: #34401 (review) #34401 (review)

  61. stickies-v commented at 8:47 PM on April 1, 2026: contributor

    ACK 10aed76c2965cac777cf500c2c015aa24cc7945e

  62. in src/streams.h:139 in 10aed76c29 outdated
     137 | +    template <typename... Args>
     138 | +    SpanWriter(std::span<unsigned char> dest, Args&&... args) : SpanWriter{dest}
     139 | +    {
     140 | +        ::SerializeMany(*this, std::forward<Args>(args)...);
     141 | +    }
     142 | +
    


    w0xlt commented at 11:35 PM on April 1, 2026:

    You expose both std::span<unsigned char> and std::span<std::byte> as valid SpanWriter destinations, but only the unsigned char path currently supports the variadic convenience constructor.

    +    template <typename... Args>
    +    SpanWriter(std::span<std::byte> dest, Args&&... args) : SpanWriter{dest}
    +    {
    +        ::SerializeMany(*this, std::forward<Args>(args)...);
    +    }
    
    

    Test for this:

    --- a/src/test/streams_tests.cpp
    +++ b/src/test/streams_tests.cpp
    @@ -231,6 +231,11 @@ BOOST_AUTO_TEST_CASE(streams_span_writer)
         std::array<std::byte, 2> expected3{std::byte{1}, std::byte{2}};
         BOOST_CHECK_EQUAL_COLLECTIONS(byte_arr.begin(), byte_arr.end(), expected3.begin(), expected3.end());
     
    +    // Use variadic constructor with std::byte span
    +    std::array<std::byte, 2> byte_arr2{};
    +    SpanWriter{std::span{byte_arr2}, a, b};
    +    BOOST_CHECK_EQUAL_COLLECTIONS(byte_arr2.begin(), byte_arr2.end(), expected3.begin(), expected3.end());
    +
         // Writing past the end throws
         std::array<unsigned char, 1> small{};
         BOOST_CHECK_THROW(SpanWriter(small, a, b), std::ios_base::failure);
    

    stickies-v commented at 12:02 AM on April 2, 2026:

    This is intentional, I think additional ctors should be added when they're necessary (i.e. actually used)?


    w0xlt commented at 5:30 PM on April 2, 2026:

    So should this line explicit SpanWriter(std::span<std::byte> dest) : m_dest{dest} {} be removed too ?


    stickies-v commented at 6:37 PM on April 10, 2026:

    Yeah, fair point. Seems wrong to move away from std::bytes so in that case perhaps we should just axe the unsigned char ctors instead?

    <details> <summary>git diff on b494a9fa11</summary>

    diff --git a/src/kernel/bitcoinkernel.cpp b/src/kernel/bitcoinkernel.cpp
    index 5a4bb7d925..b456f41a00 100644
    --- a/src/kernel/bitcoinkernel.cpp
    +++ b/src/kernel/bitcoinkernel.cpp
    @@ -1393,7 +1393,7 @@ uint32_t btck_block_header_get_nonce(const btck_BlockHeader* header)
     int btck_block_header_to_bytes(const btck_BlockHeader* header, unsigned char output[80])
     {
         try {
    -        SpanWriter{std::span{output, 80}} << btck_BlockHeader::get(header);
    +        SpanWriter{std::as_writable_bytes(std::span{output, 80})} << btck_BlockHeader::get(header);
             return 0;
         } catch (...) {
             return -1;
    diff --git a/src/streams.h b/src/streams.h
    index b57c443287..12a2cac2c7 100644
    --- a/src/streams.h
    +++ b/src/streams.h
    @@ -129,10 +129,9 @@ private:
         std::span<std::byte> m_dest;
     
     public:
    -    explicit SpanWriter(std::span<unsigned char> dest) : m_dest{std::as_writable_bytes(dest)} {}
         explicit SpanWriter(std::span<std::byte> dest) : m_dest{dest} {}
         template <typename... Args>
    -    SpanWriter(std::span<unsigned char> dest, Args&&... args) : SpanWriter{dest}
    +    SpanWriter(std::span<std::byte> dest, Args&&... args) : SpanWriter{dest}
         {
             ::SerializeMany(*this, std::forward<Args>(args)...);
         }
    diff --git a/src/test/streams_tests.cpp b/src/test/streams_tests.cpp
    index c5ee057d60..d881ae8d35 100644
    --- a/src/test/streams_tests.cpp
    +++ b/src/test/streams_tests.cpp
    @@ -212,28 +212,20 @@ BOOST_AUTO_TEST_CASE(streams_span_writer)
         unsigned char a(1);
         unsigned char b(2);
         unsigned char bytes[] = {3, 4, 5, 6};
    -    std::array<unsigned char, 8> arr{};
    +    std::array<std::byte, 8> arr{};
     
         // Test operator<<
         SpanWriter writer{arr};
         writer << a << b;
    -    std::array<unsigned char, 8> expected1{1, 2, 0, 0, 0, 0, 0, 0};
    -    BOOST_CHECK_EQUAL_COLLECTIONS(arr.begin(), arr.end(), expected1.begin(), expected1.end());
    +    BOOST_CHECK_EQUAL(HexStr(arr), "0102000000000000");
     
         // Use variadic constructor and write to subspan.
         SpanWriter{std::span{arr}.subspan(2), a, bytes, b};
    -    std::array<unsigned char, 8> expected2{1, 2, 1, 3, 4, 5, 6, 2};
    -    BOOST_CHECK_EQUAL_COLLECTIONS(arr.begin(), arr.end(), expected2.begin(), expected2.end());
    -
    -    // Test std::byte span constructor
    -    std::array<std::byte, 2> byte_arr{};
    -    SpanWriter{std::span{byte_arr}} << a << b;
    -    std::array<std::byte, 2> expected3{std::byte{1}, std::byte{2}};
    -    BOOST_CHECK_EQUAL_COLLECTIONS(byte_arr.begin(), byte_arr.end(), expected3.begin(), expected3.end());
    +    BOOST_CHECK_EQUAL(HexStr(arr), "0102010304050602");
     
         // Writing past the end throws
    -    std::array<unsigned char, 1> small{};
    -    BOOST_CHECK_THROW(SpanWriter(small, a, b), std::ios_base::failure);
    +    std::array<std::byte, 1> small{};
    +    BOOST_CHECK_THROW(SpanWriter(std::span{small}, a, b), std::ios_base::failure);
     }
     
     BOOST_AUTO_TEST_CASE(streams_vector_reader)
    
    

    </details>

    (using HexStr in the tests to minimize verbosity)

  63. DrahtBot requested review from w0xlt on Apr 1, 2026
  64. in src/kernel/bitcoinkernel_wrapper.h:753 in 10aed76c29
     745 | @@ -756,6 +746,16 @@ class BlockHeaderApi
     746 |      {
     747 |          return btck_block_header_get_nonce(impl());
     748 |      }
     749 | +
     750 | +    std::array<std::byte, 80> ToBytes() const
     751 | +    {
     752 | +        std::array<std::byte, 80> header;
     753 | +        auto res = btck_block_header_to_bytes(impl(), reinterpret_cast<unsigned char*>(header.data()));
    


    w0xlt commented at 11:35 PM on April 1, 2026:

    nit: As suggested here #34401 (review)

            int res = btck_block_header_to_bytes(impl(), reinterpret_cast<unsigned char*>(header.data()));
    

    stickies-v commented at 12:03 AM on April 2, 2026:

    I agree, but when specifying int we should use brace init to prevent narrowing conversion.


    yuvicc commented at 7:59 PM on April 6, 2026:

    Done in 42aaca85de7526d212bb6e54cdc1dc8867716ab7

  65. DrahtBot requested review from w0xlt on Apr 1, 2026
  66. yuvicc force-pushed on Apr 6, 2026
  67. yuvicc commented at 8:01 PM on April 6, 2026: contributor

    Fixed a nit

  68. in src/test/streams_tests.cpp:236 in fdf49462af
     231 | +    std::array<std::byte, 2> expected3{std::byte{1}, std::byte{2}};
     232 | +    BOOST_CHECK_EQUAL_COLLECTIONS(byte_arr.begin(), byte_arr.end(), expected3.begin(), expected3.end());
     233 | +
     234 | +    // Writing past the end throws
     235 | +    std::array<unsigned char, 1> small{};
     236 | +    BOOST_CHECK_THROW(SpanWriter(small, a, b), std::ios_base::failure);
    


    alexanderwiederin commented at 9:56 AM on April 10, 2026:

    We could also add the following to test the << operator.

    BOOST_CHECK_THROW(SpanWriter(small) << a << b, std::ios_base::failure);
    

    yuvicc commented at 1:58 PM on April 14, 2026:

    Done. Thanks.

  69. alexanderwiederin commented at 10:10 AM on April 10, 2026: contributor

    SpanWriter is appropriate here to avoid unnecessary heap allocation and ensure exception safety across the C boundary. The method is consistent with the rest of the API.

    Non-blocking: worth clarifying whether explicit SpanWriter(std::span<std::byte> dest) : m_dest{dest} {} should be removed.

    ACK https://github.com/bitcoin/bitcoin/pull/34401/changes/b494a9fa11edc37fdef3dde8cb1a0dc761bcebae

  70. DrahtBot requested review from stickies-v on Apr 10, 2026
  71. Add `SpanWriter` class for zero-allocation stream writing
    Co-authored-by: stickies-v <stickies-v@protonmail.com>
    86662623ec
  72. kernel: Add Block Header serialization method
    Add `btck_block_header_to_bytes` serialization method to serialize a
    `btck_BlockHeader` into an 80-byte buffer using `SpanWriter` to ensure
    zero-allocation serialization.
    1ad551281a
  73. test: Add check for return type in `HasToBytes` concept
    Add return type check for `ToBytes()` which should be convertible to
    `std::span<const std::byte>`.
    577a3e74c8
  74. yuvicc force-pushed on Apr 14, 2026
  75. stickies-v commented at 4:37 PM on April 14, 2026: contributor

    re-ACK 577a3e74c82d942cba24c2b3affda9c777f3c75d

  76. DrahtBot requested review from alexanderwiederin on Apr 14, 2026
  77. theStack approved
  78. theStack commented at 5:23 PM on April 14, 2026: contributor

    Code-review ACK 577a3e74c82d942cba24c2b3affda9c777f3c75d

  79. w0xlt commented at 2:35 PM on April 15, 2026: contributor

    ACK 577a3e74c82d942cba24c2b3affda9c777f3c75d

  80. fanquake merged this on Apr 15, 2026
  81. fanquake closed this on Apr 15, 2026

  82. ?
    project_v2_item_status_changed github-project-automation[bot]
  83. yuvicc deleted the branch on Apr 16, 2026

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: 2026-04-30 21:12 UTC

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