Skip to content

Commit d3232f5

Browse files
committed
fix(chain): sanitize derivation index before apply changeset
1 parent 91d7d3c commit d3232f5

File tree

8 files changed

+110
-14
lines changed

8 files changed

+110
-14
lines changed

crates/chain/src/indexed_tx_graph.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use alloc::{sync::Arc, vec::Vec};
66
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
77

88
use crate::{
9+
keychain_txout::InsertDescriptorError,
910
tx_graph::{self, TxGraph},
1011
Anchor, BlockId, Indexer, Merge, TxPosInBlock,
1112
};
@@ -46,8 +47,11 @@ impl<A, I> IndexedTxGraph<A, I> {
4647

4748
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
4849
/// Applies the [`ChangeSet`] to the [`IndexedTxGraph`].
49-
pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
50-
self.index.apply_changeset(changeset.indexer);
50+
pub fn apply_changeset(
51+
&mut self,
52+
changeset: ChangeSet<A, I::ChangeSet>,
53+
) -> Result<(), InsertDescriptorError> {
54+
self.index.apply_changeset(changeset.indexer)?;
5155

5256
for tx in &changeset.tx_graph.txs {
5357
self.index.index_tx(tx);
@@ -57,6 +61,7 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
5761
}
5862

5963
self.graph.apply_changeset(changeset.tx_graph);
64+
Ok(())
6065
}
6166

6267
/// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].

crates/chain/src/indexer.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ pub trait Indexer {
2323
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
2424

2525
/// Apply changeset to itself.
26-
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
26+
fn apply_changeset(
27+
&mut self,
28+
changeset: Self::ChangeSet,
29+
) -> Result<(), keychain_txout::InsertDescriptorError>;
2730

2831
/// Determines the [`ChangeSet`](Indexer::ChangeSet) between `self` and an empty [`Indexer`].
2932
fn initial_changeset(&self) -> Self::ChangeSet;

crates/chain/src/indexer/keychain_txout.rs

+46-5
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,17 @@ use crate::{
1010
DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator,
1111
};
1212
use alloc::{borrow::ToOwned, vec::Vec};
13-
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
13+
use bitcoin::{
14+
bip32::ChildNumber, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid,
15+
};
1416
use core::{
1517
fmt::Debug,
1618
ops::{Bound, RangeBounds},
1719
};
20+
use miniscript::{
21+
descriptor::{DescriptorXKey, Wildcard},
22+
ForEachKey,
23+
};
1824

1925
use crate::Merge;
2026

@@ -170,7 +176,7 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
170176
}
171177
}
172178

173-
fn apply_changeset(&mut self, changeset: Self::ChangeSet) {
179+
fn apply_changeset(&mut self, changeset: Self::ChangeSet) -> Result<(), InsertDescriptorError> {
174180
self.apply_changeset(changeset)
175181
}
176182

@@ -767,18 +773,38 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
767773
}
768774

769775
/// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
770-
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
776+
pub fn apply_changeset(&mut self, changeset: ChangeSet) -> Result<(), InsertDescriptorError> {
771777
for (&desc_id, &index) in &changeset.last_revealed {
778+
let descriptor = self.descriptors.get(&desc_id).expect("invariant");
779+
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards
780+
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
781+
if let DescriptorPublicKey::XPub(DescriptorXKey {
782+
derivation_path,
783+
wildcard,
784+
..
785+
}) = k
786+
{
787+
return *wildcard == Wildcard::Hardened
788+
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
789+
}
790+
791+
false
792+
});
793+
if descriptor_contains_hardened_steps {
794+
return Err(InsertDescriptorError::HardenedDerivationXpub);
795+
}
796+
descriptor.sanity_check()?;
772797
let v = self.last_revealed.entry(desc_id).or_default();
773798
*v = index.max(*v);
774799
self.replenish_inner_index_did(desc_id, self.lookahead);
775800
}
801+
Ok(())
776802
}
777803
}
778804

779-
#[derive(Clone, Debug, PartialEq)]
805+
#[derive(Debug, PartialEq)]
780806
/// Error returned from [`KeychainTxOutIndex::insert_descriptor`]
781-
pub enum InsertDescriptorError<K> {
807+
pub enum InsertDescriptorError<K = ()> {
782808
/// The descriptor has already been assigned to a keychain so you can't assign it to another
783809
DescriptorAlreadyAssigned {
784810
/// The descriptor you have attempted to reassign
@@ -793,6 +819,16 @@ pub enum InsertDescriptorError<K> {
793819
/// The descriptor that the keychain is already assigned to
794820
existing_assignment: Descriptor<DescriptorPublicKey>,
795821
},
822+
/// Miniscript error
823+
Miniscript(miniscript::Error),
824+
/// The descriptor contains hardened derivation steps on public extended keys
825+
HardenedDerivationXpub,
826+
}
827+
828+
impl From<miniscript::Error> for InsertDescriptorError {
829+
fn from(err: miniscript::Error) -> Self {
830+
InsertDescriptorError::Miniscript(err)
831+
}
796832
}
797833

798834
impl<K: core::fmt::Debug> core::fmt::Display for InsertDescriptorError<K> {
@@ -816,6 +852,11 @@ impl<K: core::fmt::Debug> core::fmt::Display for InsertDescriptorError<K> {
816852
"attempt to re-assign keychain {keychain:?} already assigned to {existing:?}"
817853
)
818854
}
855+
InsertDescriptorError::Miniscript(err) => write!(f, "Miniscript error: {}", err),
856+
InsertDescriptorError::HardenedDerivationXpub => write!(
857+
f,
858+
"The descriptor contains hardened derivation steps on public extended keys"
859+
),
819860
}
820861
}
821862
}

crates/chain/src/indexer/spk_txout.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ use crate::{
88
};
99
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
1010

11+
use super::keychain_txout::InsertDescriptorError;
12+
1113
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
1214
///
1315
/// The basic idea is that you insert script pubkeys you care about into the index with
@@ -69,8 +71,12 @@ impl<I: Clone + Ord + core::fmt::Debug> Indexer for SpkTxOutIndex<I> {
6971

7072
fn initial_changeset(&self) -> Self::ChangeSet {}
7173

72-
fn apply_changeset(&mut self, _changeset: Self::ChangeSet) {
74+
fn apply_changeset(
75+
&mut self,
76+
_changeset: Self::ChangeSet,
77+
) -> Result<(), InsertDescriptorError> {
7378
// This applies nothing.
79+
Ok(())
7480
}
7581

7682
fn is_tx_relevant(&self, tx: &Transaction) -> bool {

crates/chain/tests/test_keychain_txout_index.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,9 @@ fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
617617
.insert_descriptor(TestKeychain::External, desc.clone())
618618
.expect("must insert keychain");
619619
for changeset in changesets {
620-
indexer_a.apply_changeset(changeset.clone());
620+
indexer_a
621+
.apply_changeset(changeset.clone())
622+
.expect("must apply changeset");
621623
}
622624

623625
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
@@ -632,7 +634,9 @@ fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
632634
agg
633635
})
634636
.expect("must aggregate changesets");
635-
indexer_b.apply_changeset(aggregate_changesets);
637+
indexer_b
638+
.apply_changeset(aggregate_changesets)
639+
.expect("must apply changeset");
636640

637641
assert_eq!(
638642
indexer_a.keychains().collect::<Vec<_>>(),

crates/wallet/src/descriptor/error.rs

+27
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ pub enum Error {
4343
Hex(bitcoin::hex::HexToBytesError),
4444
/// The provided wallet descriptors are identical
4545
ExternalAndInternalAreTheSame,
46+
/// Descriptor has already been assigned to a keychain so you can't assign it to another
47+
DescriptorAlreadyAssigned,
48+
/// The keychain is already assigned to a descriptor so you can't reassign it
49+
KeychainAlreadyAssigned,
4650
}
4751

4852
impl From<crate::keys::KeyError> for Error {
@@ -83,6 +87,12 @@ impl fmt::Display for Error {
8387
Self::ExternalAndInternalAreTheSame => {
8488
write!(f, "External and internal descriptors are the same")
8589
}
90+
Self::DescriptorAlreadyAssigned => {
91+
write!(f, "attempt to re-assign descriptor already assigned")
92+
}
93+
Self::KeychainAlreadyAssigned => {
94+
write!(f, "attempt to re-assign keychain already assigned")
95+
}
8696
}
8797
}
8898
}
@@ -125,3 +135,20 @@ impl From<crate::descriptor::policy::PolicyError> for Error {
125135
Error::Policy(err)
126136
}
127137
}
138+
139+
impl From<chain::keychain_txout::InsertDescriptorError> for Error {
140+
fn from(err: chain::keychain_txout::InsertDescriptorError) -> Self {
141+
match err {
142+
chain::keychain_txout::InsertDescriptorError::DescriptorAlreadyAssigned { .. } => {
143+
Error::DescriptorAlreadyAssigned
144+
}
145+
chain::keychain_txout::InsertDescriptorError::KeychainAlreadyAssigned { .. } => {
146+
Error::KeychainAlreadyAssigned
147+
}
148+
chain::keychain_txout::InsertDescriptorError::Miniscript(err) => Error::Miniscript(err),
149+
chain::keychain_txout::InsertDescriptorError::HardenedDerivationXpub => {
150+
Error::HardenedDerivationXpub
151+
}
152+
}
153+
}
154+
}

crates/wallet/src/wallet/mod.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -608,8 +608,12 @@ impl Wallet {
608608
.map_err(LoadError::Descriptor)?;
609609

610610
let mut indexed_graph = IndexedTxGraph::new(index);
611-
indexed_graph.apply_changeset(changeset.indexer.into());
612-
indexed_graph.apply_changeset(changeset.tx_graph.into());
611+
indexed_graph
612+
.apply_changeset(changeset.indexer.into())
613+
.map_err(|err| LoadError::Descriptor(err.into()))?;
614+
indexed_graph
615+
.apply_changeset(changeset.tx_graph.into())
616+
.map_err(|err| LoadError::Descriptor(err.into()))?;
613617

614618
let stage = ChangeSet::default();
615619

@@ -2530,6 +2534,12 @@ fn create_indexer(
25302534
InsertDescriptorError::KeychainAlreadyAssigned { .. } => {
25312535
unreachable!("this is the first time we're assigning internal")
25322536
}
2537+
InsertDescriptorError::Miniscript(err) => {
2538+
crate::descriptor::error::Error::Miniscript(err)
2539+
}
2540+
InsertDescriptorError::HardenedDerivationXpub => {
2541+
crate::descriptor::error::Error::HardenedDerivationXpub
2542+
}
25332543
}
25342544
})?);
25352545
}

example-crates/example_cli/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -827,7 +827,7 @@ pub fn init_or_load<CS: clap::Subcommand, S: clap::Args>(
827827
graph.apply_changeset(indexed_tx_graph::ChangeSet {
828828
tx_graph: changeset.tx_graph,
829829
indexer: changeset.indexer,
830-
});
830+
})?;
831831
graph
832832
});
833833

0 commit comments

Comments
 (0)