Skip to content

Commit

Permalink
feat: add Wallet Descriptor Support for Transaction Indexing
Browse files Browse the repository at this point in the history
This introduces support for wallet descriptors.
Descriptors provide a compact and semi-standardized method for
describing how scripts and addresses within a wallet are generated[1].

Chainhooks users that want to track addresses derived from an extended
pubkey or a multisig-wallet for example, can now rely on this feature
instead of defining one predicate per address.

For example if we wanted to track the first 3 addressed generated by
the following descriptor:

```
wpkh(tprv8ZgxMBicQKsPePxn6j3TjvB2MBzQkuhGgc6oRh2WZancZQgxktcnjZJ44XdsRiw3jNkbVTK9JW6KFHvnRKgAMtSyuBevMJprSkZ4PTfmTgV/84'/1'/0'/0/*)
```

which reads: describe a P2WPKH output with the specified extended public
key, and produces these BIP84 addresses:

```
bcrt1qzy2rdyvu8c57qd8exyyp0mw7dk5drsu9ewzdsu
bcrt1qsfsjnagr29m8h3a3vdand2s85cg4cefkcwk2fy
bcrt1qauewfytqe5mtr0xwp786r6fl39kmum2lr65kmj
```

we could use the following predicate:

```json
...
  "networks": {
    "regtest": {
      "if_this": {
        "scope": "outputs",
        "descriptor": {
          "expression": "wpkh(tprv8ZgxMBicQKsPePxn6j3TjvB2MBzQkuhGgc6oRh2WZancZQgxktcnjZJ44XdsRiw3jNkbVTK9JW6KFHvnRKgAMtSyuBevMJprSkZ4PTfmTgV/84'/1'/0'/0/*)",
          "range": [0,3]
        }
      },
      "then_that": {
        "file_append": {
          "path": "txns.txt"
        }
      }
    }
  }
...

```

1: https://bitcoindevkit.org/descriptors/
  • Loading branch information
qustavo committed Sep 2, 2023
1 parent 59040eb commit b094ac1
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 9 deletions.
75 changes: 70 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions components/chainhook-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ dashmap = "5.4.0"
fxhash = "0.2.1"
lazy_static = "1.4.0"
regex = "1.9.3"
miniscript = "10.0.0"
bitcoin = "0.30.1"

[dev-dependencies]
test-case = "3.1.0"
Expand Down
50 changes: 47 additions & 3 deletions components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use super::types::{
BitcoinChainhookSpecification, BitcoinPredicateType, ExactMatchingRule, HookAction,
InputPredicate, MatchingRule, OrdinalOperations, OutputPredicate, StacksOperations,
BitcoinChainhookSpecification, BitcoinPredicateType, ExactMatchingRule, DescriptorMatchingRule,
HookAction, InputPredicate, MatchingRule, OrdinalOperations, OutputPredicate, StacksOperations,
};
use crate::utils::Context;

Expand All @@ -11,6 +11,11 @@ use chainhook_types::{
StacksBaseChainOperation, TransactionIdentifier,
};

use hiro_system_kit::slog;

use miniscript::bitcoin::secp256k1::Secp256k1;
use miniscript::Descriptor;

use reqwest::{Client, Method};
use serde_json::Value as JsonValue;
use std::collections::{BTreeMap, HashMap};
Expand Down Expand Up @@ -284,7 +289,7 @@ impl BitcoinPredicateType {
pub fn evaluate_transaction_predicate(
&self,
tx: &BitcoinTransactionData,
_ctx: &Context,
ctx: &Context,
) -> bool {
// TODO(lgalabru): follow-up on this implementation
match &self {
Expand Down Expand Up @@ -364,6 +369,45 @@ impl BitcoinPredicateType {
}
false
}
BitcoinPredicateType::Outputs(OutputPredicate::Descriptor(DescriptorMatchingRule{
expression, range,
})) => {
// To derive from descriptors, we need to provide a secp context.
let (sig, ver) = (&Secp256k1::signing_only(), &Secp256k1::verification_only());
let (desc, _) = Descriptor::parse_descriptor(&sig, expression).unwrap();

// If the descriptor is derivable (`has_wildcard()`), we rely on the `range` field
// defined by the predicate OR fallback to a default range of [0,5] when not set.
// When the descriptor is not derivable we force to create a unique iteration by
// ranging over [0,1].
let range = if desc.has_wildcard() {
range.unwrap_or([0,5])
} else {
[0,1]
};

// Derive the addresses and try to match them against the outputs.
for i in range[0]..range[1] {
let derived = desc.derived_descriptor(&ver, i).unwrap();

// Extract and encode the derived pubkey.
let script_pubkey = hex::encode(derived.script_pubkey().as_bytes());

// Match that script against the tx outputs.
for (index, output) in tx.metadata.outputs.iter().enumerate() {
if output.script_pubkey[2..] == script_pubkey {
ctx.try_log(|logger| slog::debug!(logger,
"Descriptor: Matched pubkey {:?} on tx {:?} output {}",
script_pubkey, tx.transaction_identifier.get_hash_bytes_str(), index,
));

return true;
}
}
}

false
}
BitcoinPredicateType::Inputs(InputPredicate::Txid(predicate)) => {
// TODO(lgalabru): add support for transaction chainhing, if enabled
for input in tx.metadata.inputs.iter() {
Expand Down
25 changes: 24 additions & 1 deletion components/chainhook-sdk/src/chainhooks/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::collections::BTreeMap;
use chainhook_types::{BitcoinNetwork, StacksNetwork};
use reqwest::Url;
use serde::ser::{SerializeSeq, Serializer};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, de};
use serde_json::Value as JsonValue;

use schemars::JsonSchema;
Expand Down Expand Up @@ -539,6 +539,7 @@ pub enum OutputPredicate {
P2sh(ExactMatchingRule),
P2wpkh(ExactMatchingRule),
P2wsh(ExactMatchingRule),
Descriptor(DescriptorMatchingRule),
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
Expand Down Expand Up @@ -689,6 +690,28 @@ pub enum ExactMatchingRule {
Equals(String),
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct DescriptorMatchingRule{
// expression defines the bitcoin descriptor.
pub expression: String,
#[serde(default, deserialize_with = "deserialize_descriptor_range")]
pub range: Option<[u32; 2]>,
}

// deserialize_descriptor_range makes sure that the range value is valid.
fn deserialize_descriptor_range<'de, D>(deserializer: D) -> Result<Option<[u32; 2]>, D::Error>
where
D: Deserializer<'de>,
{
let range: [u32;2] = Deserialize::deserialize(deserializer)?;
if !(range[0] < range[1]) {
Err(de::Error::custom("First element of 'range' must be lower than the second element"))
} else {
Ok(Some(range))
}
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum BlockIdentifierHashRule {
Expand Down

0 comments on commit b094ac1

Please sign in to comment.