Skip to content

Commit 7c716cc

Browse files
committed
Merge #190: Implement Electrum Protocol v1.6
638da77 Return `UnexpectedEof` error if we read 0 bytes from stream (Elias Rohrer) 077c8ef Mark `relay_fee` as deprecated in protocol v1.6+ (Elias Rohrer) 67e8fb0 Add `transaction_broadcast_package` method for package relay (Elias Rohrer) 1c0416f Support v1.6 `block_headers` response format with backwards compatibility (Elias Rohrer) 1dbd462 Add optional `EstimationMode` parameter to `estimate_fee` (Elias Rohrer) b708b0c Add `mempool_get_info` method for v1.6 mempool fee information (Elias Rohrer) 8ff9347 Automatically negotiate protocol version on connect (Elias Rohrer) 45afc7c Drop `test_local_timeout` test case (Elias Rohrer) 41cb8ac Switch to use a Electrum server supporting v1.6 in tests (Elias Rohrer) Pull request description: Closes #189. I asked Claude Code to implement the changes necessary to update to the recently released [Electrum Protocol v1.6](https://electrum-protocol.readthedocs.io/en/latest/protocol-changes.html#version-1-6). Putting in draft for now until I got initial feedback (cc `oleonardolima` `ValuedMammal`). We also still need to change the test setup to a v1.6-compatible server to actually run the new tests against. ACKs for top commit: ValuedMammal: ACK 638da77 oleonardolima: tACK 638da77 luisschwab: tACK 638da77 Tree-SHA512: fa0b215275fd559f488f8c35ba26acbb7333aa2b465473983fc080781c5cee61a6b6ea071a53433e03a28d08f12fcd9d28fccf7d1e2ae527972841a9dbbeeba1
2 parents b2fdc5c + 638da77 commit 7c716cc

6 files changed

Lines changed: 404 additions & 123 deletions

File tree

.github/workflows/cont_integration.yml

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ jobs:
77
name: Test
88
runs-on: ubuntu-latest
99
env:
10-
TEST_ELECTRUM_SERVER: electrum.blockstream.info:50001
10+
TEST_ELECTRUM_SERVER: fortress.qtornado.com:443
1111
strategy:
1212
matrix:
1313
rust:
@@ -30,10 +30,6 @@ jobs:
3030
toolchain: ${{ matrix.rust }}
3131
- name: Test
3232
run: cargo test --verbose --all-features
33-
- name: Setup iptables for the timeout test
34-
run: sudo ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP
35-
- name: Timeout test
36-
run: cargo test -- --ignored test_local_timeout
3733
- run: cargo check --verbose --features=use-openssl
3834
- run: cargo check --verbose --no-default-features --features=proxy
3935
- run: cargo check --verbose --no-default-features --features=minimal

src/api.rs

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ where
4242
(**self).block_headers(start_height, count)
4343
}
4444

45-
fn estimate_fee(&self, number: usize) -> Result<f64, Error> {
46-
(**self).estimate_fee(number)
45+
fn estimate_fee(&self, number: usize, mode: Option<EstimationMode>) -> Result<f64, Error> {
46+
(**self).estimate_fee(number, mode)
4747
}
4848

4949
fn relay_fee(&self) -> Result<f64, Error> {
@@ -141,6 +141,13 @@ where
141141
(**self).transaction_broadcast_raw(raw_tx)
142142
}
143143

144+
fn transaction_broadcast_package_raw<T: AsRef<[u8]>>(
145+
&self,
146+
raw_txs: &[T],
147+
) -> Result<BroadcastPackageRes, Error> {
148+
(**self).transaction_broadcast_package_raw(raw_txs)
149+
}
150+
144151
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error> {
145152
(**self).transaction_get_merkle(txid, height)
146153
}
@@ -172,6 +179,10 @@ where
172179
(**self).server_features()
173180
}
174181

182+
fn mempool_get_info(&self) -> Result<MempoolInfoRes, Error> {
183+
(**self).mempool_get_info()
184+
}
185+
175186
fn ping(&self) -> Result<(), Error> {
176187
(**self).ping()
177188
}
@@ -241,6 +252,21 @@ pub trait ElectrumApi {
241252
self.transaction_broadcast_raw(&buffer)
242253
}
243254

255+
/// Broadcasts a package of transactions to the network.
256+
///
257+
/// The package must consist of a child with its parents, where none of the parents
258+
/// depend on one another. The package must be topologically sorted, with the child
259+
/// being the last element in the array.
260+
///
261+
/// This method was added in protocol v1.6 for package relay support.
262+
fn transaction_broadcast_package(
263+
&self,
264+
txs: &[Transaction],
265+
) -> Result<BroadcastPackageRes, Error> {
266+
let raw_txs: Vec<Vec<u8>> = txs.iter().map(serialize).collect();
267+
self.transaction_broadcast_package_raw(&raw_txs)
268+
}
269+
244270
/// Executes the requested API call returning the raw answer.
245271
fn raw_call(
246272
&self,
@@ -268,9 +294,16 @@ pub trait ElectrumApi {
268294
fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error>;
269295

270296
/// Estimates the fee required in **Bitcoin per kilobyte** to confirm a transaction in `number` blocks.
271-
fn estimate_fee(&self, number: usize) -> Result<f64, Error>;
297+
///
298+
/// Optionally takes an [`EstimationMode`] parameter to specify the fee estimation mode.
299+
/// This parameter was added in protocol v1.6.
300+
fn estimate_fee(&self, number: usize, mode: Option<EstimationMode>) -> Result<f64, Error>;
272301

273302
/// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**.
303+
///
304+
/// **Note:** This method was removed in protocol v1.6+. Use
305+
/// [`mempool_get_info`](#method.mempool_get_info) instead, which provides `minrelaytxfee`
306+
/// along with additional mempool fee information.
274307
fn relay_fee(&self) -> Result<f64, Error>;
275308

276309
/// Subscribes to notifications for activity on a specific *scriptPubKey*.
@@ -370,6 +403,18 @@ pub trait ElectrumApi {
370403
/// Broadcasts the raw bytes of a transaction to the network.
371404
fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error>;
372405

406+
/// Broadcasts a package of raw transactions to the network.
407+
///
408+
/// The package must consist of a child with its parents, where none of the parents
409+
/// depend on one another. The package must be topologically sorted, with the child
410+
/// being the last element in the array.
411+
///
412+
/// This method was added in protocol v1.6 for package relay support.
413+
fn transaction_broadcast_package_raw<T: AsRef<[u8]>>(
414+
&self,
415+
raw_txs: &[T],
416+
) -> Result<BroadcastPackageRes, Error>;
417+
373418
/// Returns the merkle path for the transaction `txid` confirmed in the block at `height`.
374419
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error>;
375420

@@ -398,6 +443,12 @@ pub trait ElectrumApi {
398443
/// Returns the capabilities of the server.
399444
fn server_features(&self) -> Result<ServerFeaturesRes, Error>;
400445

446+
/// Returns information about the current state of the mempool.
447+
///
448+
/// This method was added in protocol v1.6 and replaces `relay_fee` by providing
449+
/// `minrelaytxfee` along with additional mempool fee information.
450+
fn mempool_get_info(&self) -> Result<MempoolInfoRes, Error>;
451+
401452
/// Pings the server. This method can also be used as a "dummy" call to trigger the processing
402453
/// of incoming block header or script notifications.
403454
fn ping(&self) -> Result<(), Error>;
@@ -449,7 +500,11 @@ mod test {
449500
unreachable!()
450501
}
451502

452-
fn estimate_fee(&self, _: usize) -> Result<f64, super::Error> {
503+
fn estimate_fee(
504+
&self,
505+
_: usize,
506+
_: Option<super::EstimationMode>,
507+
) -> Result<f64, super::Error> {
453508
unreachable!()
454509
}
455510

@@ -572,6 +627,13 @@ mod test {
572627
unreachable!()
573628
}
574629

630+
fn transaction_broadcast_package_raw<T: AsRef<[u8]>>(
631+
&self,
632+
_: &[T],
633+
) -> Result<super::BroadcastPackageRes, super::Error> {
634+
unreachable!()
635+
}
636+
575637
fn transaction_get_merkle(
576638
&self,
577639
_: &bitcoin::Txid,
@@ -607,6 +669,10 @@ mod test {
607669
unreachable!()
608670
}
609671

672+
fn mempool_get_info(&self) -> Result<super::MempoolInfoRes, super::Error> {
673+
unreachable!()
674+
}
675+
610676
fn ping(&self) -> Result<(), super::Error> {
611677
unreachable!()
612678
}

src/batch.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use bitcoin::{Script, Txid};
66

7-
use crate::types::{Call, Param, ToElectrumScriptHash};
7+
use crate::types::{Call, EstimationMode, Param, ToElectrumScriptHash};
88

99
/// Helper structure that caches all the requests before they are actually sent to the server.
1010
///
@@ -74,8 +74,11 @@ impl Batch {
7474
}
7575

7676
/// Add one `blockchain.estimatefee` request to the batch queue
77-
pub fn estimate_fee(&mut self, number: usize) {
78-
let params = vec![Param::Usize(number)];
77+
pub fn estimate_fee(&mut self, number: usize, mode: Option<EstimationMode>) {
78+
let mut params = vec![Param::Usize(number)];
79+
if let Some(mode) = mode {
80+
params.push(Param::String(mode.to_string()));
81+
}
7982
self.calls
8083
.push((String::from("blockchain.estimatefee"), params));
8184
}

src/client.rs

Lines changed: 15 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ impl ElectrumApi for Client {
207207
}
208208

209209
#[inline]
210-
fn estimate_fee(&self, number: usize) -> Result<f64, Error> {
211-
impl_inner_call!(self, estimate_fee, number)
210+
fn estimate_fee(&self, number: usize, mode: Option<EstimationMode>) -> Result<f64, Error> {
211+
impl_inner_call!(self, estimate_fee, number, mode)
212212
}
213213

214214
#[inline]
@@ -322,6 +322,14 @@ impl ElectrumApi for Client {
322322
impl_inner_call!(self, transaction_broadcast_raw, raw_tx)
323323
}
324324

325+
#[inline]
326+
fn transaction_broadcast_package_raw<T: AsRef<[u8]>>(
327+
&self,
328+
raw_txs: &[T],
329+
) -> Result<BroadcastPackageRes, Error> {
330+
impl_inner_call!(self, transaction_broadcast_package_raw, raw_txs)
331+
}
332+
325333
#[inline]
326334
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error> {
327335
impl_inner_call!(self, transaction_get_merkle, txid, height)
@@ -362,6 +370,11 @@ impl ElectrumApi for Client {
362370
impl_inner_call!(self, server_features)
363371
}
364372

373+
#[inline]
374+
fn mempool_get_info(&self) -> Result<MempoolInfoRes, Error> {
375+
impl_inner_call!(self, mempool_get_info)
376+
}
377+
365378
#[inline]
366379
fn ping(&self) -> Result<(), Error> {
367380
impl_inner_call!(self, ping)
@@ -407,54 +420,4 @@ mod tests {
407420

408421
assert!(!exhausted)
409422
}
410-
411-
#[test]
412-
#[ignore]
413-
fn test_local_timeout() {
414-
// This test assumes a couple things:
415-
// - that `localhost` is resolved to two IP addresses, `127.0.0.1` and `::1` (with the v6
416-
// one having higher priority)
417-
// - that the system silently drops packets to `[::1]:60000` or a different port if
418-
// specified through `TEST_ELECTRUM_TIMEOUT_PORT`
419-
//
420-
// this can be setup with: ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP
421-
// and removed with: ip6tables -D INPUT -p tcp -d ::1 --dport 60000 -j DROP
422-
//
423-
// The test tries to create a client to `localhost` and expects it to succeed, but only
424-
// after at least 2 seconds have passed which is roughly the timeout time for the first
425-
// try.
426-
427-
use std::net::TcpListener;
428-
use std::sync::mpsc::channel;
429-
use std::time::{Duration, Instant};
430-
431-
let endpoint =
432-
std::env::var("TEST_ELECTRUM_TIMEOUT_PORT").unwrap_or("localhost:60000".into());
433-
let (sender, receiver) = channel();
434-
435-
std::thread::spawn(move || {
436-
let listener = TcpListener::bind("127.0.0.1:60000").unwrap();
437-
sender.send(()).unwrap();
438-
439-
for _stream in listener.incoming() {
440-
std::thread::sleep(Duration::from_secs(60))
441-
}
442-
});
443-
444-
receiver
445-
.recv_timeout(Duration::from_secs(5))
446-
.expect("Can't start local listener");
447-
448-
let now = Instant::now();
449-
let client = Client::from_config(
450-
&endpoint,
451-
crate::config::ConfigBuilder::new()
452-
.timeout(Some(Duration::from_secs(5)))
453-
.build(),
454-
);
455-
let elapsed = now.elapsed();
456-
457-
assert!(client.is_ok());
458-
assert!(elapsed > Duration::from_secs(2));
459-
}
460423
}

0 commit comments

Comments
 (0)