feat: introduce new predicate + refactor schemas

This commit is contained in:
Ludo Galabru
2023-02-10 00:11:41 -05:00
parent fcb87b635c
commit 611c79cee3
5 changed files with 230 additions and 160 deletions

View File

@@ -47,15 +47,71 @@ struct Opts {
#[derive(Subcommand, PartialEq, Clone, Debug)]
enum Command {
/// Manage predicates
#[clap(subcommand)]
Predicates(PredicatesCommand),
/// Start chainhook-cli
#[clap(subcommand)]
Node(NodeCommand),
/// Start chainhook-cli in replay mode
#[clap(name = "replay", bin_name = "replay")]
Replay(ReplayCommand),
}
#[derive(Subcommand, PartialEq, Clone, Debug)]
#[clap(bin_name = "predicate", aliases = &["predicate"])]
enum PredicatesCommand {
/// Generate new predicate
#[clap(name = "new", bin_name = "new", aliases = &["generate"])]
New(NewPredicate),
/// Scan blocks (one-off) from specified network and apply provided predicate
#[clap(name = "scan", bin_name = "scan")]
Scan(ScanCommand),
Scan(ScanPredicate),
}
#[derive(Parser, PartialEq, Clone, Debug)]
struct NewPredicate {
/// Predicate's name
pub name: String,
/// Path to Clarinet.toml
#[clap(long = "manifest-path")]
pub manifest_path: Option<String>,
/// Generate a Bitcoin chainhook
#[clap(long = "bitcoin", conflicts_with = "stacks")]
pub bitcoin: bool,
/// Generate a Stacks chainhook
#[clap(long = "stacks", conflicts_with = "bitcoin")]
pub stacks: bool,
}
#[derive(Parser, PartialEq, Clone, Debug)]
struct ScanPredicate {
pub devnet: bool,
/// Target Testnet network
#[clap(
long = "testnet",
conflicts_with = "devnet",
conflicts_with = "mainnet"
)]
pub testnet: bool,
/// Target Mainnet network
#[clap(
long = "mainnet",
conflicts_with = "testnet",
conflicts_with = "devnet"
)]
pub mainnet: bool,
/// Load config file path
#[clap(
long = "config-path",
conflicts_with = "mainnet",
conflicts_with = "testnet",
conflicts_with = "devnet"
)]
pub config_path: Option<String>,
/// Load chainhook file path (yaml format)
#[clap(long = "predicate-path", short = 'p')]
pub chainhook_spec_path: String,
}
#[derive(Subcommand, PartialEq, Clone, Debug)]
@@ -131,39 +187,6 @@ struct ReplayCommand {
pub bitcoind_rpc_url: Option<String>,
}
#[derive(Parser, PartialEq, Clone, Debug)]
struct ScanCommand {
pub devnet: bool,
/// Target Testnet network
#[clap(
long = "testnet",
conflicts_with = "devnet",
conflicts_with = "mainnet"
)]
pub testnet: bool,
/// Target Mainnet network
#[clap(
long = "mainnet",
conflicts_with = "testnet",
conflicts_with = "devnet"
)]
pub mainnet: bool,
/// Load config file path
#[clap(
long = "config-path",
conflicts_with = "mainnet",
conflicts_with = "testnet",
conflicts_with = "devnet"
)]
pub config_path: Option<String>,
/// Load chainhook file path (yaml format)
#[clap(
long = "chainhook-spec-path",
short = 'p'
)]
pub chainhook_spec_path: String,
}
pub fn main() {
let logger = hiro_system_kit::log::setup_logger();
let _guard = hiro_system_kit::log::setup_global_logger(logger.clone());
@@ -194,6 +217,30 @@ pub fn main() {
start_node(config, ctx);
}
},
Command::Predicates(subcmd) => match subcmd {
PredicatesCommand::New(cmd) => {
// Predicates can either be generated manually by letting developers
// craft their own json payload, or using the interactive approach.
// A list of contracts is displayed, then list of methods, then list of events detected
// 3 files are generated:
// predicates/simnet/name.json
// predicates/devnet/name.json
// predicates/testnet/name.json
// predicates/mainnet/name.json
let manifest = clarinet_files::get_manifest_location(None);
}
PredicatesCommand::Scan(cmd) => {
let config =
match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) {
Ok(config) => config,
Err(e) => {
println!("{e}");
process::exit(1);
}
};
start_scan(config, ctx);
}
},
Command::Replay(cmd) => {
let mut config =
match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) {
@@ -230,41 +277,28 @@ pub fn main() {
}
start_replay(config, cmd.apply_trigger, ctx);
}
Command::Scan(cmd) => {
let config =
match Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path) {
Ok(config) => config,
Err(e) => {
println!("{e}");
process::exit(1);
}
};
start_scan(config, ctx);
}
}
}
pub fn install_ctrlc_handler(terminate_tx: Sender<DigestingCommand>, ctx: Context) {
ctrlc::set_handler(move || {
warn!(
&ctx.expect_logger(),
"Manual interruption signal received"
);
warn!(&ctx.expect_logger(), "Manual interruption signal received");
terminate_tx
.send(DigestingCommand::Kill)
.expect("Unable to terminate service");
}).expect("Error setting Ctrl-C handler");
})
.expect("Error setting Ctrl-C handler");
}
pub fn download_dataset_if_required(config: &mut Config, ctx: &Context) -> bool {
if config.is_initial_ingestion_required() {
// Download default tsv.
if config.rely_on_remote_tsv() && config.should_download_remote_tsv() {
let url = config.expected_remote_tsv_url();
let mut destination_path = config.expected_cache_path();
destination_path.push(archive::default_tsv_file_path(&config.network.stacks_network));
destination_path.push(archive::default_tsv_file_path(
&config.network.stacks_network,
));
// Download archive if not already present in cache
if !destination_path.exists() {
info!(ctx.expect_logger(), "Downloading {}", url);

View File

@@ -1,7 +1,7 @@
use super::types::{
BitcoinChainhookSpecification, BitcoinPredicateType, ExactMatchingRule, HookAction,
KeyRegistrationPredicate, LockSTXPredicate, MatchingRule, PobPredicate, PoxPredicate,
TransferSTXPredicate,
InputPredicate, MatchingRule, OrdinalOperations, OutputPredicate, Protocols, Scopes,
StacksOperations, TxinPredicate,
};
use base58::FromBase58;
use bitcoincore_rpc::bitcoin::blockdata::opcodes;
@@ -9,8 +9,8 @@ use bitcoincore_rpc::bitcoin::blockdata::script::Builder as BitcoinScriptBuilder
use bitcoincore_rpc::bitcoin::util::address::Payload;
use bitcoincore_rpc::bitcoin::Address;
use chainhook_types::{
BitcoinBlockData, BitcoinChainEvent, BitcoinTransactionData, StacksBaseChainOperation,
TransactionIdentifier,
BitcoinBlockData, BitcoinChainEvent, BitcoinTransactionData, OrdinalOperation,
StacksBaseChainOperation, TransactionIdentifier,
};
use clarity_repl::clarity::util::hash::to_hex;
use reqwest::{Client, Method};
@@ -144,6 +144,7 @@ pub fn serialize_bitcoin_payload_to_json<'a>(
"inputs": transaction.metadata.inputs,
"outputs": transaction.metadata.outputs,
"stacks_operations": transaction.metadata.stacks_operations,
"ordinal_operations": transaction.metadata.ordinal_operations,
"proof": proofs.get(&transaction.transaction_identifier),
}),
})
@@ -231,11 +232,13 @@ pub fn handle_bitcoin_hook_action<'a>(
impl BitcoinChainhookSpecification {
pub fn evaluate_transaction_predicate(&self, tx: &BitcoinTransactionData) -> bool {
// TODO(lgalabru): follow-up on this implementation
match &self.predicate.kind {
BitcoinPredicateType::TransactionIdentifierHash(ExactMatchingRule::Equals(txid)) => {
match &self.predicate {
BitcoinPredicateType::Txid(ExactMatchingRule::Equals(txid)) => {
tx.transaction_identifier.hash.eq(txid)
}
BitcoinPredicateType::OpReturn(MatchingRule::Equals(hex_bytes)) => {
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn(
MatchingRule::Equals(hex_bytes),
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.eq(hex_bytes) {
return true;
@@ -243,7 +246,9 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::OpReturn(MatchingRule::StartsWith(hex_bytes)) => {
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn(
MatchingRule::StartsWith(hex_bytes),
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.starts_with(hex_bytes) {
return true;
@@ -251,7 +256,9 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::OpReturn(MatchingRule::EndsWith(hex_bytes)) => {
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::OpReturn(
MatchingRule::EndsWith(hex_bytes),
))) => {
for output in tx.metadata.outputs.iter() {
if output.script_pubkey.ends_with(hex_bytes) {
return true;
@@ -259,7 +266,9 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::P2pkh(ExactMatchingRule::Equals(address)) => {
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2pkh(
ExactMatchingRule::Equals(address),
))) => {
let pubkey_hash = address
.from_base58()
.expect("Unable to get bytes from btc address");
@@ -278,7 +287,9 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::P2sh(ExactMatchingRule::Equals(address)) => {
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2sh(
ExactMatchingRule::Equals(address),
))) => {
let script_hash = address
.from_base58()
.expect("Unable to get bytes from btc address");
@@ -295,8 +306,12 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::P2wpkh(ExactMatchingRule::Equals(encoded_address))
| BitcoinPredicateType::P2wsh(ExactMatchingRule::Equals(encoded_address)) => {
BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2wpkh(
ExactMatchingRule::Equals(encoded_address),
)))
| BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2wsh(
ExactMatchingRule::Equals(encoded_address),
))) => {
let address = match Address::from_str(encoded_address) {
Ok(address) => match address.payload {
Payload::WitnessProgram {
@@ -315,7 +330,22 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Pob(PobPredicate::Any) => {
BitcoinPredicateType::Scope(Scopes::Inputs(InputPredicate::Txid(predicate))) => {
// TODO(lgalabru): add support for transaction chainhing, if enabled
for input in tx.metadata.inputs.iter() {
if input.previous_output.txid.eq(&predicate.txid)
&& input.previous_output.vout.eq(&predicate.vout)
{
return true;
}
}
false
}
BitcoinPredicateType::Scope(Scopes::Inputs(InputPredicate::WitnessScript(_))) => {
// TODO(lgalabru)
unimplemented!()
}
BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::Pob)) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::PobBlockCommitment(_) = op {
return true;
@@ -323,7 +353,7 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Pox(PoxPredicate::Any) => {
BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::Pox)) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::PoxBlockCommitment(_) = op {
return true;
@@ -331,45 +361,9 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Pox(PoxPredicate::Recipient(MatchingRule::Equals(address))) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::PoxBlockCommitment(commitment) = op {
for reward in commitment.rewards.iter() {
if reward.recipient.eq(address) {
return true;
}
}
}
}
false
}
BitcoinPredicateType::Pox(PoxPredicate::Recipient(MatchingRule::StartsWith(
prefix,
))) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::PoxBlockCommitment(commitment) = op {
for reward in commitment.rewards.iter() {
if reward.recipient.starts_with(prefix) {
return true;
}
}
}
}
false
}
BitcoinPredicateType::Pox(PoxPredicate::Recipient(MatchingRule::EndsWith(suffix))) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::PoxBlockCommitment(commitment) = op {
for reward in commitment.rewards.iter() {
if reward.recipient.ends_with(suffix) {
return true;
}
}
}
}
false
}
BitcoinPredicateType::KeyRegistration(KeyRegistrationPredicate::Any) => {
BitcoinPredicateType::Protocol(Protocols::Stacks(
StacksOperations::KeyRegistration,
)) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::KeyRegistration(_) = op {
return true;
@@ -377,7 +371,7 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::TransferSTX(TransferSTXPredicate::Any) => {
BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::TransferSTX)) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::TransferSTX(_) = op {
return true;
@@ -385,7 +379,7 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::LockSTX(LockSTXPredicate::Any) => {
BitcoinPredicateType::Protocol(Protocols::Stacks(StacksOperations::LockSTX)) => {
for op in tx.metadata.stacks_operations.iter() {
if let StacksBaseChainOperation::LockSTX(_) = op {
return true;
@@ -393,6 +387,16 @@ impl BitcoinChainhookSpecification {
}
false
}
BitcoinPredicateType::Protocol(Protocols::Ordinal(
OrdinalOperations::NewInscription,
)) => {
for op in tx.metadata.ordinal_operations.iter() {
if let OrdinalOperation::InscriptionReveal(_) = op {
return true;
}
}
false
}
}
}
}

View File

@@ -39,7 +39,7 @@ impl ChainhookConfig {
pub fn get_serialized_bitcoin_predicates(
&self,
) -> Vec<(&String, &BitcoinNetwork, &BitcoinTransactionFilterPredicate)> {
) -> Vec<(&String, &BitcoinNetwork, &BitcoinPredicateType)> {
let mut bitcoin = vec![];
for chainhook in self.bitcoin_chainhooks.iter() {
bitcoin.push((&chainhook.uuid, &chainhook.network, &chainhook.predicate));
@@ -189,7 +189,7 @@ pub struct BitcoinChainhookSpecification {
pub end_block: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expire_after_occurrence: Option<u64>,
pub predicate: BitcoinTransactionFilterPredicate,
pub predicate: BitcoinPredicateType,
pub action: HookAction,
}
@@ -276,42 +276,82 @@ impl ScriptTemplate {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct BitcoinTransactionFilterPredicate {
pub scope: Scope,
#[serde(flatten)]
pub kind: BitcoinPredicateType,
pub predicate: BitcoinPredicateType,
}
impl BitcoinTransactionFilterPredicate {
pub fn new(scope: Scope, kind: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate {
BitcoinTransactionFilterPredicate { scope, kind }
pub fn new(predicate: BitcoinPredicateType) -> BitcoinTransactionFilterPredicate {
BitcoinTransactionFilterPredicate { predicate }
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type", content = "rule")]
pub enum BitcoinPredicateType {
TransactionIdentifierHash(ExactMatchingRule),
Txid(ExactMatchingRule),
Scope(Scopes),
Protocol(Protocols),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Scopes {
Inputs(InputPredicate),
Outputs(OutputPredicate),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum InputPredicate {
Txid(TxinPredicate),
WitnessScript(MatchingRule),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OutputPredicate {
OpReturn(MatchingRule),
P2pkh(ExactMatchingRule),
P2sh(ExactMatchingRule),
P2wpkh(ExactMatchingRule),
P2wsh(ExactMatchingRule),
Pox(PoxPredicate),
Pob(PobPredicate),
KeyRegistration(KeyRegistrationPredicate),
TransferSTX(TransferSTXPredicate),
LockSTX(LockSTXPredicate),
}
pub fn get_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum Protocols {
Stacks(StacksOperations),
Ordinal(OrdinalOperations),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum StacksOperations {
Pox,
Pob,
KeyRegistration,
TransferSTX,
LockSTX,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OrdinalOperations {
NewInscription,
}
pub fn get_stacks_canonical_magic_bytes(network: &BitcoinNetwork) -> [u8; 2] {
match network {
BitcoinNetwork::Mainnet => ['X' as u8, '2' as u8],
BitcoinNetwork::Testnet => ['T' as u8, '2' as u8],
BitcoinNetwork::Regtest => ['i' as u8, 'd' as u8],
BitcoinNetwork::Mainnet => *b"X2",
BitcoinNetwork::Testnet => *b"T2",
BitcoinNetwork::Regtest => *b"id",
}
}
pub fn get_ordinal_canonical_magic_bytes() -> (usize, [u8; 3]) {
return (37, *b"ord");
}
pub struct PoxConfig {
pub genesis_block_height: u64,
pub prepare_phase_len: u64,
@@ -386,33 +426,9 @@ impl TryFrom<u8> for StacksOpcodes {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum KeyRegistrationPredicate {
Any,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum TransferSTXPredicate {
Any,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum LockSTXPredicate {
Any,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PoxPredicate {
Any,
Recipient(MatchingRule),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum PobPredicate {
Any,
pub struct TxinPredicate {
pub txid: String,
pub vout: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)]

View File

@@ -1,7 +1,7 @@
use crate::chainhooks::types::{
BitcoinChainhookSpecification, BitcoinPredicateType, BitcoinTransactionFilterPredicate,
ChainhookConfig, ChainhookSpecification, ExactMatchingRule, HookAction, Scope,
StacksChainhookSpecification, StacksContractCallBasedPredicate,
ChainhookConfig, ChainhookSpecification, ExactMatchingRule, HookAction, OutputPredicate, Scope,
Scopes, StacksChainhookSpecification, StacksContractCallBasedPredicate,
StacksTransactionFilterPredicate,
};
use crate::indexer::tests::helpers::transactions::generate_test_tx_bitcoin_p2pkh_transfer;
@@ -89,10 +89,9 @@ fn bitcoin_chainhook_p2pkh(
start_block: None,
end_block: None,
expire_after_occurrence,
predicate: BitcoinTransactionFilterPredicate {
scope: Scope::Outputs,
kind: BitcoinPredicateType::P2pkh(ExactMatchingRule::Equals(address.to_string())),
},
predicate: BitcoinPredicateType::Scope(Scopes::Outputs(OutputPredicate::P2pkh(
ExactMatchingRule::Equals(address.to_string()),
))),
action: HookAction::Noop,
};
spec

View File

@@ -268,9 +268,26 @@ pub struct BitcoinTransactionMetadata {
pub inputs: Vec<TxIn>,
pub outputs: Vec<TxOut>,
pub stacks_operations: Vec<StacksBaseChainOperation>,
pub ordinal_operations: Vec<OrdinalOperation>,
pub proof: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum OrdinalOperation {
InscriptionCommit(OrdinalInscriptionCommitData),
InscriptionReveal(OrdinalInscriptionRevealData),
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct OrdinalInscriptionCommitData {}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct OrdinalInscriptionRevealData {
pub satoshi_point: String,
pub content_type: String,
pub content: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum StacksBaseChainOperation {
PoxBlockCommitment(PoxBlockCommitmentData),