Skip to content

Commit 4c7685d

Browse files
authored
Merge pull request #625 from evoskuil/master
Implement electrum_protocol handle_blockchain_block_headers.
2 parents 4f905fb + b8d0d0f commit 4c7685d

7 files changed

Lines changed: 206 additions & 22 deletions

File tree

include/bitcoin/server/error.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@ enum error_t : uint8_t
5959

6060
/// server (rpc response codes)
6161
not_found,
62+
not_implemented,
6263
invalid_argument,
63-
not_implemented
64+
argument_overflow,
65+
target_overflow,
66+
server_error
6467
};
6568

6669
// No current need for error_code equivalence mapping.

include/bitcoin/server/settings.hpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ class BCS_API settings
8484
virtual bool enabled() const NOEXCEPT;
8585
};
8686

87+
struct electrum_server
88+
: public network::settings::tls_server
89+
{
90+
using base = network::settings::tls_server;
91+
using base::base;
92+
93+
// Maximum number of headers the server will return in single request.
94+
// Recommended to be multiple of difficulty retarget period, e.g. 2016.
95+
uint32_t maximum_headers{ 10 * 2016 };
96+
};
97+
8798
/// html (http/s) document server settings (has directory/default).
8899
/// This is for web servers that expose a local file system directory.
89100
struct html_server
@@ -128,7 +139,7 @@ class BCS_API settings
128139
network::settings::http_server bitcoind{ "bitcoind" };
129140

130141
/// electrum compat interface (tcp/s, json-rpc-v2)
131-
network::settings::tls_server electrum{ "electrum" };
142+
electrum_server electrum{ "electrum" };
132143

133144
/// stratum v1 compat interface (tcp/s, json-rpc-v1, auth handshake)
134145
network::settings::tls_server stratum_v1{ "stratum_v1" };

src/error.cpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ DEFINE_ERROR_T_MESSAGE_MAP(error)
4949

5050
// server (rpc response codes)
5151
{ not_found, "not_found" },
52+
{ not_implemented, "not_implemented" },
5253
{ invalid_argument, "invalid_argument" },
53-
{ not_implemented, "not_implemented" }
54+
{ argument_overflow, "argument_overflow" },
55+
{ target_overflow, "target_overflow" },
56+
{ server_error, "server_error" }
5457
};
5558

5659
DEFINE_ERROR_T_CATEGORY(error, "server", "server code")

src/parser.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,6 +1129,11 @@ options_metadata parser::load_settings() THROWS
11291129
value<uint32_t>(&configured.server.electrum.maximum_request),
11301130
"The maximum allowed request size, defaults to '4000000'."
11311131
)
1132+
(
1133+
"electrum.maximum_headers",
1134+
value<uint32_t>(&configured.server.electrum.maximum_headers),
1135+
"The maximum allowed header request cound, defaults to '20160'."
1136+
)
11321137

11331138
/* [stratum_v1] */
11341139
(

src/protocols/protocol_electrum.cpp

Lines changed: 148 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <bitcoin/server/protocols/protocol_electrum.hpp>
2020

2121
#include <algorithm>
22+
#include <ranges>
2223
#include <variant>
2324
#include <bitcoin/server/define.hpp>
2425
#include <bitcoin/server/interfaces/interfaces.hpp>
@@ -114,6 +115,43 @@ bool protocol_electrum::handle_event(const code&, node::chase event_,
114115
return true;
115116
}
116117

118+
// Utility.
119+
// ----------------------------------------------------------------------------
120+
121+
// TODO: move to system/math.
122+
template <typename Integer, if_integer<Integer> = true>
123+
bool to_integer(Integer& out, double value) NOEXCEPT
124+
{
125+
if (!std::isfinite(value))
126+
return false;
127+
128+
double integral{};
129+
const double fractional = std::modf(value, &integral);
130+
if (fractional != 0.0)
131+
return false;
132+
133+
if (integral > static_cast<double>(system::maximum<Integer>) ||
134+
integral < static_cast<double>(system::minimum<Integer>))
135+
return false;
136+
137+
BC_PUSH_WARNING(NO_STATIC_CAST)
138+
out = static_cast<Integer>(integral);
139+
BC_POP_WARNING()
140+
return true;
141+
}
142+
143+
// TODO: centralize in server (also used in bitcoind and native interfaces).
144+
template <typename Object, typename ...Args>
145+
std::string to_hex(const Object& object, size_t size, Args&&... args) NOEXCEPT
146+
{
147+
std::string out(two * size, '\0');
148+
stream::out::fast sink{ out };
149+
write::base16::fast writer{ sink };
150+
object.to_data(writer, std::forward<Args>(args)...);
151+
BC_ASSERT(writer);
152+
return out;
153+
}
154+
117155
// Handlers (blockchain).
118156
// ----------------------------------------------------------------------------
119157

@@ -127,11 +165,104 @@ void protocol_electrum::handle_blockchain_block_header(const code& ec,
127165

128166
// electrum-protocol.readthedocs.io/en/latest/protocol-basics.html#block-headers
129167
void protocol_electrum::handle_blockchain_block_headers(const code& ec,
130-
rpc_interface::blockchain_block_headers, double ,
131-
double , double ) NOEXCEPT
168+
rpc_interface::blockchain_block_headers, double start_height, double count,
169+
double cp_height) NOEXCEPT
132170
{
133-
if (stopped(ec)) return;
134-
send_code(error::not_implemented);
171+
using namespace system;
172+
if (stopped(ec))
173+
return;
174+
175+
size_t quantity{};
176+
size_t waypoint{};
177+
size_t starting{};
178+
if (!to_integer(quantity, count) ||
179+
!to_integer(waypoint, cp_height) ||
180+
!to_integer(starting, start_height))
181+
{
182+
send_code(error::invalid_argument);
183+
return;
184+
}
185+
186+
if (is_add_overflow(starting, quantity))
187+
{
188+
send_code(error::argument_overflow);
189+
return;
190+
}
191+
192+
// The documented requirement: `start_height + (count - 1) <= cp_height` is
193+
// ambiguous at count = 0 so guard must be applied to both args and prover.
194+
const auto target = starting + sub1(quantity);
195+
const auto prove = !is_zero(quantity) && !is_zero(waypoint);
196+
if (prove && target > waypoint)
197+
{
198+
send_code(error::target_overflow);
199+
return;
200+
}
201+
202+
// Recommended to be at least one difficulty retarget period, e.g. 2016.
203+
// The maximum number of headers the server will return in single request.
204+
const auto maximum = server_settings().electrum.maximum_headers;
205+
206+
// Returned headers are assured to be contiguous despite intervening reorg.
207+
// No headers may be returned, which implies start > confirmed top block.
208+
const auto& query = archive();
209+
const auto bound = limit(quantity, maximum);
210+
const auto links = query.get_confirmed_headers(starting, bound);
211+
constexpr auto header_size = chain::header::serialized_size();
212+
auto size = two * header_size * links.size();
213+
214+
// Fetch and serialize headers.
215+
array_t headers{};
216+
headers.reserve(links.size());
217+
for (const auto& link: links)
218+
{
219+
if (const auto header = query.get_header(link); header)
220+
{
221+
// TODO: optimize by query directly returning wire serialization.
222+
headers.push_back(to_hex(*header, header_size));
223+
}
224+
else
225+
{
226+
send_code(error::server_error);
227+
return;
228+
}
229+
};
230+
231+
value_t value
232+
{
233+
object_t
234+
{
235+
{ "count", quantity },
236+
{ "headers", std::move(headers) },
237+
{ "max", maximum }
238+
}
239+
};
240+
241+
// There is a very slim change of inconsistency given an intervening reorg
242+
// because of get_merkle_root_and_proof() use of height-based calculations.
243+
// This is acceptable as it must be verified by caller in any case.
244+
if (prove)
245+
{
246+
hashes proof{};
247+
hash_digest root{};
248+
if (const auto code = query.get_merkle_root_and_proof(root, proof,
249+
target, waypoint))
250+
{
251+
send_code(code);
252+
return;
253+
}
254+
255+
array_t branch(proof.size());
256+
std::ranges::transform(proof, branch.begin(),
257+
[](const auto& hash) { return encode_base16(hash); });
258+
259+
auto& result = std::get<object_t>(value.value());
260+
result["root"] = encode_base16(root);
261+
result["branch"] = std::move(branch);
262+
size += two * hash_size * add1(proof.size());
263+
}
264+
265+
send_result(std::move(value), size + 42u, BIND(complete, _1));
135266
}
136267

137268
void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec,
@@ -141,21 +272,22 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec,
141272
return;
142273

143274
const auto& query = archive();
144-
const auto link = query.to_header(query.get_top_confirmed_hash());
145-
const auto height = query.get_height(link);
146-
const auto header = query.get_header(link);
147-
if (height.is_terminal() || !header)
275+
const auto top = query.get_top_confirmed();
276+
const auto link = query.to_confirmed(top);
277+
278+
// This is unlikely but possible due to a race condition during reorg.
279+
if (!link.is_terminal())
148280
{
149281
send_code(error::not_found);
150282
return;
151283
}
152284

153-
// See protocol_native::to_hex().
154-
std::string hex(two * chain::header::serialized_size(), '\0');
155-
stream::out::fast sink{ hex };
156-
write::base16::fast writer{ sink };
157-
header->to_data(writer);
158-
BC_ASSERT(writer);
285+
const auto header = query.get_header(link);
286+
if (!header)
287+
{
288+
send_code(error::server_error);
289+
return;
290+
}
159291

160292
// TODO: signal header subscription.
161293

@@ -166,8 +298,8 @@ void protocol_electrum::handle_blockchain_headers_subscribe(const code& ec,
166298
{
167299
object_t
168300
{
169-
{ "height", height.value },
170-
{ "hex", hex }
301+
{ "height", top },
302+
{ "hex", to_hex(*header, chain::header::serialized_size()) }
171303
}
172304
}, 256, BIND(complete, _1));
173305
}

test/error.cpp

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,15 @@ BOOST_AUTO_TEST_CASE(error_t__code__not_found__true_expected_message)
191191
BOOST_REQUIRE_EQUAL(ec.message(), "not_found");
192192
}
193193

194+
BOOST_AUTO_TEST_CASE(error_t__code__not_implemented__true_expected_message)
195+
{
196+
constexpr auto value = error::not_implemented;
197+
const auto ec = code(value);
198+
BOOST_REQUIRE(ec);
199+
BOOST_REQUIRE(ec == value);
200+
BOOST_REQUIRE_EQUAL(ec.message(), "not_implemented");
201+
}
202+
194203
BOOST_AUTO_TEST_CASE(error_t__code__invalid_argument__true_expected_message)
195204
{
196205
constexpr auto value = error::invalid_argument;
@@ -200,13 +209,31 @@ BOOST_AUTO_TEST_CASE(error_t__code__invalid_argument__true_expected_message)
200209
BOOST_REQUIRE_EQUAL(ec.message(), "invalid_argument");
201210
}
202211

203-
BOOST_AUTO_TEST_CASE(error_t__code__not_implemented__true_expected_message)
212+
BOOST_AUTO_TEST_CASE(error_t__code__argument_overflow__true_expected_message)
204213
{
205-
constexpr auto value = error::not_implemented;
214+
constexpr auto value = error::argument_overflow;
206215
const auto ec = code(value);
207216
BOOST_REQUIRE(ec);
208217
BOOST_REQUIRE(ec == value);
209-
BOOST_REQUIRE_EQUAL(ec.message(), "not_implemented");
218+
BOOST_REQUIRE_EQUAL(ec.message(), "argument_overflow");
219+
}
220+
221+
BOOST_AUTO_TEST_CASE(error_t__code__target_overflow__true_expected_message)
222+
{
223+
constexpr auto value = error::target_overflow;
224+
const auto ec = code(value);
225+
BOOST_REQUIRE(ec);
226+
BOOST_REQUIRE(ec == value);
227+
BOOST_REQUIRE_EQUAL(ec.message(), "target_overflow");
228+
}
229+
230+
BOOST_AUTO_TEST_CASE(error_t__code__server_error__true_expected_message)
231+
{
232+
constexpr auto value = error::server_error;
233+
const auto ec = code(value);
234+
BOOST_REQUIRE(ec);
235+
BOOST_REQUIRE(ec == value);
236+
BOOST_REQUIRE_EQUAL(ec.message(), "server_error");
210237
}
211238

212239
BOOST_AUTO_TEST_SUITE_END()

test/settings.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ BOOST_AUTO_TEST_CASE(server__electrum_server__defaults__expected)
230230
BOOST_REQUIRE(server.cert_path.empty());
231231
BOOST_REQUIRE(server.key_path.empty());
232232
BOOST_REQUIRE(server.key_pass.empty());
233+
234+
// electrum_server
235+
BOOST_REQUIRE_EQUAL(server.maximum_headers, 10u * 2016u);
233236
}
234237

235238
BOOST_AUTO_TEST_CASE(server__stratum_v1_server__defaults__expected)

0 commit comments

Comments
 (0)