Skip to content

Commit 02db65e

Browse files
author
Ludo Galabru
committed
feat: upgrade service start implementation + documentation
1 parent 6516155 commit 02db65e

File tree

6 files changed

+142
-79
lines changed

6 files changed

+142
-79
lines changed

README.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,24 @@ Tbe first time this command run, a chainstate archive will be downloaded, uncomp
604604
The subsequent scans will use the cached chainstate if already present, speeding up iterations and the overall feedback loop.
605605

606606
---
607-
## Running `chainhook` in production mode
607+
## Run `chainhook` as a service for streaming new blocks
608608

609-
To be documented.
609+
`chainhook` can be ran as a background service for streaming and processing new canonical blocks appended to the Bitcoin and Stacks blockchains.
610610

611+
When running chainhook as a service, `if_this` / `then_that` predicates can be registered by passing the path of the `json` file in the command line:
612+
613+
```bash
614+
$ chainhook service start --predicate-path=./path/to/predicate-1.json --predicate-path=./path/to/predicate-2.json --config-path=./path/to/config.toml
615+
```
616+
617+
Predicates can also be added dynamically. When the `--predicate-path` option is not passed or when the `--start-http-api` option is passed, `chainhook` will instantiate a REST API allowing developers to list, add and removes preducates at runtime:
618+
619+
```bash
620+
$ chainhook service start --config-path=./path/to/config.toml
621+
```
622+
623+
```bash
624+
$ chainhook service start --predicate-path=./path/to/predicate-1.json --start-http-api --config-path=./path/to/config.toml
625+
```
626+
627+
A comprehensive OpenAPI spcification explaining how to interact with the Chainhook REST API can be found [here](./docs/chainhook-openapi.json).

components/chainhook-cli/src/cli/mod.rs

+33-13
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,12 @@ struct StartCommand {
166166
conflicts_with = "devnet"
167167
)]
168168
pub config_path: Option<String>,
169+
/// Specify relative path of the chainhooks (yaml format) to evaluate
170+
#[clap(long = "predicate-path")]
171+
pub predicates_paths: Vec<String>,
172+
/// Start REST API for managing predicates
173+
#[clap(long = "start-http-api")]
174+
pub start_http_api: bool,
169175
}
170176

171177
#[derive(Subcommand, PartialEq, Clone, Debug)]
@@ -321,10 +327,19 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> {
321327
match opts.command {
322328
Command::Service(subcmd) => match subcmd {
323329
ServiceCommand::Start(cmd) => {
324-
let config =
330+
let mut config =
325331
Config::default(cmd.devnet, cmd.testnet, cmd.mainnet, &cmd.config_path)?;
332+
// We disable the API if a predicate was passed, and the --enable-
333+
if cmd.predicates_paths.len() > 0 && !cmd.start_http_api {
334+
config.chainhooks.enable_http_api = false;
335+
}
326336
let mut service = Service::new(config, ctx);
327-
return service.run().await;
337+
let predicates = cmd
338+
.predicates_paths
339+
.iter()
340+
.map(|p| load_predicate_from_path(p))
341+
.collect::<Result<Vec<ChainhookFullSpecification>, _>>()?;
342+
return service.run(predicates).await;
328343
}
329344
},
330345
Command::Config(subcmd) => match subcmd {
@@ -455,17 +470,7 @@ async fn handle_command(opts: Opts, ctx: Context) -> Result<(), String> {
455470
PredicatesCommand::Scan(cmd) => {
456471
let mut config =
457472
Config::default(false, cmd.testnet, cmd.mainnet, &cmd.config_path)?;
458-
let file = std::fs::File::open(&cmd.predicate_path)
459-
.map_err(|e| format!("unable to read file {}\n{:?}", cmd.predicate_path, e))?;
460-
let mut file_reader = BufReader::new(file);
461-
let mut file_buffer = vec![];
462-
file_reader
463-
.read_to_end(&mut file_buffer)
464-
.map_err(|e| format!("unable to read file {}\n{:?}", cmd.predicate_path, e))?;
465-
let predicate: ChainhookFullSpecification = serde_json::from_slice(&file_buffer)
466-
.map_err(|e| {
467-
format!("unable to parse json file {}\n{:?}", cmd.predicate_path, e)
468-
})?;
473+
let predicate = load_predicate_from_path(&cmd.predicate_path)?;
469474
match predicate {
470475
ChainhookFullSpecification::Bitcoin(predicate) => {
471476
scan_bitcoin_chain_with_predicate(predicate, &config, &ctx).await?;
@@ -654,3 +659,18 @@ pub fn install_ctrlc_handler(terminate_tx: Sender<DigestingCommand>, ctx: Contex
654659
})
655660
.expect("Error setting Ctrl-C handler");
656661
}
662+
663+
pub fn load_predicate_from_path(
664+
predicate_path: &str,
665+
) -> Result<ChainhookFullSpecification, String> {
666+
let file = std::fs::File::open(&predicate_path)
667+
.map_err(|e| format!("unable to read file {}\n{:?}", predicate_path, e))?;
668+
let mut file_reader = BufReader::new(file);
669+
let mut file_buffer = vec![];
670+
file_reader
671+
.read_to_end(&mut file_buffer)
672+
.map_err(|e| format!("unable to read file {}\n{:?}", predicate_path, e))?;
673+
let predicate: ChainhookFullSpecification = serde_json::from_slice(&file_buffer)
674+
.map_err(|e| format!("unable to parse json file {}\n{:?}", predicate_path, e))?;
675+
Ok(predicate)
676+
}

components/chainhook-cli/src/config/mod.rs

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ pub struct TsvUrlConfig {
6868
pub struct ChainhooksConfig {
6969
pub max_stacks_registrations: u16,
7070
pub max_bitcoin_registrations: u16,
71+
pub enable_http_api: bool,
7172
}
7273

7374
impl Config {
@@ -97,6 +98,7 @@ impl Config {
9798
chainhook_config: None,
9899
ingestion_port: DEFAULT_INGESTION_PORT,
99100
control_port: DEFAULT_CONTROL_PORT,
101+
control_api_enabled: self.chainhooks.enable_http_api,
100102
bitcoind_rpc_username: self.network.bitcoind_rpc_username.clone(),
101103
bitcoind_rpc_password: self.network.bitcoind_rpc_password.clone(),
102104
bitcoind_rpc_url: self.network.bitcoind_rpc_url.clone(),
@@ -149,6 +151,7 @@ impl Config {
149151
.chainhooks
150152
.max_bitcoin_registrations
151153
.unwrap_or(100),
154+
enable_http_api: true,
152155
},
153156
network: IndexerConfig {
154157
stacks_node_rpc_url: config_file.network.stacks_node_rpc_url.to_string(),
@@ -273,6 +276,7 @@ impl Config {
273276
chainhooks: ChainhooksConfig {
274277
max_stacks_registrations: 50,
275278
max_bitcoin_registrations: 50,
279+
enable_http_api: true,
276280
},
277281
network: IndexerConfig {
278282
stacks_node_rpc_url: "http://0.0.0.0:20443".into(),
@@ -302,6 +306,7 @@ impl Config {
302306
chainhooks: ChainhooksConfig {
303307
max_stacks_registrations: 10,
304308
max_bitcoin_registrations: 10,
309+
enable_http_api: true,
305310
},
306311
network: IndexerConfig {
307312
stacks_node_rpc_url: "http://0.0.0.0:20443".into(),
@@ -331,6 +336,7 @@ impl Config {
331336
chainhooks: ChainhooksConfig {
332337
max_stacks_registrations: 10,
333338
max_bitcoin_registrations: 10,
339+
enable_http_api: true,
334340
},
335341
network: IndexerConfig {
336342
stacks_node_rpc_url: "http://0.0.0.0:20443".into(),

components/chainhook-cli/src/service/mod.rs

+82-64
Original file line numberDiff line numberDiff line change
@@ -39,70 +39,39 @@ impl Service {
3939
Self { config, ctx }
4040
}
4141

42-
pub async fn run(&mut self) -> Result<(), String> {
42+
pub async fn run(
43+
&mut self,
44+
mut predicates: Vec<ChainhookFullSpecification>,
45+
) -> Result<(), String> {
4346
let mut chainhook_config = ChainhookConfig::new();
4447

45-
{
46-
let redis_config = self.config.expected_redis_config();
47-
let client = redis::Client::open(redis_config.uri.clone()).unwrap();
48-
let mut redis_con = match client.get_connection() {
49-
Ok(con) => con,
50-
Err(message) => {
51-
error!(
48+
if predicates.is_empty() {
49+
let mut registered_predicates = load_predicates_from_redis(&self.config, &self.ctx)?;
50+
predicates.append(&mut registered_predicates);
51+
}
52+
53+
for predicate in predicates.into_iter() {
54+
match chainhook_config.register_hook(
55+
(
56+
&self.config.network.bitcoin_network,
57+
&self.config.network.stacks_network,
58+
),
59+
predicate,
60+
&ApiKey(None),
61+
) {
62+
Ok(spec) => {
63+
info!(
5264
self.ctx.expect_logger(),
53-
"Unable to connect to redis server: {}",
54-
message.to_string()
65+
"Predicate {} retrieved from storage and loaded",
66+
spec.uuid(),
5567
);
56-
std::thread::sleep(std::time::Duration::from_secs(1));
57-
std::process::exit(1);
5868
}
59-
};
60-
61-
let chainhooks_to_load: Vec<String> = redis_con
62-
.scan_match("chainhook:*:*:*")
63-
.expect("unable to retrieve prunable entries")
64-
.into_iter()
65-
.collect();
66-
67-
for key in chainhooks_to_load.iter() {
68-
let chainhook = match redis_con.hget::<_, _, String>(key, "specification") {
69-
Ok(spec) => {
70-
ChainhookFullSpecification::deserialize_specification(&spec, key).unwrap()
71-
// todo
72-
}
73-
Err(e) => {
74-
error!(
75-
self.ctx.expect_logger(),
76-
"unable to load chainhook associated with key {}: {}",
77-
key,
78-
e.to_string()
79-
);
80-
continue;
81-
}
82-
};
83-
84-
match chainhook_config.register_hook(
85-
(
86-
&self.config.network.bitcoin_network,
87-
&self.config.network.stacks_network,
88-
),
89-
chainhook,
90-
&ApiKey(None),
91-
) {
92-
Ok(spec) => {
93-
info!(
94-
self.ctx.expect_logger(),
95-
"Predicate {} retrieved from storage and loaded",
96-
spec.uuid(),
97-
);
98-
}
99-
Err(e) => {
100-
error!(
101-
self.ctx.expect_logger(),
102-
"Failed loading predicate from storage: {}",
103-
e.to_string()
104-
);
105-
}
69+
Err(e) => {
70+
error!(
71+
self.ctx.expect_logger(),
72+
"Failed loading predicate from storage: {}",
73+
e.to_string()
74+
);
10675
}
10776
}
10877
}
@@ -133,11 +102,13 @@ impl Service {
133102
}
134103
}
135104

136-
info!(
137-
self.ctx.expect_logger(),
138-
"Listening for chainhook predicate registrations on port {}",
139-
event_observer_config.control_port
140-
);
105+
if self.config.chainhooks.enable_http_api {
106+
info!(
107+
self.ctx.expect_logger(),
108+
"Listening for chainhook predicate registrations on port {}",
109+
event_observer_config.control_port
110+
);
111+
}
141112

142113
// let ordinal_index = match initialize_ordinal_index(&event_observer_config, None, &self.ctx)
143114
// {
@@ -618,3 +589,50 @@ fn update_storage_with_confirmed_stacks_blocks(
618589
redis_con.set(&format!("stx:tip"), block.block_identifier.index);
619590
}
620591
}
592+
593+
fn load_predicates_from_redis(
594+
config: &Config,
595+
ctx: &Context,
596+
) -> Result<Vec<ChainhookFullSpecification>, String> {
597+
let redis_config = config.expected_redis_config();
598+
let client = redis::Client::open(redis_config.uri.clone()).unwrap();
599+
let mut redis_con = match client.get_connection() {
600+
Ok(con) => con,
601+
Err(message) => {
602+
error!(
603+
ctx.expect_logger(),
604+
"Unable to connect to redis server: {}",
605+
message.to_string()
606+
);
607+
std::thread::sleep(std::time::Duration::from_secs(1));
608+
std::process::exit(1);
609+
}
610+
};
611+
612+
let chainhooks_to_load: Vec<String> = redis_con
613+
.scan_match("chainhook:*:*:*")
614+
.expect("unable to retrieve prunable entries")
615+
.into_iter()
616+
.collect();
617+
618+
let mut predicates = vec![];
619+
for key in chainhooks_to_load.iter() {
620+
let chainhook = match redis_con.hget::<_, _, String>(key, "specification") {
621+
Ok(spec) => {
622+
ChainhookFullSpecification::deserialize_specification(&spec, key).unwrap()
623+
// todo
624+
}
625+
Err(e) => {
626+
error!(
627+
ctx.expect_logger(),
628+
"unable to load chainhook associated with key {}: {}",
629+
key,
630+
e.to_string()
631+
);
632+
continue;
633+
}
634+
};
635+
predicates.push(chainhook);
636+
}
637+
Ok(predicates)
638+
}

components/chainhook-event-observer/src/observer/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ pub struct EventObserverConfig {
131131
pub event_handlers: Vec<EventHandler>,
132132
pub ingestion_port: u16,
133133
pub control_port: u16,
134+
pub control_api_enabled: bool,
134135
pub bitcoind_rpc_username: String,
135136
pub bitcoind_rpc_password: String,
136137
pub bitcoind_rpc_url: String,

components/chainhook-event-observer/src/observer/tests/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ fn generate_test_config() -> (EventObserverConfig, ChainhookStore) {
3333
event_handlers: vec![],
3434
ingestion_port: 0,
3535
control_port: 0,
36+
control_api_enabled: false,
3637
bitcoind_rpc_username: "user".into(),
3738
bitcoind_rpc_password: "user".into(),
3839
bitcoind_rpc_url: "http://localhost:18443".into(),

0 commit comments

Comments
 (0)