Skip to content

Commit ed78d18

Browse files
committed
Merge #628: rpc: use importdescriptors with Core >= 0.21
e1a1372 rpc: use `importdescriptors` with Core >= 0.21 (Alekos Filini) Pull request description: ### Description Only use the old `importmulti` with Core versions that don't support descriptor-based (sqlite) wallets. Add an extra feature to test against Core 0.20 called `test-rpc-legacy`. This also makes us compatible with Core 23.0 and is thus a replacement for #613, which actually looking back at it was adding support for 23.0 but probably breaking older wallets by adding the extra argument to `createwallet`. I believe #613 should now only focus on getting the tests to work against 23.0, which is still important but not such a high priority as being compatible with the latest version of Core. Also fixes #598 ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `cargo fmt` and `cargo clippy` before committing #### New Features: * [x] I've added tests for the new feature * [x] I've added docs for the new feature * [x] I've updated `CHANGELOG.md` ACKs for top commit: notmandatory: ACK e1a1372 Tree-SHA512: 7891b8ab2fc900ea2a186f64a32aea970f28a50339063ed0e1a8d13248e5c038b8fff3d9e26b93cb7daafd0c873379e64a28836dbe4e4b82f1983577a88971ff
2 parents 3269923 + e1a1372 commit ed78d18

File tree

6 files changed

+113
-35
lines changed

6 files changed

+113
-35
lines changed

.github/workflows/cont_integration.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ jobs:
9595
- name: rpc
9696
testprefix: blockchain::rpc::test
9797
features: test-rpc
98+
- name: rpc-legacy
99+
testprefix: blockchain::rpc::test
100+
features: test-rpc-legacy
98101
- name: esplora
99102
testprefix: esplora
100103
features: test-esplora,use-esplora-reqwest,verify

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1818
- Creation of Taproot PSBTs (BIP-371)
1919
- Signing Taproot PSBTs (key spend and script spend)
2020
- Support for `tr()` descriptors in the `descriptor!()` macro
21+
- Add support for Bitcoin Core 23.0 when using the `rpc` blockchain
2122

2223
## [v0.18.0] - [v0.17.0]
2324

Cargo.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,17 @@ reqwest-default-tls = ["reqwest/default-tls"]
8888

8989
# Debug/Test features
9090
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
91-
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "test-blockchains"]
92-
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "test-blockchains"]
93-
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "test-blockchains"]
91+
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
92+
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
93+
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
94+
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
9495
test-md-docs = ["electrum"]
9596

9697
[dev-dependencies]
9798
lazy_static = "1.4"
9899
env_logger = "0.7"
99100
clap = "2.33"
100-
electrsd = { version= "0.19.1", features = ["bitcoind_22_0"] }
101+
electrsd = "0.19.1"
101102

102103
[[example]]
103104
name = "address_validator"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Integration testing require testing features, for example:
166166
cargo test --features test-electrum
167167
```
168168

169-
The other options are `test-esplora` or `test-rpc`.
169+
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
170170
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
171171

172172
## License

src/blockchain/rpc.rs

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,17 @@
3232
//! ```
3333
3434
use crate::bitcoin::consensus::deserialize;
35+
use crate::bitcoin::hashes::hex::ToHex;
3536
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
3637
use crate::blockchain::*;
3738
use crate::database::{BatchDatabase, DatabaseUtils};
39+
use crate::descriptor::get_checksum;
3840
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
3941
use bitcoincore_rpc::json::{
4042
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
4143
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
4244
};
43-
use bitcoincore_rpc::jsonrpc::serde_json::Value;
45+
use bitcoincore_rpc::jsonrpc::serde_json::{json, Value};
4446
use bitcoincore_rpc::Auth as RpcAuth;
4547
use bitcoincore_rpc::{Client, RpcApi};
4648
use log::debug;
@@ -54,6 +56,8 @@ use std::str::FromStr;
5456
pub struct RpcBlockchain {
5557
/// Rpc client to the node, includes the wallet name
5658
client: Client,
59+
/// Whether the wallet is a "descriptor" or "legacy" wallet in Core
60+
is_descriptors: bool,
5761
/// Blockchain capabilities, cached here at startup
5862
capabilities: HashSet<Capability>,
5963
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
@@ -177,22 +181,53 @@ impl WalletSync for RpcBlockchain {
177181
"importing {} script_pubkeys (some maybe already imported)",
178182
scripts_pubkeys.len()
179183
);
180-
let requests: Vec<_> = scripts_pubkeys
181-
.iter()
182-
.map(|s| ImportMultiRequest {
183-
timestamp: ImportMultiRescanSince::Timestamp(0),
184-
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(s)),
185-
watchonly: Some(true),
186-
..Default::default()
187-
})
188-
.collect();
189-
let options = ImportMultiOptions {
190-
rescan: Some(false),
191-
};
192-
// Note we use import_multi because as of bitcoin core 0.21.0 many descriptors are not supported
193-
// https://bitcoindevkit.org/descriptors/#compatibility-matrix
194-
//TODO maybe convenient using import_descriptor for compatible descriptor and import_multi as fallback
195-
self.client.import_multi(&requests, Some(&options))?;
184+
185+
if self.is_descriptors {
186+
// Core still doesn't support complex descriptors like BDK, but when the wallet type is
187+
// "descriptors" we should import individual addresses using `importdescriptors` rather
188+
// than `importmulti`, using the `raw()` descriptor which allows us to specify an
189+
// arbitrary script
190+
let requests = Value::Array(
191+
scripts_pubkeys
192+
.iter()
193+
.map(|s| {
194+
let desc = format!("raw({})", s.to_hex());
195+
json!({
196+
"timestamp": "now",
197+
"desc": format!("{}#{}", desc, get_checksum(&desc).unwrap()),
198+
})
199+
})
200+
.collect(),
201+
);
202+
203+
let res: Vec<Value> = self.client.call("importdescriptors", &[requests])?;
204+
res.into_iter()
205+
.map(|v| match v["success"].as_bool() {
206+
Some(true) => Ok(()),
207+
Some(false) => Err(Error::Generic(
208+
v["error"]["message"]
209+
.as_str()
210+
.unwrap_or("Unknown error")
211+
.to_string(),
212+
)),
213+
_ => Err(Error::Generic("Unexpected response from Core".to_string())),
214+
})
215+
.collect::<Result<Vec<_>, _>>()?;
216+
} else {
217+
let requests: Vec<_> = scripts_pubkeys
218+
.iter()
219+
.map(|s| ImportMultiRequest {
220+
timestamp: ImportMultiRescanSince::Timestamp(0),
221+
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(s)),
222+
watchonly: Some(true),
223+
..Default::default()
224+
})
225+
.collect();
226+
let options = ImportMultiOptions {
227+
rescan: Some(false),
228+
};
229+
self.client.import_multi(&requests, Some(&options))?;
230+
}
196231

197232
loop {
198233
let current_height = self.get_height()?;
@@ -369,20 +404,36 @@ impl ConfigurableBlockchain for RpcBlockchain {
369404
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
370405

371406
let client = Client::new(wallet_url.as_str(), config.auth.clone().into())?;
407+
let rpc_version = client.version()?;
408+
372409
let loaded_wallets = client.list_wallets()?;
373410
if loaded_wallets.contains(&wallet_name) {
374411
debug!("wallet already loaded {:?}", wallet_name);
412+
} else if list_wallet_dir(&client)?.contains(&wallet_name) {
413+
client.load_wallet(&wallet_name)?;
414+
debug!("wallet loaded {:?}", wallet_name);
375415
} else {
376-
let existing_wallets = list_wallet_dir(&client)?;
377-
if existing_wallets.contains(&wallet_name) {
378-
client.load_wallet(&wallet_name)?;
379-
debug!("wallet loaded {:?}", wallet_name);
380-
} else {
416+
// pre-0.21 use legacy wallets
417+
if rpc_version < 210_000 {
381418
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
382-
debug!("wallet created {:?}", wallet_name);
419+
} else {
420+
// TODO: move back to api call when https://github.com/rust-bitcoin/rust-bitcoincore-rpc/issues/225 is closed
421+
let args = [
422+
Value::String(wallet_name.clone()),
423+
Value::Bool(true),
424+
Value::Bool(false),
425+
Value::Null,
426+
Value::Bool(false),
427+
Value::Bool(true),
428+
];
429+
let _: Value = client.call("createwallet", &args)?;
383430
}
431+
432+
debug!("wallet created {:?}", wallet_name);
384433
}
385434

435+
let is_descriptors = is_wallet_descriptor(&client)?;
436+
386437
let blockchain_info = client.get_blockchain_info()?;
387438
let network = match blockchain_info.chain.as_str() {
388439
"main" => Network::Bitcoin,
@@ -399,7 +450,6 @@ impl ConfigurableBlockchain for RpcBlockchain {
399450
}
400451

401452
let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect();
402-
let rpc_version = client.version()?;
403453
if rpc_version >= 210_000 {
404454
let info: HashMap<String, Value> = client.call("getindexinfo", &[]).unwrap();
405455
if info.contains_key("txindex") {
@@ -416,6 +466,7 @@ impl ConfigurableBlockchain for RpcBlockchain {
416466
Ok(RpcBlockchain {
417467
client,
418468
capabilities,
469+
is_descriptors,
419470
_storage_address: storage_address,
420471
skip_blocks: config.skip_blocks,
421472
})
@@ -438,6 +489,20 @@ fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
438489
Ok(result.wallets.into_iter().map(|n| n.name).collect())
439490
}
440491

492+
/// Returns whether a wallet is legacy or descriptors by calling `getwalletinfo`.
493+
///
494+
/// This API is mapped by bitcoincore_rpc, but it doesn't have the fields we need (either
495+
/// "descriptors" or "format") so we have to call the RPC manually
496+
fn is_wallet_descriptor(client: &Client) -> Result<bool, Error> {
497+
#[derive(Deserialize)]
498+
struct CallResult {
499+
descriptors: Option<bool>,
500+
}
501+
502+
let result: CallResult = client.call("getwalletinfo", &[])?;
503+
Ok(result.descriptors.unwrap_or(false))
504+
}
505+
441506
/// Factory of [`RpcBlockchain`] instances, implements [`BlockchainFactory`]
442507
///
443508
/// Internally caches the node url and authentication params and allows getting many different [`RpcBlockchain`]
@@ -500,7 +565,7 @@ impl BlockchainFactory for RpcBlockchainFactory {
500565
}
501566

502567
#[cfg(test)]
503-
#[cfg(feature = "test-rpc")]
568+
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
504569
mod test {
505570
use super::*;
506571
use crate::testutils::blockchain_tests::TestClient;
@@ -514,7 +579,7 @@ mod test {
514579
url: test_client.bitcoind.rpc_url(),
515580
auth: Auth::Cookie { file: test_client.bitcoind.params.cookie_file.clone() },
516581
network: Network::Regtest,
517-
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
582+
wallet_name: format!("client-wallet-test-{}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos() ),
518583
skip_blocks: None,
519584
};
520585
RpcBlockchain::from_config(&config).unwrap()

src/testutils/blockchain_tests.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ macro_rules! bdk_blockchain_tests {
387387
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new()).unwrap()
388388
}
389389

390+
#[allow(dead_code)]
390391
enum WalletType {
391392
WpkhSingleSig,
392393
TaprootKeySpend,
@@ -421,7 +422,7 @@ macro_rules! bdk_blockchain_tests {
421422
let wallet = get_wallet_from_descriptors(&descriptors);
422423

423424
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
424-
#[cfg(feature = "test-rpc")]
425+
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
425426
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
426427

427428
(wallet, blockchain, descriptors, test_client)
@@ -446,7 +447,7 @@ macro_rules! bdk_blockchain_tests {
446447

447448
// the RPC blockchain needs to call `sync()` during initialization to import the
448449
// addresses (see `init_single_sig()`), so we skip this assertion
449-
#[cfg(not(feature = "test-rpc"))]
450+
#[cfg(not(any(feature = "test-rpc", feature = "test-rpc-legacy")))]
450451
assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none");
451452

452453
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
@@ -933,7 +934,7 @@ macro_rules! bdk_blockchain_tests {
933934
#[test]
934935
fn test_sync_bump_fee_add_input_no_change() {
935936
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
936-
let node_addr = test_client.get_node_address(None);
937+
let node_addr = test_client.get_node_address(None);
937938

938939
test_client.receive(testutils! {
939940
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
@@ -1021,6 +1022,7 @@ macro_rules! bdk_blockchain_tests {
10211022
}
10221023

10231024
#[test]
1025+
#[cfg(not(feature = "test-rpc-legacy"))]
10241026
fn test_send_to_bech32m_addr() {
10251027
use std::str::FromStr;
10261028
use serde;
@@ -1207,7 +1209,7 @@ macro_rules! bdk_blockchain_tests {
12071209
let blockchain = get_blockchain(&test_client);
12081210

12091211
let wallet = get_wallet_from_descriptors(&descriptors);
1210-
#[cfg(feature = "test-rpc")]
1212+
#[cfg(any(feature = "test-rpc", feature = "test-rpc-legacy"))]
12111213
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
12121214

12131215
let _ = test_client.receive(testutils! {
@@ -1231,6 +1233,7 @@ macro_rules! bdk_blockchain_tests {
12311233
}
12321234

12331235
#[test]
1236+
#[cfg(not(feature = "test-rpc-legacy"))]
12341237
fn test_taproot_key_spend() {
12351238
let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootKeySpend);
12361239

@@ -1251,6 +1254,7 @@ macro_rules! bdk_blockchain_tests {
12511254
}
12521255

12531256
#[test]
1257+
#[cfg(not(feature = "test-rpc-legacy"))]
12541258
fn test_taproot_script_spend() {
12551259
let (wallet, blockchain, descriptors, mut test_client) = init_wallet(WalletType::TaprootScriptSpend);
12561260

@@ -1279,20 +1283,24 @@ macro_rules! bdk_blockchain_tests {
12791283
}
12801284

12811285
#[test]
1286+
#[cfg(not(feature = "test-rpc-legacy"))]
12821287
fn test_sign_taproot_core_keyspend_psbt() {
12831288
test_sign_taproot_core_psbt(WalletType::TaprootKeySpend);
12841289
}
12851290

12861291
#[test]
1292+
#[cfg(not(feature = "test-rpc-legacy"))]
12871293
fn test_sign_taproot_core_scriptspend2_psbt() {
12881294
test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend2);
12891295
}
12901296

12911297
#[test]
1298+
#[cfg(not(feature = "test-rpc-legacy"))]
12921299
fn test_sign_taproot_core_scriptspend3_psbt() {
12931300
test_sign_taproot_core_psbt(WalletType::TaprootScriptSpend3);
12941301
}
12951302

1303+
#[cfg(not(feature = "test-rpc-legacy"))]
12961304
fn test_sign_taproot_core_psbt(wallet_type: WalletType) {
12971305
use std::str::FromStr;
12981306
use serde_json;

0 commit comments

Comments
 (0)