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

feat: predicate validation #611

Merged
merged 40 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
694fb4d
chore: remove or rename event observer config fields
MicaiahReid Apr 24, 2024
0947acc
add prometheus monitoring port to config overrides
MicaiahReid Jun 20, 2024
4e997fc
rename ingestion_port in config overrides
MicaiahReid Jun 20, 2024
3f6ef10
create `default` fn for `EventObserverConfig`
MicaiahReid Jun 20, 2024
3b0f7a3
create `EventObserverConfig` helper to register a predicate
MicaiahReid Jun 20, 2024
fc67dff
feat: add `BitcoinEventObserverBuilder` struct
MicaiahReid Jun 20, 2024
6d6f9fe
add impl fns to `EventObserverConfigOverrides` to allow using as builder
MicaiahReid Jun 20, 2024
9da1178
rename EventObserverConfigOverrides to EventObserverConfigBuilder
MicaiahReid Jun 20, 2024
bc7c735
rsdoc comments
MicaiahReid Jun 20, 2024
5a4cb39
chore: rename structs
MicaiahReid Jun 20, 2024
f992880
fix tests
MicaiahReid Jun 20, 2024
39986d5
fix openapi.json
MicaiahReid Jun 21, 2024
473a8da
fix doc comments
MicaiahReid Jun 21, 2024
83e8336
move stacks/bitcoin specific chainhook types
MicaiahReid Jun 21, 2024
4debc28
add helpers for registering chainhooks
MicaiahReid Jun 24, 2024
c54b6e7
rename ChainhookConfig -> ChainhookStore
MicaiahReid Jun 24, 2024
08c15ba
rs doc comment
MicaiahReid Jun 24, 2024
cee7be6
fix helpers to return self
MicaiahReid Jun 24, 2024
dddc69a
rename `BitcoinEventObserverBuilder` -> `BitcoinEventObserverConfigBu…
MicaiahReid Jun 24, 2024
fe04dd9
add builder for starting an event observer
MicaiahReid Jun 24, 2024
1d1b1f2
rs doc for ChainhookSpecificationNetworkMap
MicaiahReid Jun 24, 2024
8731e3c
fix rsdoc
MicaiahReid Jun 24, 2024
7e99a3c
clean up
MicaiahReid Jun 24, 2024
ea66e72
update openapi spec
MicaiahReid Jun 24, 2024
2812b21
`uwrap_or_else` instead of `unwrap_or`
MicaiahReid Jun 24, 2024
1d5106f
remove dead code
MicaiahReid Jun 27, 2024
f0eb836
Merge branch 'main' into 'feat/improve-sdk-interface'
MicaiahReid Jun 27, 2024
99e745e
Merge branch 'main' into feat/improve-sdk-interface
MicaiahReid Jun 27, 2024
59c8e1a
apply suggestion from review
MicaiahReid Jun 27, 2024
42f7271
add as_str helper for BitcoinNetwork
MicaiahReid Jun 27, 2024
ab4455e
add builder functions for BitcoinChainhookSpecification
MicaiahReid Jun 27, 2024
486d051
refactor descriptor matcher rule predicate eval
MicaiahReid Jun 27, 2024
695a738
merge cleanup
MicaiahReid Jul 1, 2024
98802f5
add validation functions for stacks/bitcoin predicates
MicaiahReid Jul 1, 2024
16b667b
rearrange bitcoin tests
MicaiahReid Jul 1, 2024
e8465b8
fix existing test to have valid descriptor
MicaiahReid Jul 1, 2024
0aa53bd
add tests for predicate spec validation
MicaiahReid Jul 1, 2024
4038ab6
call predicate validation for cli methods
MicaiahReid Jul 1, 2024
d06a636
add predicate name to validation errors
MicaiahReid Jul 1, 2024
b1720e0
Merge branch 'main' into feat/predicate-validation
MicaiahReid Jul 1, 2024
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
2 changes: 2 additions & 0 deletions components/chainhook-cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> {
let mut config =
Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?;
let predicate = load_predicate_from_path(&cmd.predicate_path)?;
predicate.validate()?;
match predicate {
ChainhookSpecificationNetworkMap::Bitcoin(predicate) => {
let predicate_spec = match predicate
Expand Down Expand Up @@ -578,6 +579,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> {
let config = Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?;
let predicate: ChainhookSpecificationNetworkMap =
load_predicate_from_path(&cmd.predicate_path)?;
predicate.validate()?;

match predicate {
ChainhookSpecificationNetworkMap::Bitcoin(predicate) => {
Expand Down
2 changes: 1 addition & 1 deletion components/chainhook-cli/src/service/http_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::{
};

use chainhook_sdk::{
chainhooks::types::{ChainhookSpecificationNetworkMap, ChainhookInstance},
chainhooks::types::{ChainhookInstance, ChainhookSpecificationNetworkMap},
observer::ObserverCommand,
utils::Context,
};
Expand Down
2 changes: 1 addition & 1 deletion components/chainhook-cli/src/service/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ async fn it_handles_bitcoin_predicates_with_network(network: &str) {
#[test_case(json!({ "scope": "outputs","p2sh": {"equals": "2MxDJ723HBJtEMa2a9vcsns4qztxBuC8Zb2"}}) ; "with scope outputs type p2sh")]
#[test_case(json!({"scope": "outputs","p2wpkh": {"equals": "bcrt1qnxknq3wqtphv7sfwy07m7e4sr6ut9yt6ed99jg"}}) ; "with scope outputs type p2wpkh")]
#[test_case(json!({"scope": "outputs","p2wsh": {"equals": "bc1qklpmx03a8qkv263gy8te36w0z9yafxplc5kwzc"}}) ; "with scope outputs type p2wsh")]
#[test_case(json!({"scope": "outputs","descriptor": {"expression": "a descriptor", "range": [0,3]}}) ; "with scope outputs type descriptor")]
#[test_case(json!({"scope": "outputs","descriptor": {"expression": "wpkh(02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9)", "range": [0,3]}}) ; "with scope outputs type descriptor")]
#[test_case(json!({"scope": "stacks_protocol","operation": "stacker_rewarded"}) ; "with scope stacks_protocol operation stacker_rewarded")]
#[test_case(json!({"scope": "stacks_protocol","operation": "block_committed"}) ; "with scope stacks_protocol operation block_committed")]
#[test_case(json!({"scope": "stacks_protocol","operation": "leader_registered"}) ; "with scope stacks_protocol operation leader_registered")]
Expand Down
223 changes: 197 additions & 26 deletions components/chainhook-sdk/src/chainhooks/bitcoin/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use super::types::{ChainhookInstance, ExactMatchingRule, HookAction, MatchingRule};
use crate::utils::Context;
use super::types::{
append_error_context, validate_txid, ChainhookInstance, ExactMatchingRule, HookAction,
MatchingRule,
};
use crate::utils::{Context, MAX_BLOCK_HEIGHTS_ENTRIES};

use bitcoincore_rpc_json::bitcoin::{address::Payload, Address};
use chainhook_types::{
Expand Down Expand Up @@ -49,6 +52,90 @@ pub struct BitcoinChainhookSpecification {
pub action: HookAction,
}

impl BitcoinChainhookSpecification {
pub fn new(predicate: BitcoinPredicateType, action: HookAction) -> Self {
BitcoinChainhookSpecification {
blocks: None,
start_block: None,
end_block: None,
expire_after_occurrence: None,
include_proof: None,
include_inputs: None,
include_outputs: None,
include_witness: None,
predicate,
action,
}
}

pub fn blocks(&mut self, blocks: Vec<u64>) -> &mut Self {
self.blocks = Some(blocks);
self
}

pub fn start_block(&mut self, start_block: u64) -> &mut Self {
self.start_block = Some(start_block);
self
}

pub fn end_block(&mut self, end_block: u64) -> &mut Self {
self.end_block = Some(end_block);
self
}

pub fn expire_after_occurrence(&mut self, occurrence: u64) -> &mut Self {
self.expire_after_occurrence = Some(occurrence);
self
}

pub fn include_proof(&mut self, do_include: bool) -> &mut Self {
self.include_proof = Some(do_include);
self
}

pub fn include_inputs(&mut self, do_include: bool) -> &mut Self {
self.include_inputs = Some(do_include);
self
}

pub fn include_outputs(&mut self, do_include: bool) -> &mut Self {
self.include_outputs = Some(do_include);
self
}

pub fn include_witness(&mut self, do_include: bool) -> &mut Self {
self.include_witness = Some(do_include);
self
}

pub fn validate(&self) -> Result<(), Vec<String>> {
let mut errors = vec![];
if let Err(e) = self.action.validate() {
errors.append(&mut append_error_context("invalid 'then_that' value", e));
}
if let Err(e) = self.predicate.validate() {
errors.append(&mut append_error_context("invalid 'if_this' value", e));
}

if let Some(end_block) = self.end_block {
let start_block = self.start_block.unwrap_or(0);
if start_block > end_block {
errors.push(
"Chainhook specification field `end_block` should be greater than `start_block`.".into()
);
}
if (end_block - start_block) > MAX_BLOCK_HEIGHTS_ENTRIES {
errors.push(format!("Chainhook specification exceeds max number of blocks to scan. Maximum: {}, Attempted: {}", MAX_BLOCK_HEIGHTS_ENTRIES, (end_block - start_block)));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
}

/// Maps some [BitcoinChainhookSpecification] to a corresponding [BitcoinNetwork]. This allows maintaining one
/// serialized predicate file for a given predicate on each network.
///
Expand Down Expand Up @@ -184,6 +271,41 @@ pub enum BitcoinPredicateType {
OrdinalsProtocol(OrdinalOperations),
}

impl BitcoinPredicateType {
pub fn validate(&self) -> Result<(), Vec<String>> {
match self {
BitcoinPredicateType::Block => {}
BitcoinPredicateType::Txid(ExactMatchingRule::Equals(txid)) => {
if let Err(e) = validate_txid(txid) {
return Err(append_error_context(
"invalid predicate for scope 'txid'",
vec![e],
));
}
}
BitcoinPredicateType::Inputs(input) => {
if let Err(e) = input.validate() {
return Err(append_error_context(
"invalid predicate for scope 'inputs'",
e,
));
}
}
BitcoinPredicateType::Outputs(outputs) => {
if let Err(e) = outputs.validate() {
return Err(append_error_context(
"invalid predicate for scope 'outputs'",
vec![e],
));
}
}
BitcoinPredicateType::StacksProtocol(_) => {}
BitcoinPredicateType::OrdinalsProtocol(_) => {}
}
Ok(())
}
}

pub struct BitcoinTriggerChainhook<'a> {
pub chainhook: &'a BitcoinChainhookInstance,
pub apply: Vec<(Vec<&'a BitcoinTransactionData>, &'a BitcoinBlockData)>,
Expand All @@ -208,6 +330,15 @@ pub enum InputPredicate {
WitnessScript(MatchingRule),
}

impl InputPredicate {
pub fn validate(&self) -> Result<(), Vec<String>> {
match self {
InputPredicate::Txid(txin) => txin.validate(),
InputPredicate::WitnessScript(_) => Ok(()),
}
}
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OutputPredicate {
Expand All @@ -219,6 +350,20 @@ pub enum OutputPredicate {
Descriptor(DescriptorMatchingRule),
}

impl OutputPredicate {
pub fn validate(&self) -> Result<(), String> {
match self {
OutputPredicate::OpReturn(_) => {}
OutputPredicate::P2pkh(ExactMatchingRule::Equals(_p2pkh)) => {}
OutputPredicate::P2sh(ExactMatchingRule::Equals(_p2sh)) => {}
OutputPredicate::P2wpkh(ExactMatchingRule::Equals(_p2wpkh)) => {}
OutputPredicate::P2wsh(ExactMatchingRule::Equals(_p2wsh)) => {}
OutputPredicate::Descriptor(descriptor) => descriptor.validate()?,
}
Ok(())
}
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case", tag = "operation")]
pub enum StacksOperations {
Expand Down Expand Up @@ -348,6 +493,15 @@ pub struct TxinPredicate {
pub vout: u32,
}

impl TxinPredicate {
pub fn validate(&self) -> Result<(), Vec<String>> {
if let Err(e) = validate_txid(&self.txid) {
return Err(vec![e]);
}
Ok(())
}
}

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct DescriptorMatchingRule {
Expand All @@ -357,6 +511,43 @@ pub struct DescriptorMatchingRule {
pub range: Option<[u32; 2]>,
}

impl DescriptorMatchingRule {
pub fn validate(&self) -> Result<(), String> {
let _ = self.derive_script_pubkeys()?;
Ok(())
}

pub fn derive_script_pubkeys(&self) -> Result<Vec<String>, String> {
let DescriptorMatchingRule { expression, range } = self;
// 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)
.map_err(|e| format!("invalid descriptor: {}", e.to_string()))?;

// 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]
};

let mut script_pubkeys = vec![];
// 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)
.map_err(|e| format!("error deriving descriptor: {}", e))?;

// Extract and encode the derived pubkey.
script_pubkeys.push(hex::encode(derived.script_pubkey().as_bytes()));
}
Ok(script_pubkeys)
}
}

// 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
Expand Down Expand Up @@ -788,31 +979,11 @@ 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());
BitcoinPredicateType::Outputs(OutputPredicate::Descriptor(descriptor)) => {
let script_pubkeys = descriptor.derive_script_pubkeys().unwrap();

// Match that script against the tx outputs.
for script_pubkey in script_pubkeys {
// Match the 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| {
Expand Down
Loading
Loading