Skip to content

Commit a1408ef

Browse files
committed
Merge #848: feat: implement xpub network consistency check
497e994 feat: implement xpub network consistency check (nahem) Pull request description: ## Description This PR addresses issue #844 by implementing a network consistency check for extended public keys (xpubs) in descriptors. The change introduces an `XKeyNetwork` enum and a `xkey_network()` method on `Descriptor<DescriptorPublicKey>` and `Descriptor<DefiniteDescriptorKey>` to: - Detect the presence of xpubs. - Identify if they belong to a single network (e.g., mainnet or testnet). - Flag cases where xpubs have mixed networks, which could lead to unsafe usage (e.g., sending mainnet funds to testnet addresses). The implementation iterates over keys to determine consistency, returning `XKeyNetwork::NoXKeys`, `XKeyNetwork::Mixed`, or `XKeyNetwork::Single(Network)`. ## Changes - Added `XKeyNetwork` enum to represent network states. - Implemented `xkey_network()` method. ## Testing - Added unit tests to verify behavior with single-network, mixed-network, and no-xpub descriptors. - Tested against existing descriptor examples to ensure compatibility. ## Notes - This is a safety feature and does not enforce network consistency at parse time by default. ## Related Issues - Closes #844 ACKs for top commit: apoelstra: ACK 497e994; successfully ran local tests Tree-SHA512: 5e4aa1ecf63201b45e4f8003427cfb7c178074416dcbe79d9cc39fca5e327a48c74f10d1daaad1ad8fbc99a09e7e76f30867a29bb111a0ced0db907ba73f69c6
2 parents 7ba28c0 + 497e994 commit a1408ef

File tree

2 files changed

+326
-1
lines changed

2 files changed

+326
-1
lines changed

src/descriptor/key.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use bitcoin::bip32::{self, XKeyIdentifier};
1010
use bitcoin::hashes::{hash160, ripemd160, sha256, Hash, HashEngine};
1111
use bitcoin::key::{PublicKey, XOnlyPublicKey};
1212
use bitcoin::secp256k1::{Secp256k1, Signing, Verification};
13+
use bitcoin::NetworkKind;
1314

1415
use crate::prelude::*;
1516
#[cfg(feature = "serde")]
@@ -116,6 +117,17 @@ pub enum SinglePubKey {
116117
#[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Hash)]
117118
pub struct DefiniteDescriptorKey(DescriptorPublicKey);
118119

120+
/// Network information extracted from extended keys in a descriptor.
121+
#[derive(Debug, Eq, PartialEq, Clone)]
122+
pub enum XKeyNetwork {
123+
/// No extended keys are present in the descriptor.
124+
NoXKeys,
125+
/// Extended keys are present but have conflicting network prefixes.
126+
Mixed,
127+
/// Extended key(s) are present and all have the same network prefix.
128+
Single(NetworkKind),
129+
}
130+
119131
impl fmt::Display for DescriptorSecretKey {
120132
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
121133
match self {
@@ -889,6 +901,17 @@ impl DescriptorPublicKey {
889901
}
890902
}
891903
}
904+
905+
/// Get the network of this key, if it's an extended key.
906+
///
907+
/// Returns `None` for single keys (non-extended keys), `Some(NetworkKind)` for extended keys.
908+
pub fn xkey_network(&self) -> Option<NetworkKind> {
909+
match self {
910+
DescriptorPublicKey::Single(_) => None,
911+
DescriptorPublicKey::XPub(xpub) => Some(xpub.xkey.network),
912+
DescriptorPublicKey::MultiXPub(multi_xpub) => Some(multi_xpub.xkey.network),
913+
}
914+
}
892915
}
893916

894917
impl FromStr for DescriptorSecretKey {
@@ -1780,4 +1803,27 @@ mod test {
17801803
Err(NonDefiniteKeyError::HardenedStep)
17811804
));
17821805
}
1806+
1807+
#[test]
1808+
fn test_xkey_network() {
1809+
use bitcoin::NetworkKind;
1810+
1811+
// Test mainnet xpub
1812+
let mainnet_xpub = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8"
1813+
.parse::<DescriptorPublicKey>()
1814+
.unwrap();
1815+
assert_eq!(mainnet_xpub.xkey_network(), Some(NetworkKind::Main));
1816+
1817+
// Test testnet xpub
1818+
let testnet_xpub = "tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi"
1819+
.parse::<DescriptorPublicKey>()
1820+
.unwrap();
1821+
assert_eq!(testnet_xpub.xkey_network(), Some(NetworkKind::Test));
1822+
1823+
// Test single public key (no extended key)
1824+
let single_key = "021d4ea7132d4e1a362ee5efd8d0b59dd4d1fe8906eefa7dd812b05a46b73d829b"
1825+
.parse::<DescriptorPublicKey>()
1826+
.unwrap();
1827+
assert_eq!(single_key.xkey_network(), None);
1828+
}
17831829
}

src/descriptor/mod.rs

Lines changed: 280 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ mod key;
5555
pub use self::key::{
5656
DefiniteDescriptorKey, DerivPaths, DescriptorKeyParseError, DescriptorMultiXKey,
5757
DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, InnerXKey, MalformedKeyDataKind,
58-
NonDefiniteKeyError, SinglePriv, SinglePub, SinglePubKey, Wildcard,
58+
NonDefiniteKeyError, SinglePriv, SinglePub, SinglePubKey, Wildcard, XKeyNetwork,
5959
};
6060

6161
/// Alias type for a map of public key to secret key
@@ -927,6 +927,33 @@ impl Descriptor<DescriptorPublicKey> {
927927

928928
Ok(descriptors)
929929
}
930+
931+
/// Check the network consistency of all extended keys in this descriptor.
932+
///
933+
/// Returns `XKeyNetwork::NoXKeys` if no extended keys are present,
934+
/// `XKeyNetwork::Mixed` if extended keys have conflicting network prefixes,
935+
/// or `XKeyNetwork::Single(network)` if all extended keys have the same network.
936+
///
937+
/// This can be used to prevent accidentally using testnet keys on mainnet
938+
/// and vice versa, which could lead to funds being sent to unspendable addresses.
939+
pub fn xkey_network(&self) -> XKeyNetwork {
940+
let mut first_network = None;
941+
942+
for key in self.iter_pk() {
943+
if let Some(network) = key.xkey_network() {
944+
match first_network {
945+
None => first_network = Some(network),
946+
Some(ref n) if *n != network => return XKeyNetwork::Mixed,
947+
_ => continue,
948+
}
949+
}
950+
}
951+
952+
match first_network {
953+
Some(network) => XKeyNetwork::Single(network),
954+
None => XKeyNetwork::NoXKeys,
955+
}
956+
}
930957
}
931958

932959
impl Descriptor<DefiniteDescriptorKey> {
@@ -977,6 +1004,33 @@ impl Descriptor<DefiniteDescriptorKey> {
9771004
Err(e) => panic!("Context errors when deriving keys: {}", e.into_outer_err()),
9781005
}
9791006
}
1007+
1008+
/// Check the network consistency of all extended keys in this descriptor.
1009+
///
1010+
/// Returns `XKeyNetwork::NoXKeys` if no extended keys are present,
1011+
/// `XKeyNetwork::Mixed` if extended keys have conflicting network prefixes,
1012+
/// or `XKeyNetwork::Single(network)` if all extended keys have the same network.
1013+
///
1014+
/// This can be used to prevent accidentally using testnet keys on mainnet
1015+
/// and vice versa, which could lead to funds being sent to unspendable addresses.
1016+
pub fn xkey_network(&self) -> XKeyNetwork {
1017+
let mut first_network = None;
1018+
1019+
for key in self.iter_pk() {
1020+
if let Some(network) = key.as_descriptor_public_key().xkey_network() {
1021+
match first_network {
1022+
None => first_network = Some(network),
1023+
Some(ref n) if *n != network => return XKeyNetwork::Mixed,
1024+
_ => continue,
1025+
}
1026+
}
1027+
}
1028+
1029+
match first_network {
1030+
Some(network) => XKeyNetwork::Single(network),
1031+
None => XKeyNetwork::NoXKeys,
1032+
}
1033+
}
9801034
}
9811035

9821036
impl<Pk: FromStrKey> crate::expression::FromTree for Descriptor<Pk> {
@@ -2421,4 +2475,229 @@ pk(03f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa8))";
24212475
assert!(keys[1..].iter().any(|k| k.to_string()
24222476
== "0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352"));
24232477
}
2478+
2479+
// Helper function to provide unique test keys for multi-key scenarios
2480+
fn test_keys() -> (Vec<&'static str>, Vec<&'static str>, Vec<&'static str>) {
2481+
// Unique mainnet xpubs
2482+
let mainnet_xpubs = vec![
2483+
"xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
2484+
"xpub6ASuArnXKPbfEwhqN6e3mwBcDTgzisQN1wXN9BJcM47sSikHjJf3UFHKkNAWbWMiGj7Wf5uMash7SyYq527Hqck2AxYysAA7xmALppuCkwQ",
2485+
"xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB",
2486+
"xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH",
2487+
];
2488+
2489+
// Unique testnet tpubs
2490+
let testnet_tpubs = vec![
2491+
"tpubDBrgjcxBxnXyL575sHdkpKohWu5qHKoQ7TJXKNrYznh5fVEGBv89hA8ENW7A8MFVpFUSvgLqc4Nj1WZcpePX6rrxviVtPowvMuGF5rdT2Vi",
2492+
"tpubD6NzVbkrYhZ4WQdzxL7NmJN7b85ePo4p6RSj9QQHF7te2RR9iUeVSGgnGkoUsB9LBRosgvNbjRv9bcsJgzgBd7QKuxDm23ZewkTRzNSLEDr",
2493+
"tpubD6NzVbkrYhZ4YqYr3amYH15zjxHvBkUUeadieW8AxTZC7aY2L8aPSk3tpW6yW1QnWzXAB7zoiaNMfwXPPz9S68ZCV4yWvkVXjdeksLskCed",
2494+
"tpubDCvNhURocXGZsLNqWcqD3syHTqPXrMSTwi8feKVwAcpi29oYKsDD3Vex7x2TDneKMVN23RbLprfxB69v94iYqdaYHsVz3kPR37NQXeqouVz",
2495+
];
2496+
2497+
// Unique single public keys
2498+
let single_keys = vec![
2499+
"021d4ea7132d4e1a362ee5efd8d0b59dd4d1fe8906eefa7dd812b05a46b73d829b",
2500+
"025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357",
2501+
"022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01",
2502+
"023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb",
2503+
];
2504+
2505+
(mainnet_xpubs, testnet_tpubs, single_keys)
2506+
}
2507+
2508+
#[test]
2509+
fn test_descriptor_pubkey_xkey_network() {
2510+
use core::str::FromStr;
2511+
2512+
use bitcoin::NetworkKind;
2513+
2514+
use crate::descriptor::{DescriptorPublicKey, XKeyNetwork};
2515+
2516+
let (mainnet_xpubs, testnet_tpubs, single_keys) = test_keys();
2517+
2518+
// Basic single key scenarios
2519+
let basic_tests = vec![
2520+
// Single mainnet xpub
2521+
(format!("wpkh({})", mainnet_xpubs[0]), XKeyNetwork::Single(NetworkKind::Main)),
2522+
// Single testnet tpub
2523+
(format!("wpkh({})", testnet_tpubs[0]), XKeyNetwork::Single(NetworkKind::Test)),
2524+
// Single public key (no extended keys)
2525+
(format!("wpkh({})", single_keys[0]), XKeyNetwork::NoXKeys),
2526+
];
2527+
2528+
for (desc_str, expected) in basic_tests {
2529+
let desc = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
2530+
assert_eq!(desc.xkey_network(), expected, "Failed for basic descriptor: {}", desc_str);
2531+
}
2532+
2533+
// Multi-key descriptor combinations with unique keys
2534+
let multi_key_tests = vec![
2535+
// Mixed networks: mainnet + testnet
2536+
(
2537+
format!("wsh(multi(2,{},{}))", mainnet_xpubs[0], testnet_tpubs[0]),
2538+
XKeyNetwork::Mixed,
2539+
),
2540+
// Consistent mainnet keys
2541+
(
2542+
format!("wsh(multi(2,{},{}))", mainnet_xpubs[0], mainnet_xpubs[1]),
2543+
XKeyNetwork::Single(NetworkKind::Main),
2544+
),
2545+
// Consistent testnet multisig
2546+
(
2547+
format!(
2548+
"wsh(multi(2,{},{},{}))",
2549+
testnet_tpubs[0], testnet_tpubs[1], testnet_tpubs[2]
2550+
),
2551+
XKeyNetwork::Single(NetworkKind::Test),
2552+
),
2553+
// Sorted multisig with mixed key types
2554+
(
2555+
format!(
2556+
"wsh(sortedmulti(2,{},{},{}))",
2557+
mainnet_xpubs[0], testnet_tpubs[0], single_keys[0]
2558+
),
2559+
XKeyNetwork::Mixed,
2560+
),
2561+
// 3-of-4 multisig with all mainnet keys
2562+
(
2563+
format!(
2564+
"wsh(multi(3,{},{},{},{}))",
2565+
mainnet_xpubs[0], mainnet_xpubs[1], mainnet_xpubs[2], mainnet_xpubs[3]
2566+
),
2567+
XKeyNetwork::Single(NetworkKind::Main),
2568+
),
2569+
];
2570+
2571+
for (desc_str, expected) in multi_key_tests {
2572+
let desc = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
2573+
assert_eq!(
2574+
desc.xkey_network(),
2575+
expected,
2576+
"Failed for multi-key descriptor: {}",
2577+
desc_str
2578+
);
2579+
}
2580+
2581+
// Threshold and logical operator tests
2582+
let threshold_tests = vec![
2583+
// Threshold with mixed key types
2584+
(
2585+
format!(
2586+
"wsh(thresh(2,c:pk_k({}),sc:pk_k({}),sc:pk_k({})))",
2587+
single_keys[0], mainnet_xpubs[0], testnet_tpubs[0]
2588+
),
2589+
XKeyNetwork::Mixed,
2590+
),
2591+
// OR with mixed networks
2592+
(
2593+
format!("wsh(or_d(pk({}),pk({})))", mainnet_xpubs[0], testnet_tpubs[0]),
2594+
XKeyNetwork::Mixed,
2595+
),
2596+
// AND with consistent mainnet keys
2597+
(
2598+
format!("wsh(and_v(v:pk({}),pk({})))", mainnet_xpubs[0], mainnet_xpubs[1]),
2599+
XKeyNetwork::Single(NetworkKind::Main),
2600+
),
2601+
// Complex threshold with all testnet keys
2602+
(
2603+
format!(
2604+
"wsh(thresh(3,c:pk_k({}),sc:pk_k({}),sc:pk_k({}),sc:pk_k({})))",
2605+
testnet_tpubs[0], testnet_tpubs[1], testnet_tpubs[2], testnet_tpubs[3]
2606+
),
2607+
XKeyNetwork::Single(NetworkKind::Test),
2608+
),
2609+
];
2610+
2611+
for (desc_str, expected) in threshold_tests {
2612+
let desc = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
2613+
assert_eq!(
2614+
desc.xkey_network(),
2615+
expected,
2616+
"Failed for threshold descriptor: {}",
2617+
desc_str
2618+
);
2619+
}
2620+
2621+
// Taproot and complex miniscript tests
2622+
let complex_tests = vec![
2623+
// Taproot with mixed networks
2624+
(format!("tr({},pk({}))", mainnet_xpubs[0], testnet_tpubs[0]), XKeyNetwork::Mixed),
2625+
// Taproot with consistent mainnet keys
2626+
(format!("tr({},pk({}))", mainnet_xpubs[0], mainnet_xpubs[1]), XKeyNetwork::Single(NetworkKind::Main)),
2627+
// HTLC-like pattern with mixed networks
2628+
(format!("wsh(andor(pk({}),sha256(1111111111111111111111111111111111111111111111111111111111111111),and_v(v:pkh({}),older(144))))", mainnet_xpubs[0], testnet_tpubs[0]), XKeyNetwork::Mixed),
2629+
// Multi-path spending with testnet keys
2630+
(format!("wsh(or_d(multi(2,{},{}),and_v(v:pk({}),older(1000))))", testnet_tpubs[0], testnet_tpubs[1], testnet_tpubs[2]), XKeyNetwork::Single(NetworkKind::Test)),
2631+
// Nested conditions with only single keys
2632+
(format!("wsh(thresh(3,c:pk_k({}),sc:pk_k({}),sc:pk_k({}),sc:pk_k({})))", single_keys[0], single_keys[1], single_keys[2], single_keys[3]), XKeyNetwork::NoXKeys),
2633+
// Complex pattern with mainnet keys
2634+
(format!("wsh(or_d(multi(2,{},{}),and_v(v:pk({}),older(1000))))", mainnet_xpubs[0], mainnet_xpubs[1], mainnet_xpubs[2]), XKeyNetwork::Single(NetworkKind::Main)),
2635+
];
2636+
2637+
for (desc_str, expected) in complex_tests {
2638+
let desc = Descriptor::<DescriptorPublicKey>::from_str(&desc_str).unwrap();
2639+
assert_eq!(
2640+
desc.xkey_network(),
2641+
expected,
2642+
"Failed for complex descriptor: {}",
2643+
desc_str
2644+
);
2645+
}
2646+
}
2647+
2648+
#[test]
2649+
fn test_definite_descriptor_key_xkey_network() {
2650+
use core::str::FromStr;
2651+
2652+
use bitcoin::NetworkKind;
2653+
2654+
use crate::descriptor::{DefiniteDescriptorKey, XKeyNetwork};
2655+
2656+
let (mainnet_xpubs, testnet_tpubs, single_keys) = test_keys();
2657+
2658+
// DefiniteDescriptorKey tests (no wildcards, specific derivation paths)
2659+
let definite_key_tests = vec![
2660+
// Basic single key scenarios
2661+
(format!("wpkh({})", mainnet_xpubs[0]), XKeyNetwork::Single(NetworkKind::Main)),
2662+
(format!("wpkh({})", testnet_tpubs[0]), XKeyNetwork::Single(NetworkKind::Test)),
2663+
(format!("wpkh({})", single_keys[0]), XKeyNetwork::NoXKeys),
2664+
// Multi-key scenarios with specific derivation paths
2665+
(
2666+
format!("wsh(multi(2,{},{}))", mainnet_xpubs[0], testnet_tpubs[0]),
2667+
XKeyNetwork::Mixed,
2668+
),
2669+
(
2670+
format!("wsh(multi(2,{}/0,{}/1))", testnet_tpubs[0], testnet_tpubs[1]),
2671+
XKeyNetwork::Single(NetworkKind::Test),
2672+
),
2673+
(
2674+
format!("wsh(multi(2,{}/0,{}/1))", mainnet_xpubs[0], mainnet_xpubs[1]),
2675+
XKeyNetwork::Single(NetworkKind::Main),
2676+
),
2677+
// Sorted multisig with specific paths
2678+
(
2679+
format!(
2680+
"wsh(sortedmulti(2,{}/0,{}/1,{}))",
2681+
mainnet_xpubs[0], testnet_tpubs[0], single_keys[0]
2682+
),
2683+
XKeyNetwork::Mixed,
2684+
),
2685+
// Taproot scenarios
2686+
(format!("tr({})", mainnet_xpubs[0]), XKeyNetwork::Single(NetworkKind::Main)),
2687+
(
2688+
format!("tr({},pk({}/0))", mainnet_xpubs[0], testnet_tpubs[0]),
2689+
XKeyNetwork::Mixed,
2690+
),
2691+
];
2692+
2693+
for (desc_str, expected) in definite_key_tests {
2694+
let desc = Descriptor::<DefiniteDescriptorKey>::from_str(&desc_str).unwrap();
2695+
assert_eq!(
2696+
desc.xkey_network(),
2697+
expected,
2698+
"Failed for DefiniteDescriptorKey: {}",
2699+
desc_str
2700+
);
2701+
}
2702+
}
24242703
}

0 commit comments

Comments
 (0)