Skip to content

Commit a3f087c

Browse files
committed
fix(chain): handle changeset where the index is greater that BIP32_MAX_INDEX
1 parent 2582e02 commit a3f087c

File tree

8 files changed

+68
-12
lines changed

8 files changed

+68
-12
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

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! [`Indexer`] provides utilities for indexing transaction data.
22
33
use bitcoin::{OutPoint, Transaction, TxOut};
4+
use keychain_txout::InsertDescriptorError;
45

56
#[cfg(feature = "miniscript")]
67
pub mod keychain_txout;
@@ -23,7 +24,7 @@ pub trait Indexer {
2324
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
2425

2526
/// Apply changeset to itself.
26-
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
27+
fn apply_changeset(&mut self, changeset: Self::ChangeSet) -> Result<(), InsertDescriptorError>;
2728

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

crates/chain/src/indexer/keychain_txout.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
176176
}
177177
}
178178

179-
fn apply_changeset(&mut self, changeset: Self::ChangeSet) {
179+
fn apply_changeset(&mut self, changeset: Self::ChangeSet) -> Result<(), InsertDescriptorError> {
180180
self.apply_changeset(changeset)
181181
}
182182

@@ -790,18 +790,22 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
790790
}
791791

792792
/// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
793-
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
793+
pub fn apply_changeset(&mut self, changeset: ChangeSet) -> Result<(), InsertDescriptorError> {
794794
for (&desc_id, &index) in &changeset.last_revealed {
795795
let v = self.last_revealed.entry(desc_id).or_default();
796+
if index > BIP32_MAX_INDEX {
797+
return Err(InsertDescriptorError::OutOfBounds);
798+
}
796799
*v = index.max(*v);
797800
self.replenish_inner_index_did(desc_id, self.lookahead);
798801
}
802+
Ok(())
799803
}
800804
}
801805

802806
#[derive(Debug, PartialEq)]
803807
/// Error returned from [`KeychainTxOutIndex::insert_descriptor`]
804-
pub enum InsertDescriptorError<K> {
808+
pub enum InsertDescriptorError<K = ()> {
805809
/// The descriptor has already been assigned to a keychain so you can't assign it to another
806810
DescriptorAlreadyAssigned {
807811
/// The descriptor you have attempted to reassign
@@ -820,6 +824,8 @@ pub enum InsertDescriptorError<K> {
820824
Miniscript(miniscript::Error),
821825
/// The descriptor contains hardened derivation steps on public extended keys
822826
HardenedDerivationXpub,
827+
/// The last revealed index of derivation is out of bounds
828+
OutOfBounds,
823829
}
824830

825831
impl<K> From<miniscript::Error> for InsertDescriptorError<K> {
@@ -854,6 +860,9 @@ impl<K: core::fmt::Debug> core::fmt::Display for InsertDescriptorError<K> {
854860
f,
855861
"The descriptor contains hardened derivation steps on public extended keys"
856862
),
863+
InsertDescriptorError::OutOfBounds => {
864+
write!(f, "The last revealed index of derivation is out of bounds")
865+
}
857866
}
858867
}
859868
}

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

+17-2
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,17 @@ fn insert_descriptor_should_reject_hardened_steps() {
613613
assert!(indexer.insert_descriptor("keychain", desc).is_err())
614614
}
615615

616+
#[test]
617+
fn apply_changesets_should_reject_invalid_index() {
618+
let desc = parse_descriptor(DESCRIPTORS[3]);
619+
let changeset: ChangeSet = ChangeSet {
620+
last_revealed: [(desc.descriptor_id(), bdk_chain::BIP32_MAX_INDEX + 1)].into(),
621+
};
622+
623+
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
624+
assert!(indexer_a.apply_changeset(changeset.clone()).is_err())
625+
}
626+
616627
#[test]
617628
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
618629
let desc = parse_descriptor(DESCRIPTORS[0]);
@@ -630,7 +641,9 @@ fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
630641
.insert_descriptor(TestKeychain::External, desc.clone())
631642
.expect("must insert keychain");
632643
for changeset in changesets {
633-
indexer_a.apply_changeset(changeset.clone());
644+
indexer_a
645+
.apply_changeset(changeset.clone())
646+
.expect("must apply_changeset");
634647
}
635648

636649
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
@@ -645,7 +658,9 @@ fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
645658
agg
646659
})
647660
.expect("must aggregate changesets");
648-
indexer_b.apply_changeset(aggregate_changesets);
661+
indexer_b
662+
.apply_changeset(aggregate_changesets)
663+
.expect("must apply_changeset");
649664

650665
assert_eq!(
651666
indexer_a.keychains().collect::<Vec<_>>(),

crates/wallet/src/descriptor/error.rs

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ pub enum Error {
4343
Hex(bitcoin::hex::HexToBytesError),
4444
/// The provided wallet descriptors are identical
4545
ExternalAndInternalAreTheSame,
46+
/// The last revealed index of derivation is out of bounds
47+
IndexDescriptorOutOfBounds,
4648
}
4749

4850
impl From<crate::keys::KeyError> for Error {
@@ -83,6 +85,9 @@ impl fmt::Display for Error {
8385
Self::ExternalAndInternalAreTheSame => {
8486
write!(f, "External and internal descriptors are the same")
8587
}
88+
Self::IndexDescriptorOutOfBounds => {
89+
write!(f, "The last revealed index of derivation is out of bounds")
90+
}
8691
}
8792
}
8893
}

crates/wallet/src/wallet/mod.rs

+17-2
Original file line numberDiff line numberDiff line change
@@ -608,8 +608,20 @@ 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(|_| {
614+
LoadError::Descriptor(
615+
crate::descriptor::DescriptorError::IndexDescriptorOutOfBounds,
616+
)
617+
})?;
618+
indexed_graph
619+
.apply_changeset(changeset.tx_graph.into())
620+
.map_err(|_| {
621+
LoadError::Descriptor(
622+
crate::descriptor::DescriptorError::IndexDescriptorOutOfBounds,
623+
)
624+
})?;
613625

614626
let stage = ChangeSet::default();
615627

@@ -2536,6 +2548,9 @@ fn create_indexer(
25362548
InsertDescriptorError::HardenedDerivationXpub => {
25372549
crate::descriptor::error::Error::HardenedDerivationXpub
25382550
}
2551+
InsertDescriptorError::OutOfBounds => {
2552+
crate::descriptor::error::Error::IndexDescriptorOutOfBounds
2553+
}
25392554
}
25402555
})?);
25412556
}

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)