Skip to content

Commit 1499c47

Browse files
committed
Add headers_subscribe tests, fix test setup.
1 parent dfcf568 commit 1499c47

9 files changed

Lines changed: 341 additions & 23 deletions

File tree

Makefile.am

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ test_libbitcoin_server_test_SOURCES = \
8282
test/protocols/electrum/electrum.cpp \
8383
test/protocols/electrum/electrum.hpp \
8484
test/protocols/electrum/electrum_block_header.cpp \
85+
test/protocols/electrum/electrum_block_headers.cpp \
86+
test/protocols/electrum/electrum_headers_subscribe.cpp \
8587
test/protocols/electrum/electrum_server.cpp \
8688
test/protocols/electrum/electrum_server_version.cpp
8789

builds/cmake/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,8 @@ if (with-tests)
311311
"../../test/protocols/electrum/electrum.cpp"
312312
"../../test/protocols/electrum/electrum.hpp"
313313
"../../test/protocols/electrum/electrum_block_header.cpp"
314+
"../../test/protocols/electrum/electrum_block_headers.cpp"
315+
"../../test/protocols/electrum/electrum_headers_subscribe.cpp"
314316
"../../test/protocols/electrum/electrum_server.cpp"
315317
"../../test/protocols/electrum/electrum_server_version.cpp" )
316318

builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@
134134
<ObjectFileName>$(IntDir)test_protocols_electrum_electrum.obj</ObjectFileName>
135135
</ClCompile>
136136
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_block_header.cpp" />
137+
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_block_headers.cpp" />
138+
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_headers_subscribe.cpp" />
137139
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_server.cpp" />
138140
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_server_version.cpp" />
139141
<ClCompile Include="..\..\..\..\test\settings.cpp" />

builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_block_header.cpp">
6161
<Filter>src\protocols\electrum</Filter>
6262
</ClCompile>
63+
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_block_headers.cpp">
64+
<Filter>src\protocols\electrum</Filter>
65+
</ClCompile>
66+
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_headers_subscribe.cpp">
67+
<Filter>src\protocols\electrum</Filter>
68+
</ClCompile>
6369
<ClCompile Include="..\..\..\..\test\protocols\electrum\electrum_server.cpp">
6470
<Filter>src\protocols\electrum</Filter>
6571
</ClCompile>

include/bitcoin/server/protocols/protocol_electrum.hpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@ class BCS_API protocol_electrum
128128
void blockchain_block_headers(size_t starting, size_t quantity,
129129
size_t waypoint, bool multiplicity) NOEXCEPT;
130130

131-
inline bool is_version(electrum::version version) const NOEXCEPT
131+
/// Notify client of new header.
132+
void do_header(node::header_t link) NOEXCEPT;
133+
134+
inline bool is_version(server::electrum::version version) const NOEXCEPT
132135
{
133136
return channel_->version() >= version;
134137
}
@@ -139,8 +142,9 @@ class BCS_API protocol_electrum
139142
}
140143

141144
private:
142-
// This is thread safe.
145+
// These are thread safe.
143146
const options_t& options_;
147+
std::atomic_bool subscribed_{};
144148

145149
// This is mostly thread safe, and used in a thread safe manner.
146150
const channel_t::ptr channel_;

src/protocols/protocol_electrum.cpp

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -94,16 +94,22 @@ void protocol_electrum::stopping(const code& ec) NOEXCEPT
9494
// ----------------------------------------------------------------------------
9595

9696
bool protocol_electrum::handle_event(const code&, node::chase event_,
97-
node::event_value) NOEXCEPT
97+
node::event_value value) NOEXCEPT
9898
{
9999
// Do not pass ec to stopped as it is not a call status.
100100
if (stopped())
101101
return false;
102102

103103
switch (event_)
104104
{
105-
case node::chase::suspend:
105+
case node::chase::organized:
106106
{
107+
if (subscribed_.load(std::memory_order_relaxed))
108+
{
109+
BC_ASSERT(std::holds_alternative<node::header_t>(value));
110+
POST(do_header, std::get<node::header_t>(value));
111+
}
112+
107113
break;
108114
}
109115
default:
@@ -297,19 +303,39 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec,
297303
return;
298304
}
299305

300-
// TODO: signal header subscription.
306+
subscribed_.store(true, std::memory_order_relaxed);
307+
send_result(
308+
{
309+
object_t
310+
{
311+
{ "height", uint64_t{ top } },
312+
{ "hex", to_hex(*header, chain::header::serialized_size()) }
313+
}
314+
}, 256, BIND(complete, _1));
315+
}
301316

302-
// TODO: idempotent subscribe to chase::organized via session/chaser/node.
303-
// TODO: upon notification send just the header notified by the link.
304-
// TODO: it is client responsibility to deal with reorgs and race gaps.
305-
send_result(value_t
317+
void protocol_electrum::do_header(node::header_t link) NOEXCEPT
318+
{
319+
BC_ASSERT(stranded());
320+
321+
const auto& query = archive();
322+
const auto height = query.get_height(link);
323+
const auto header = query.get_header(link);
324+
325+
if (height.is_terminal() || !header)
326+
{
327+
LOGF("Electrum::do_header, object not found (" << link << ").");
328+
return;
329+
}
330+
331+
send_notification("blockchain.headers.subscribe",
332+
{
333+
object_t
306334
{
307-
object_t
308-
{
309-
{ "height", uint64_t{ top } },
310-
{ "hex", to_hex(*header, chain::header::serialized_size()) }
311-
}
312-
}, 256, BIND(complete, _1));
335+
{ "height", height.value },
336+
{ "hex", to_hex(*header, chain::header::serialized_size()) }
337+
}
338+
}, 100, BIND(complete, _1));
313339
}
314340

315341
void protocol_electrum::handle_blockchain_estimate_fee(const code& ec,
@@ -477,14 +503,14 @@ void protocol_electrum::handle_mempool_get_fee_histogram(const code& ec,
477503

478504
// TODO: requires tx pool metadata graph.
479505
send_result(value_t
506+
{
507+
array_t
480508
{
481-
array_t
482-
{
483-
array_t{ 1, 1024 },
484-
array_t{ 2, 2048 },
485-
array_t{ 4, 4096 }
486-
}
487-
}, 256, BIND(complete, _1));
509+
array_t{ 1, 1024 },
510+
array_t{ 2, 2048 },
511+
array_t{ 4, 4096 }
512+
}
513+
}, 256, BIND(complete, _1));
488514
}
489515

490516
BC_POP_WARNING()

test/protocols/blocks.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ const chain::block block5{ block5_data, true };
7575
const chain::block block6{ block6_data, true };
7676
const chain::block block7{ block7_data, true };
7777
const chain::block block8{ block8_data, true };
78-
const chain::block block9{ block8_data, true };
78+
const chain::block block9{ block9_data, true };
7979

8080
const server::settings::embedded_pages admin{};
8181
const server::settings::embedded_pages native{};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS)
3+
*
4+
* This file is part of libbitcoin.
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
#include "../../test.hpp"
20+
#include "electrum.hpp"
21+
22+
BOOST_FIXTURE_TEST_SUITE(electrum_tests, electrum_setup_fixture)
23+
24+
// blockchain.block.headers
25+
26+
using namespace system;
27+
static const code not_found{ server::error::not_found };
28+
static const code target_overflow{ server::error::target_overflow };
29+
static const code invalid_argument{ server::error::invalid_argument };
30+
31+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__genesis_count1_no_checkpoint__expected)
32+
{
33+
BOOST_CHECK(handshake());
34+
35+
const auto response = get(R"({"id":60,"method":"blockchain.block.headers","params":[0,1]})" "\n");
36+
const auto& result = response.at("result").as_object();
37+
BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
38+
BOOST_CHECK_EQUAL(result.at("count").as_int64(), 1);
39+
BOOST_CHECK(result.at("headers").is_array());
40+
BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 1u);
41+
BOOST_CHECK_EQUAL(result.at("headers").as_array().at(0).as_string(), encode_base16(header0_data));
42+
}
43+
44+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__block1to3_no_checkpoint__expected)
45+
{
46+
BOOST_CHECK(handshake());
47+
48+
const auto response = get(R"({"id":61,"method":"blockchain.block.headers","params":[1,3]})" "\n");
49+
const auto& result = response.at("result").as_object();
50+
BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
51+
BOOST_CHECK_EQUAL(result.at("count").as_int64(), 3);
52+
BOOST_CHECK(result.at("headers").is_array());
53+
54+
const auto& headers = result.at("headers").as_array();
55+
BOOST_CHECK_EQUAL(headers.size(), 3u);
56+
BOOST_CHECK_EQUAL(headers.at(0).as_string(), encode_base16(header1_data));
57+
BOOST_CHECK_EQUAL(headers.at(1).as_string(), encode_base16(header2_data));
58+
BOOST_CHECK_EQUAL(headers.at(2).as_string(), encode_base16(header3_data));
59+
}
60+
61+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_exceeds_max__capped)
62+
{
63+
BOOST_CHECK(handshake());
64+
65+
const auto response = get(R"({"id":62,"method":"blockchain.block.headers","params":[0,10]})" "\n");
66+
const auto& result = response.at("result").as_object();
67+
BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
68+
BOOST_CHECK_EQUAL(result.at("count").as_int64(), 5);
69+
BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 5u);
70+
}
71+
72+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__count_zero__empty_headers)
73+
{
74+
BOOST_CHECK(handshake());
75+
76+
const auto response = get(R"({"id":63,"method":"blockchain.block.headers","params":[5,0]})" "\n");
77+
const auto& result = response.at("result").as_object();
78+
BOOST_CHECK_EQUAL(result.at("count").as_int64(), 0);
79+
BOOST_CHECK(result.at("headers").as_array().empty());
80+
}
81+
82+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_no_offset__expected)
83+
{
84+
BOOST_CHECK(handshake());
85+
86+
const auto expected_root = encode_hash(merkle_root(
87+
{
88+
block0_hash,
89+
block1_hash,
90+
block2_hash,
91+
block3_hash,
92+
block4_hash,
93+
block5_hash,
94+
block6_hash,
95+
block7_hash,
96+
block8_hash
97+
}));
98+
99+
const string_list expected_branch
100+
{
101+
encode_hash(block4_hash),
102+
encode_hash(root67),
103+
encode_hash(root03),
104+
encode_hash(root88)
105+
};
106+
107+
const auto response = get(R"({"id":64,"method":"blockchain.block.headers","params":[5,1,8]})" "\n");
108+
const auto& result = response.at("result").as_object();
109+
BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
110+
BOOST_CHECK_EQUAL(result.at("count").as_int64(), 1);
111+
BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 1u);
112+
BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
113+
114+
const auto& branch = result.at("branch").as_array();
115+
BOOST_CHECK_EQUAL(branch.size(), expected_branch.size());
116+
BOOST_CHECK_EQUAL(branch.at(0).as_string(), expected_branch[0]);
117+
BOOST_CHECK_EQUAL(branch.at(1).as_string(), expected_branch[1]);
118+
BOOST_CHECK_EQUAL(branch.at(2).as_string(), expected_branch[2]);
119+
BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]);
120+
}
121+
122+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__proof_offset__expected)
123+
{
124+
BOOST_CHECK(handshake());
125+
126+
const auto expected_root = encode_hash(merkle_root(
127+
{
128+
block0_hash,
129+
block1_hash,
130+
block2_hash,
131+
block3_hash,
132+
block4_hash,
133+
block5_hash,
134+
block6_hash,
135+
block7_hash,
136+
block8_hash
137+
}));
138+
139+
const string_list expected_branch
140+
{
141+
encode_hash(block6_hash),
142+
encode_hash(root45),
143+
encode_hash(root03),
144+
encode_hash(root88)
145+
};
146+
147+
const auto response = get(R"({"id":64,"method":"blockchain.block.headers","params":[5,3,8]})" "\n");
148+
const auto& result = response.at("result").as_object();
149+
BOOST_CHECK_EQUAL(result.at("max").as_int64(), 5);
150+
BOOST_CHECK_EQUAL(result.at("count").as_int64(), 3);
151+
BOOST_CHECK_EQUAL(result.at("headers").as_array().size(), 3u);
152+
BOOST_CHECK_EQUAL(result.at("root").as_string(), expected_root);
153+
154+
const auto& branch = result.at("branch").as_array();
155+
BOOST_CHECK_EQUAL(branch.size(), expected_branch.size());
156+
BOOST_CHECK_EQUAL(branch.at(0).as_string(), expected_branch[0]);
157+
BOOST_CHECK_EQUAL(branch.at(1).as_string(), expected_branch[1]);
158+
BOOST_CHECK_EQUAL(branch.at(2).as_string(), expected_branch[2]);
159+
BOOST_CHECK_EQUAL(branch.at(3).as_string(), expected_branch[3]);
160+
}
161+
162+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_above_top__not_found)
163+
{
164+
BOOST_CHECK(handshake());
165+
166+
const auto response = get(R"({"id":65,"method":"blockchain.block.headers","params":[10,1]})" "\n");
167+
BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
168+
}
169+
170+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__target_exceeds_waypoint__target_overflow)
171+
{
172+
BOOST_CHECK(handshake());
173+
174+
const auto response = get(R"({"id":66,"method":"blockchain.block.headers","params":[2,3,1]})" "\n");
175+
BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), target_overflow.value());
176+
}
177+
178+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__waypoint_above_top__not_found)
179+
{
180+
BOOST_CHECK(handshake());
181+
182+
const auto response = get(R"({"id":67,"method":"blockchain.block.headers","params":[0,1,10]})" "\n");
183+
BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
184+
}
185+
186+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__negative_start__invalid_argument)
187+
{
188+
BOOST_CHECK(handshake());
189+
190+
const auto response = get(R"({"id":68,"method":"blockchain.block.headers","params":[-1,1]})" "\n");
191+
BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
192+
}
193+
194+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__fractional_count__invalid_argument)
195+
{
196+
BOOST_CHECK(handshake());
197+
198+
const auto response = get(R"({"id":69,"method":"blockchain.block.headers","params":[0,1.5]})" "\n");
199+
BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), invalid_argument.value());
200+
}
201+
202+
BOOST_AUTO_TEST_CASE(electrum__blockchain_block_headers__start_plus_count_huge__not_found)
203+
{
204+
BOOST_CHECK(handshake());
205+
206+
// argument_overflow is not actually reachable via json due to its integer limits.
207+
const auto response = get(R"({"id":70,"method":"blockchain.block.headers","params":[9007199254740991,2]})" "\n");
208+
BOOST_CHECK_EQUAL(response.at("error").as_object().at("code").as_int64(), not_found.value());
209+
}
210+
211+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)