Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: op_return predicate evaluation #416

Merged
merged 3 commits into from
Oct 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 64 additions & 24 deletions components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ use std::str::FromStr;

use reqwest::RequestBuilder;

use hex::FromHex;

pub struct BitcoinTriggerChainhook<'a> {
pub chainhook: &'a BitcoinChainhookSpecification,
pub apply: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>,
Expand Down Expand Up @@ -301,6 +303,25 @@ pub fn handle_bitcoin_hook_action<'a>(
}
}

struct OpReturn(String);
impl OpReturn {
fn from_string(hex: &String) -> Result<String, String> {
// Remove the `0x` prefix if present so that we can call from_hex without errors.
let hex = hex.strip_prefix("0x").unwrap_or(hex);

// Parse the hex bytes.
let bytes = Vec::<u8>::from_hex(hex).unwrap();
match bytes.as_slice() {
// An OpReturn is composed by:
// - OP_RETURN 0x6a
// - Data length <N> (ignored)
// - The data
[0x6a, _, rest @ ..] => Ok(hex::encode(rest)),
_ => Err(String::from("not an OP_RETURN")),
}
}
}

impl BitcoinPredicateType {
pub fn evaluate_transaction_predicate(
&self,
Expand All @@ -313,32 +334,48 @@ impl BitcoinPredicateType {
BitcoinPredicateType::Txid(ExactMatchingRule::Equals(txid)) => {
tx.transaction_identifier.hash.eq(txid)
}
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::Equals(
hex_bytes,
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.eq(hex_bytes) {
return true;
}
}
false
}
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::StartsWith(
hex_bytes,
))) => {
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(rule)) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.starts_with(hex_bytes) {
return true;
// opret contains the op_return data section prefixed with `0x`.
let opret = match OpReturn::from_string(&output.script_pubkey) {
Ok(op) => op,
Err(_) => continue,
};

// encoded_pattern takes a predicate pattern and return its lowercase hex
// representation.
fn encoded_pattern(pattern: &str) -> String {
// If the pattern starts with 0x, return it in lowercase and without the 0x
// prefix.
if pattern.starts_with("0x") {
return pattern
.strip_prefix("0x")
.unwrap()
.to_lowercase()
.to_string();
}

// In this case it should be trated as ASCII so let's return its hex
// representation.
hex::encode(pattern)
}
}
false
}
BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(MatchingRule::EndsWith(
hex_bytes,
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.ends_with(hex_bytes) {
return true;

match rule {
MatchingRule::StartsWith(pattern) => {
if opret.starts_with(&encoded_pattern(pattern)) {
return true;
}
}
MatchingRule::EndsWith(pattern) => {
if opret.ends_with(&encoded_pattern(pattern)) {
return true;
}
}
MatchingRule::Equals(pattern) => {
if opret.eq(&encoded_pattern(pattern)) {
return true;
}
}
}
}
false
Expand Down Expand Up @@ -452,3 +489,6 @@ impl BitcoinPredicateType {
}
}
}

#[cfg(test)]
pub mod tests;
87 changes: 87 additions & 0 deletions components/chainhook-sdk/src/chainhooks/bitcoin/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use super::super::types::MatchingRule;
use super::*;
use crate::types::BitcoinTransactionMetadata;
use chainhook_types::bitcoin::TxOut;

use test_case::test_case;

#[test_case(
"0x6affAAAA",
MatchingRule::Equals(String::from("0xAAAA")),
true;
"OpReturn: Equals matches Hex value"
)]
#[test_case(
"0x60ff0000",
MatchingRule::Equals(String::from("0x0000")),
false;
"OpReturn: Invalid OP_RETURN opcode"
)]
#[test_case(
"0x6aff012345",
MatchingRule::Equals(String::from("0x0000")),
false;
"OpReturn: Equals does not match Hex value"
)]
#[test_case(
"0x6aff68656C6C6F",
MatchingRule::Equals(String::from("hello")),
true;
"OpReturn: Equals matches ASCII value"
)]
#[test_case(
"0x6affAA0000",
MatchingRule::StartsWith(String::from("0xAA")),
true;
"OpReturn: StartsWith matches Hex value"
)]
#[test_case(
"0x6aff585858", // 0x585858 => XXX
MatchingRule::StartsWith(String::from("X")),
true;
"OpReturn: StartsWith matches ASCII value"
)]
#[test_case(
"0x6aff0000AA",
MatchingRule::EndsWith(String::from("0xAA")),
true;
"OpReturn: EndsWith matches Hex value"
)]
#[test_case(
"0x6aff000058",
MatchingRule::EndsWith(String::from("X")),
true;
"OpReturn: EndsWith matches ASCII value"
)]

fn test_script_pubkey_evaluation(script_pubkey: &str, rule: MatchingRule, matches: bool) {
let predicate = BitcoinPredicateType::Outputs(OutputPredicate::OpReturn(rule));

let outputs = vec![TxOut {
value: 0,
script_pubkey: String::from(script_pubkey),
}];

let tx = BitcoinTransactionData {
transaction_identifier: TransactionIdentifier {
hash: String::from(""),
},
operations: vec![],
metadata: BitcoinTransactionMetadata {
fee: 0,
proof: None,
inputs: vec![],
stacks_operations: vec![],
ordinal_operations: vec![],

outputs,
},
};

let ctx = Context {
logger: None,
tracer: false,
};

assert_eq!(matches, predicate.evaluate_transaction_predicate(&tx, &ctx),);
}
46 changes: 24 additions & 22 deletions docs/how-to-guides/how-to-use-chainhooks-with-bitcoin.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
---
title: Use Chainhooks with Bitcoin
---
# Use Chainhooks with Bitcoin

The following guide helps you define predicates to use Chainhook with Bitcoin. The predicates are specified based on `if-this`, `then-that` constructs.

Expand All @@ -23,54 +21,58 @@ Get any transaction matching a given transaction ID (txid):
}
```

Get any transaction, including:
Get any transaction matching a given `OP_RETURN` payload:
Example: Given the following `script_pubkey` :

- OP_RETURN output starting with a set of characters.
- `starts_with` mandatory argument admits:
- ASCII string type. Example: `X2[`
- hex encoded bytes. Example: `0x589403`
```
OP_RETURN
PUSHDATA(0x03)
0x616263
```

or `0x6a03616263` in hex, the following predicates will match the transaction above.

Get any transaction, where its `OP_RETURN` payload starts with a set of characters:
- `starts_with` mandatory argument admits:
- ASCII string type. Example: `ab`
- hex encoded bytes. Example: `0x6162`

```json
{
"if_this": {
"scope": "outputs",
"op_return": {
"starts_with": "X2["
"starts_with": "ab"
}
}
}
```

`op_return` is used to find blocks starting, ending, or equivalent to a specific string from the list of output blocks.

Get any transaction, including an OP_RETURN output matching the sequence of bytes specified:

Get any transaction, where its `OP_RETURN` payload is equals to set of characters:
- `equals` mandatory argument admits:
- hex encoded bytes. Example: `0x69bd04208265aca9424d0337dac7d9e84371a2c91ece1891d67d3554bd9fdbe60afc6924d4b0773d90000006700010000006600012`
- ASCII string type: Example `abc`
- hex encoded bytes. Example: `0x616263`

```json
{
"if_this": {
"scope": "outputs",
"op_return": {
"equals": "0x69bd04208265aca9424d0337dac7d9e84371a2c91ece1891d67d3554bd9fdbe60afc6924d4b0773d90000006700010000006600012"
"equals": "0x616263"
}
}
}
```

Get any transaction, including an OP_RETURN output ending with a set of characters:

Get any transaction, where its `OP_RETURN` payload ends with a set of characters:
- `ends_with` mandatory argument admits:
- ASCII string type. Example: `X2[`
- hex encoded bytes. Example: `0x76a914000000000000000000000000000000000000000088ac`
- ASCII string type. Example: `bc`
- hex encoded bytes. Example: `0x6263`

```json
{
"if_this": {
"scope": "outputs",
"op_return": {
"ends_with": "0x76a914000000000000000000000000000000000000000088ac"
"ends_with": "0x6263"
}
}
}
Expand Down
Loading