Skip to content

Commit 78a1134

Browse files
committed
feat(kyoto): update bdk_kyoto to 0.9.0
Debug strings and info messages from Kyoto are on separate channels now. `TxSent` is renamed to `TxGossiped` and is only emitted if we are sure that the remote node has requested the transaction from us and we could successfully send it. The node accepts a Socks5 proxy to route traffic over Tor. I added a specific `Socks5Proxy` for this. Internally the `bdk_kyoto` crate simply provides an extension trait over the Kyoto node builder, so users can take full advantage of the Kyoto features build still build for a specific `Wallet`. I will follow up with some additional configurations on the builder that are non-breaking.
1 parent 1d8ee94 commit 78a1134

File tree

6 files changed

+95
-69
lines changed

6 files changed

+95
-69
lines changed

bdk-ffi/Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bdk-ffi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ bdk_wallet = { version = "1.2.0", features = ["all-keys", "keys-bip39", "rusqlit
2222
bdk_core = { version = "0.4.1" }
2323
bdk_esplora = { version = "0.20.1", default-features = false, features = ["std", "blocking", "blocking-https-rustls"] }
2424
bdk_electrum = { version = "0.21.0", default-features = false, features = ["use-rustls-ring"] }
25-
bdk_kyoto = { version = "0.8.0" }
25+
bdk_kyoto = { version = "0.9.0" }
2626

2727
uniffi = { version = "=0.29.1" }
2828
thiserror = "1.0.58"

bdk-ffi/src/kyoto.rs

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
use bdk_kyoto::builder::LightClientBuilder as BDKCbfBuilder;
2-
use bdk_kyoto::builder::ServiceFlags;
3-
use bdk_kyoto::builder::TrustedPeer;
1+
use bdk_kyoto::builder::NodeBuilder as BDKCbfBuilder;
2+
use bdk_kyoto::builder::NodeBuilderExt;
43
use bdk_kyoto::kyoto::tokio;
54
use bdk_kyoto::kyoto::AddrV2;
65
use bdk_kyoto::kyoto::ScriptBuf;
6+
use bdk_kyoto::kyoto::ServiceFlags;
77
use bdk_kyoto::LightClient as BDKLightClient;
88
use bdk_kyoto::NodeDefault;
99
use bdk_kyoto::Receiver;
1010
use bdk_kyoto::RejectReason;
1111
use bdk_kyoto::Requester;
12+
use bdk_kyoto::TrustedPeer;
1213
use bdk_kyoto::UnboundedReceiver;
1314
use bdk_kyoto::UpdateSubscriber;
1415
use bdk_kyoto::WalletExt;
@@ -17,7 +18,6 @@ use bdk_kyoto::Warning as Warn;
1718
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
1819
use std::path::PathBuf;
1920
use std::sync::Arc;
20-
use std::time::Duration;
2121

2222
use tokio::sync::Mutex;
2323

@@ -31,7 +31,6 @@ type LogLevel = bdk_kyoto::kyoto::LogLevel;
3131
type NodeState = bdk_kyoto::NodeState;
3232
type ScanType = bdk_kyoto::ScanType;
3333

34-
const TIMEOUT: u64 = 10;
3534
const DEFAULT_CONNECTIONS: u8 = 2;
3635
const CWD_PATH: &str = ".";
3736

@@ -48,7 +47,8 @@ pub struct CbfComponents {
4847
#[derive(Debug, uniffi::Object)]
4948
pub struct CbfClient {
5049
sender: Arc<Requester>,
51-
log_rx: Mutex<Receiver<bdk_kyoto::Log>>,
50+
log_rx: Mutex<Receiver<String>>,
51+
info_rx: Mutex<Receiver<bdk_kyoto::Info>>,
5252
warning_rx: Mutex<UnboundedReceiver<bdk_kyoto::Warning>>,
5353
update_rx: Mutex<UpdateSubscriber>,
5454
}
@@ -98,6 +98,7 @@ pub struct CbfBuilder {
9898
scan_type: ScanType,
9999
log_level: LogLevel,
100100
dns_resolver: Option<Arc<IpAddress>>,
101+
socks5_proxy: Option<Socks5Proxy>,
101102
peers: Vec<Peer>,
102103
}
103104

@@ -112,6 +113,7 @@ impl CbfBuilder {
112113
scan_type: ScanType::default(),
113114
log_level: LogLevel::default(),
114115
dns_resolver: None,
116+
socks5_proxy: None,
115117
peers: Vec::new(),
116118
}
117119
}
@@ -167,6 +169,13 @@ impl CbfBuilder {
167169
})
168170
}
169171

172+
pub fn socks5_proxy(&self, proxy: Socks5Proxy) -> Arc<Self> {
173+
Arc::new(CbfBuilder {
174+
socks5_proxy: Some(proxy),
175+
..self.clone()
176+
})
177+
}
178+
170179
/// Construct a [`CbfComponents`] for a [`Wallet`].
171180
pub fn build(&self, wallet: &Wallet) -> Result<CbfComponents, CbfBuilderError> {
172181
let wallet = wallet.get_wallet();
@@ -181,31 +190,41 @@ impl CbfBuilder {
181190
.map(|path| PathBuf::from(&path))
182191
.unwrap_or(PathBuf::from(CWD_PATH));
183192

184-
let mut builder = BDKCbfBuilder::new()
185-
.connections(self.connections)
193+
let mut builder = BDKCbfBuilder::new(wallet.network())
194+
.required_peers(self.connections)
186195
.data_dir(path_buf)
187-
.scan_type(self.scan_type)
188196
.log_level(self.log_level)
189-
.timeout_duration(Duration::from_secs(TIMEOUT))
190-
.peers(trusted_peers);
197+
.add_peers(trusted_peers);
191198

192199
if let Some(ip_addr) = self.dns_resolver.clone().map(|ip| ip.inner) {
193200
builder = builder.dns_resolver(ip_addr);
194201
}
195202

203+
if let Some(proxy) = &self.socks5_proxy {
204+
let port = proxy.port;
205+
let addr = proxy.address.inner;
206+
builder = builder.socks5_proxy((addr, port));
207+
}
208+
196209
let BDKLightClient {
197210
requester,
198211
log_subscriber,
212+
info_subscriber,
199213
warning_subscriber,
200214
update_subscriber,
201215
node,
202-
} = builder.build(&wallet)?;
216+
} = builder
217+
.build_with_wallet(&wallet, self.scan_type)
218+
.map_err(|e| CbfBuilderError::DatabaseError {
219+
reason: e.to_string(),
220+
})?;
203221

204222
let node = CbfNode { node };
205223

206224
let client = CbfClient {
207225
sender: Arc::new(requester),
208226
log_rx: Mutex::new(log_subscriber),
227+
info_rx: Mutex::new(info_subscriber),
209228
warning_rx: Mutex::new(warning_subscriber),
210229
update_rx: Mutex::new(update_subscriber),
211230
};
@@ -220,12 +239,17 @@ impl CbfBuilder {
220239
#[uniffi::export]
221240
impl CbfClient {
222241
/// Return the next available log message from a node. If none is returned, the node has stopped.
223-
pub async fn next_log(&self) -> Result<Log, CbfError> {
242+
pub async fn next_log(&self) -> Result<String, CbfError> {
224243
let mut log_rx = self.log_rx.lock().await;
225-
log_rx
244+
log_rx.recv().await.ok_or(CbfError::NodeStopped)
245+
}
246+
247+
pub async fn next_info(&self) -> Result<Info, CbfError> {
248+
let mut info_rx = self.info_rx.lock().await;
249+
info_rx
226250
.recv()
227251
.await
228-
.map(|log| log.into())
252+
.map(|e| e.into())
229253
.ok_or(CbfError::NodeStopped)
230254
}
231255

@@ -241,33 +265,32 @@ impl CbfClient {
241265

242266
/// Return an [`Update`]. This is method returns once the node syncs to the rest of
243267
/// the network or a new block has been gossiped.
244-
pub async fn update(&self) -> Option<Arc<Update>> {
268+
pub async fn update(&self) -> Arc<Update> {
245269
let update = self.update_rx.lock().await.update().await;
246-
update.map(|update| Arc::new(Update(update)))
270+
Arc::new(Update(update))
247271
}
248272

249273
/// Add scripts for the node to watch for as they are revealed. Typically used after creating
250274
/// a transaction or revealing a receive address.
251275
///
252276
/// Note that only future blocks will be checked for these scripts, not past blocks.
253-
pub async fn add_revealed_scripts(&self, wallet: &Wallet) -> Result<(), CbfError> {
277+
pub fn add_revealed_scripts(&self, wallet: &Wallet) -> Result<(), CbfError> {
254278
let script_iter: Vec<ScriptBuf> = {
255279
let wallet_lock = wallet.get_wallet();
256280
wallet_lock.peek_revealed_plus_lookahead().collect()
257281
};
258282
for script in script_iter.into_iter() {
259283
self.sender
260284
.add_script(script)
261-
.await
262285
.map_err(|_| CbfError::NodeStopped)?
263286
}
264287
Ok(())
265288
}
266289

267290
/// Broadcast a transaction to the network, erroring if the node has stopped running.
268-
pub async fn broadcast(&self, transaction: &Transaction) -> Result<(), CbfError> {
291+
pub fn broadcast(&self, transaction: &Transaction) -> Result<(), CbfError> {
269292
let tx = transaction.into();
270-
self.sender.broadcast_random(tx).await.map_err(From::from)
293+
self.sender.broadcast_random(tx).map_err(From::from)
271294
}
272295

273296
/// The minimum fee rate required to broadcast a transcation to all connected peers.
@@ -280,44 +303,41 @@ impl CbfClient {
280303
}
281304

282305
/// Check if the node is still running in the background.
283-
pub async fn is_running(&self) -> bool {
284-
self.sender.is_running().await
306+
pub fn is_running(&self) -> bool {
307+
self.sender.is_running()
285308
}
286309

287310
/// Stop the [`CbfNode`]. Errors if the node is already stopped.
288-
pub async fn shutdown(&self) -> Result<(), CbfError> {
289-
self.sender.shutdown().await.map_err(From::from)
311+
pub fn shutdown(&self) -> Result<(), CbfError> {
312+
self.sender.shutdown().map_err(From::from)
290313
}
291314
}
292315

293316
/// A log message from the node.
294317
#[derive(Debug, uniffi::Enum)]
295-
pub enum Log {
296-
/// A human-readable debug message.
297-
Debug { log: String },
318+
pub enum Info {
298319
/// All the required connections have been met. This is subject to change.
299320
ConnectionsMet,
300321
/// A percentage value of filters that have been scanned.
301322
Progress { progress: f32 },
302323
/// A state in the node syncing process.
303324
StateUpdate { node_state: NodeState },
304-
/// A transaction was broadcast over the wire.
325+
/// A transaction was broadcast over the wire to a peer that requested it from our inventory.
305326
/// The transaction may or may not be rejected by recipient nodes.
306-
TxSent { txid: String },
327+
TxGossiped { wtxid: String },
307328
}
308329

309-
impl From<bdk_kyoto::Log> for Log {
310-
fn from(value: bdk_kyoto::Log) -> Log {
330+
impl From<bdk_kyoto::Info> for Info {
331+
fn from(value: bdk_kyoto::Info) -> Info {
311332
match value {
312-
bdk_kyoto::Log::Debug(log) => Log::Debug { log },
313-
bdk_kyoto::Log::ConnectionsMet => Log::ConnectionsMet,
314-
bdk_kyoto::Log::Progress(progress) => Log::Progress {
333+
bdk_kyoto::Info::ConnectionsMet => Info::ConnectionsMet,
334+
bdk_kyoto::Info::Progress(progress) => Info::Progress {
315335
progress: progress.percentage_complete(),
316336
},
317-
bdk_kyoto::Log::TxSent(txid) => Log::TxSent {
318-
txid: txid.to_string(),
337+
bdk_kyoto::Info::TxGossiped(wtxid) => Info::TxGossiped {
338+
wtxid: wtxid.to_string(),
319339
},
320-
bdk_kyoto::Log::StateChange(state) => Log::StateUpdate { node_state: state },
340+
bdk_kyoto::Info::StateChange(state) => Info::StateUpdate { node_state: state },
321341
}
322342
}
323343
}
@@ -354,7 +374,7 @@ pub enum Warning {
354374
EvaluatingFork,
355375
/// The peer database has no values.
356376
EmptyPeerDatabase,
357-
/// An unexpected error occured processing a peer-to-peer message.
377+
/// An unexpected error occurred processing a peer-to-peer message.
358378
UnexpectedSyncError { warning: String },
359379
/// The node failed to respond to a message sent from the client.
360380
RequestFailed,
@@ -393,11 +413,13 @@ impl From<Warn> for Warning {
393413
/// Select the category of messages for the node to emit.
394414
#[uniffi::remote(Enum)]
395415
pub enum LogLevel {
396-
/// Send `Log::Debug` messages. These messages are intended for debugging or troubleshooting
416+
/// Send string messages. These messages are intended for debugging or troubleshooting
397417
/// node operation.
398418
Debug,
399-
/// Omit `Log::Debug` messages, including their memory allocations. Ideal for a production
400-
/// application that uses minimal logging.
419+
/// Send info and warning messages, but omit debug strings - including their memory allocations.
420+
/// Ideal for a production application that uses minimal logging.
421+
Info,
422+
/// Omit debug strings and info messages, including their memory allocations.
401423
Warning,
402424
}
403425

@@ -415,6 +437,7 @@ pub enum NodeState {
415437
/// We found all known transactions to the wallet.
416438
TransactionsSynced,
417439
}
440+
418441
/// Sync a wallet from the last known block hash, recover a wallet from a specified height,
419442
/// or perform an expedited block header download for a new wallet.
420443
#[uniffi::remote(Enum)]
@@ -468,6 +491,16 @@ impl IpAddress {
468491
}
469492
}
470493

494+
/// A proxy to route network traffic, most likely through a Tor daemon. Normally this proxy is
495+
/// exposed at 127.0.0.1:9050.
496+
#[derive(Debug, Clone, uniffi::Record)]
497+
pub struct Socks5Proxy {
498+
/// The IP address, likely `127.0.0.1`
499+
pub address: Arc<IpAddress>,
500+
/// The listening port, likely `9050`
501+
pub port: u16,
502+
}
503+
471504
impl From<Peer> for TrustedPeer {
472505
fn from(peer: Peer) -> Self {
473506
let services = if peer.v2_transport {

bdk-jvm/lib/src/test/kotlin/org/bitcoindevkit/LiveKyotoTest.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,11 @@ class LiveKyotoTest {
4444
val logJob = launch {
4545
while (true) {
4646
val log = client.nextLog()
47-
println("$log")
47+
println(log)
4848
}
4949
}
5050
node.run()
51-
val updateOpt: Update? = client.update()
52-
val update = assertNotNull(updateOpt)
51+
val update: Update = client.update()
5352
wallet.applyUpdate(update)
5453
assert(wallet.balance().total.toSat() > 0uL) {
5554
"Wallet balance must be greater than 0! Please send funds to ${wallet.revealNextAddress(KeychainKind.EXTERNAL).address} and try again."

bdk-python/tests/test_live_kyoto.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,8 @@ async def log_loop(client: CbfClient):
4545
print(log)
4646
log_task = asyncio.create_task(log_loop(client))
4747
node.run()
48-
update: Update | None = await client.update()
49-
self.assertIsNotNone(update, "Update is None. This should not be possible.")
50-
if update is not None:
51-
wallet.apply_update(update)
48+
update: Update = await client.update()
49+
wallet.apply_update(update)
5250
self.assertGreater(
5351
wallet.balance().total.to_sat(),
5452
0,

bdk-swift/Tests/BitcoinDevKitTests/LiveKyotoTests.swift

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,14 @@ final class LiveKyotoTests: XCTestCase {
4646
}
4747
}
4848
let update = await client.update()
49-
if let update = update {
50-
try wallet.applyUpdate(update: update)
51-
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
52-
XCTAssertGreaterThan(
53-
wallet.balance().total.toSat(),
54-
UInt64(0),
55-
"Wallet must have positive balance, please send funds to \(address)"
56-
)
57-
print("Update applied correctly")
58-
try await client.shutdown()
59-
} else {
60-
print("Update is nil. Ensure this test is ran infrequently.")
61-
}
49+
try wallet.applyUpdate(update: update)
50+
let address = wallet.revealNextAddress(keychain: KeychainKind.external).address.description
51+
XCTAssertGreaterThan(
52+
wallet.balance().total.toSat(),
53+
UInt64(0),
54+
"Wallet must have positive balance, please send funds to \(address)"
55+
)
56+
print("Update applied correctly")
57+
try client.shutdown()
6258
}
6359
}

0 commit comments

Comments
 (0)