From 18f04f6c73c8159506bb5f3aeb36b4a47004b146 Mon Sep 17 00:00:00 2001 From: echennells Date: Wed, 19 Aug 2026 04:40:08 +0000 Subject: [PATCH 1/3] Derive the block.headers cp_height proof from the last returned header. --- .../electrum/protocol_electrum_headers.cpp | 8 ++- test/protocols/electrum/electrum_headers.cpp | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum_headers.cpp b/src/protocols/electrum/protocol_electrum_headers.cpp index 5c622454..d81ce3d7 100644 --- a/src/protocols/electrum/protocol_electrum_headers.cpp +++ b/src/protocols/electrum/protocol_electrum_headers.cpp @@ -312,15 +312,19 @@ void protocol_electrum::blockchain_block_headers(size_t starting, } } - if (prove) + if (prove && !is_zero(links.size())) { // A very slim chance of inconsistency given an intervening reorg // because of get_merkle_root_and_proof() and height-based calcs. // This is acceptable as must be verified by caller in any case. hashes proof{}; hash_digest root{}; + + // The branch proves the last returned header, which is not the + // last requested header when the request exceeds maximum_headers. + const auto proof_height = starting + sub1(links.size()); if (const auto code = query.get_merkle_root_and_proof(root, proof, - target, waypoint)) + proof_height, waypoint)) { using namespace error::electrum; send_code(translate(code, daemon_error)); diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp index 6456b584..490f3536 100644 --- a/test/protocols/electrum/electrum_headers.cpp +++ b/test/protocols/electrum/electrum_headers.cpp @@ -541,6 +541,55 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_offset__expected) BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__request_over_maximum__proves_last_returned) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + using namespace test; + const auto expected_root = encode_hash(merkle_root( + { + block0_hash, + block1_hash, + block2_hash, + block3_hash, + block4_hash, + block5_hash, + block6_hash, + block7_hash, + block8_hash + })); + + const string_list expected_branch + { + encode_hash(block5_hash), + encode_hash(root67), + encode_hash(root03), + encode_hash(root88) + }; + + const auto response = get(R"({"id":73,"method":"blockchain.block.headers","params":[0,6,8]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 5u); + BOOST_REQUIRE_EQUAL(result.at("root").as_string(), expected_root); + + const auto& branch = result.at("branch").as_array(); + BOOST_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_above_top__not_found) { BOOST_REQUIRE(handshake(electrum::version::v1_6)); From 7e33e129156bde1fb4f03282b62e2738f9c2c68a Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 20 Sep 2026 10:04:20 -0400 Subject: [PATCH 2/3] Remove dead size values, extend and rename block.headers proof tests. --- .../electrum/protocol_electrum_headers.cpp | 7 +- test/protocols/electrum/electrum_headers.cpp | 71 ++++++++++++++++++- 2 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum_headers.cpp b/src/protocols/electrum/protocol_electrum_headers.cpp index d81ce3d7..a1b7e917 100644 --- a/src/protocols/electrum/protocol_electrum_headers.cpp +++ b/src/protocols/electrum/protocol_electrum_headers.cpp @@ -232,7 +232,6 @@ void protocol_electrum::blockchain_block_headers(size_t starting, // No headers may be returned, which implies start > confirmed top block. const auto count = limit(quantity, maximum_headers); const auto links = query.get_confirmed_headers(starting, count); - auto size = two * chain::header::serialized_size() * links.size(); if (single && !is_one(links.size())) { @@ -288,6 +287,8 @@ void protocol_electrum::blockchain_block_headers(size_t starting, else { // Stream headers into single buffer. + const auto size = two * chain::header::serialized_size() * + links.size(); std::string headers(size, '\0'); stream::out::fast sink{ headers }; write::base16::fast writer{ sink }; @@ -337,7 +338,6 @@ void protocol_electrum::blockchain_block_headers(size_t starting, result["branch"] = std::move(branch); result["root"] = encode_hash(root); - size += two * hash_size * add1(proof.size()); } value = std::move(result); @@ -383,7 +383,6 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, return; } - size_t size{}; boost::json::value value{}; if (raw) { @@ -394,7 +393,6 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, return; } - size = two * chain::header::serialized_size(); value = { { "height", top }, @@ -418,7 +416,6 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec, return; } - size = 256; auto& object = value.as_object(); object["block_height"] = top; } diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp index 490f3536..4de0c9ae 100644 --- a/test/protocols/electrum/electrum_headers.cpp +++ b/test/protocols/electrum/electrum_headers.cpp @@ -541,7 +541,7 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_offset__expected) BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } -BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__request_over_maximum__proves_last_returned) +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_count_exceeds_max__proves_last_returned) { BOOST_REQUIRE(handshake(electrum::version::v1_6)); @@ -590,6 +590,75 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__request_over_maximum__p BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_count_exceeds_max_v1_4__proves_last_returned) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_4)); + + using namespace test; + const auto expected_root = encode_hash(merkle_root( + { + block0_hash, + block1_hash, + block2_hash, + block3_hash, + block4_hash, + block5_hash, + block6_hash, + block7_hash, + block8_hash + })); + + const string_list expected_branch + { + encode_hash(block5_hash), + encode_hash(root67), + encode_hash(root03), + encode_hash(root88) + }; + + const auto response = get(R"({"id":74,"method":"blockchain.block.headers","params":[0,6,8]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("hex").is_string()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("root").as_string(), expected_root); + + // "hex" prior to v1.6 + const auto expected = encode_base16(header0_data) + encode_base16(header1_data) + encode_base16(header2_data) + encode_base16(header3_data) + encode_base16(header4_data); + BOOST_REQUIRE_EQUAL(result.at("hex").as_string(), expected); + + const auto& branch = result.at("branch").as_array(); + BOOST_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_zero_checkpoint__no_proof) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + const auto response = get(R"({"id":75,"method":"blockchain.block.headers","params":[5,0,8]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 0); + BOOST_REQUIRE(result.at("headers").as_array().empty()); + BOOST_REQUIRE(!result.contains("root")); + BOOST_REQUIRE(!result.contains("branch")); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_above_top__not_found) { BOOST_REQUIRE(handshake(electrum::version::v1_6)); From 2e57c676e1fc102fde015da315daf49395aa8bd7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sun, 20 Sep 2026 10:05:31 -0400 Subject: [PATCH 3/3] Apply the cp_height requirement to the returned header count. --- .../electrum/protocol_electrum_headers.cpp | 25 +++-- test/protocols/electrum/electrum_headers.cpp | 98 +++++++++++++++++++ 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/src/protocols/electrum/protocol_electrum_headers.cpp b/src/protocols/electrum/protocol_electrum_headers.cpp index a1b7e917..64accb0a 100644 --- a/src/protocols/electrum/protocol_electrum_headers.cpp +++ b/src/protocols/electrum/protocol_electrum_headers.cpp @@ -201,13 +201,10 @@ void protocol_electrum::blockchain_block_headers(size_t starting, size_t quantity, size_t waypoint, bool single) NOEXCEPT { const auto prove = !is_zero(quantity) && !is_zero(waypoint); - const auto target = starting + sub1(quantity); const auto& query = archive(); const auto top = query.get_top_confirmed(); using namespace system; - // The documented requirement: `start_height + (count - 1) <= cp_height` is - // ambiguous at count = 0 so guard must be applied to both args and prover. if (is_add_overflow(starting, quantity)) { send_code(error::electrum::bad_request); @@ -218,11 +215,6 @@ void protocol_electrum::blockchain_block_headers(size_t starting, send_code(error::electrum::bad_request); return; } - else if (prove && target > waypoint) - { - send_code(error::electrum::bad_request); - return; - } // Recommended to be at least one difficulty retarget period, e.g. 2016. // The maximum number of headers the server will return in single request. @@ -239,6 +231,17 @@ void protocol_electrum::blockchain_block_headers(size_t starting, return; } + // The proof is over the last returned header, which is the last requested + // header only when the returned count is not reduced. + const auto proving = prove && !is_zero(links.size()); + const auto proof_height = proving ? starting + sub1(links.size()) : zero; + + if (proving && proof_height > waypoint) + { + send_code(error::electrum::bad_request); + return; + } + value_t value{}; if (single && !prove) @@ -313,17 +316,13 @@ void protocol_electrum::blockchain_block_headers(size_t starting, } } - if (prove && !is_zero(links.size())) + if (proving) { // A very slim chance of inconsistency given an intervening reorg // because of get_merkle_root_and_proof() and height-based calcs. // This is acceptable as must be verified by caller in any case. hashes proof{}; hash_digest root{}; - - // The branch proves the last returned header, which is not the - // last requested header when the request exceeds maximum_headers. - const auto proof_height = starting + sub1(links.size()); if (const auto code = query.get_merkle_root_and_proof(root, proof, proof_height, waypoint)) { diff --git a/test/protocols/electrum/electrum_headers.cpp b/test/protocols/electrum/electrum_headers.cpp index 4de0c9ae..c4f2d38b 100644 --- a/test/protocols/electrum/electrum_headers.cpp +++ b/test/protocols/electrum/electrum_headers.cpp @@ -642,6 +642,104 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_count_exceeds_max BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); } +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_requested_exceeds_checkpoint__proves_last_returned) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + using namespace test; + const auto expected_root = encode_hash(merkle_root( + { + block0_hash, + block1_hash, + block2_hash, + block3_hash, + block4_hash, + block5_hash, + block6_hash, + block7_hash, + block8_hash + })); + + const string_list expected_branch + { + encode_hash(block5_hash), + encode_hash(root67), + encode_hash(root03), + encode_hash(root88) + }; + + const auto response = get(R"({"id":76,"method":"blockchain.block.headers","params":[0,20,8]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("max").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + BOOST_REQUIRE_EQUAL(result.at("max").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 5); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 5u); + BOOST_REQUIRE_EQUAL(result.at("root").as_string(), expected_root); + + const auto& branch = result.at("branch").as_array(); + BOOST_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_count_exceeds_top__proves_last_returned) +{ + BOOST_REQUIRE(handshake(electrum::version::v1_6)); + + using namespace test; + const auto expected_root = encode_hash(merkle_root( + { + block0_hash, + block1_hash, + block2_hash, + block3_hash, + block4_hash, + block5_hash, + block6_hash, + block7_hash, + block8_hash, + block9_hash + })); + + const auto root89 = sha256::double_hash(block8_hash, block9_hash); + const string_list expected_branch + { + encode_hash(block8_hash), + encode_hash(root89), + encode_hash(sha256::double_hash(root89, root89)), + encode_hash(root07) + }; + + const auto response = get(R"({"id":77,"method":"blockchain.block.headers","params":[6,5,9]})" "\n"); + BOOST_REQUIRE_MESSAGE(response.is_object() && response.as_object().contains("result"), serialize(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + REQUIRE_NO_THROW_TRUE(result.at("count").is_int64()); + REQUIRE_NO_THROW_TRUE(result.at("headers").is_array()); + REQUIRE_NO_THROW_TRUE(result.at("root").is_string()); + BOOST_REQUIRE_EQUAL(result.at("count").as_int64(), 4); + BOOST_REQUIRE_EQUAL(result.at("headers").as_array().size(), 4u); + BOOST_REQUIRE_EQUAL(result.at("root").as_string(), expected_root); + + const auto& branch = result.at("branch").as_array(); + BOOST_REQUIRE(branch.at(0).is_string()); + BOOST_REQUIRE_EQUAL(branch.size(), expected_branch.size()); + BOOST_REQUIRE_EQUAL(branch.at(0).as_string(), expected_branch[0]); + BOOST_REQUIRE_EQUAL(branch.at(1).as_string(), expected_branch[1]); + BOOST_REQUIRE_EQUAL(branch.at(2).as_string(), expected_branch[2]); + BOOST_REQUIRE_EQUAL(branch.at(3).as_string(), expected_branch[3]); +} + BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_zero_checkpoint__no_proof) { BOOST_REQUIRE(handshake(electrum::version::v1_6));