Skip to content

Commit 92e8026

Browse files
authored
Implement a non-consensus query node that reads data using the light client (#3902)
* Implement a non-consensus query node that reads data using the light client * Fix cargo features * Fix sqlite db configuration * Add light client fetch methods for when you already have the header * Make sleep configurable * Fix clippy
1 parent 01b8d26 commit 92e8026

File tree

15 files changed

+1092
-38
lines changed

15 files changed

+1092
-38
lines changed

.typos.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ extend-exclude = [
1313
"crates/hotshot/orchestrator/run-config.toml",
1414
"crates/hotshot/macros/src/lib.rs",
1515
"crates/hotshot/types/src/light_client.rs",
16+
"light-client-query-service/genesis/*.toml",
1617
"staking-cli/tests/cli.rs",
1718
"sdks/go/types/common/consts.go",
1819
"sequencer/src/genesis.rs",

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ members = [
3131
"hotshot-query-service",
3232
"hotshot-state-prover",
3333
"light-client",
34+
"light-client-query-service",
3435
"node-metrics",
3536
"request-response",
3637
"sdks/crypto-helper",
@@ -72,6 +73,8 @@ default-members = [
7273
"hotshot-events-service",
7374
"hotshot-query-service",
7475
"hotshot-state-prover",
76+
"light-client",
77+
"light-client-query-service",
7578
"node-metrics",
7679
"request-response",
7780
"sdks/crypto-helper",

justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ fix *args:
6464
just clippy --fix {{args}}
6565

6666
lint *args:
67-
just clippy -- -D warnings
67+
just clippy {{args}} -- -D warnings
6868

6969
clippy *args:
7070
# check all targets in default workspace members
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
[package]
2+
name = "light-client-query-service"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
default-run = "light-client-query-service"
7+
8+
[[bin]]
9+
name = "genesis"
10+
required-features = ["decaf", "genesis"]
11+
12+
[features]
13+
default = ["decaf", "genesis"]
14+
decaf = ["light-client/decaf"]
15+
genesis = ["hotshot-types", "surf-disco"]
16+
17+
[dependencies]
18+
anyhow = { workspace = true }
19+
clap = { workspace = true }
20+
espresso-types = { path = "../types" }
21+
hotshot-query-service = { path = "../hotshot-query-service" }
22+
light-client = { path = "../light-client", features = ["clap"] }
23+
log-panics = { workspace = true }
24+
semver = { workspace = true }
25+
sequencer = { path = "../sequencer" }
26+
tide-disco = { workspace = true }
27+
tokio = { workspace = true }
28+
toml = { workspace = true }
29+
tracing = { workspace = true }
30+
tracing-subscriber = { workspace = true }
31+
vbs = { workspace = true }
32+
33+
# Optional dependencies for genesis utility.
34+
hotshot-types = { path = "../crates/hotshot/types", optional = true }
35+
surf-disco = { workspace = true, optional = true }
36+
37+
[lints]
38+
workspace = true

light-client-query-service/genesis/decaf.toml

Lines changed: 403 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
use std::{fs, path::PathBuf, process::ExitCode};
2+
3+
use anyhow::{Context, Result};
4+
use clap::Parser;
5+
use espresso_types::{config::PublicNetworkConfig, DrbAndHeaderUpgradeVersion, Header};
6+
use hotshot_types::{
7+
data::EpochNumber, traits::node_implementation::ConsensusTime, utils::epoch_from_block_number,
8+
};
9+
use light_client::state::Genesis;
10+
use light_client_query_service::{init_logging, LogFormat};
11+
use sequencer::SequencerApiVersion;
12+
use surf_disco::{Client, Url};
13+
use tracing::instrument;
14+
use vbs::version::StaticVersionType;
15+
16+
/// Generate a light client genesis file for an existing chain.
17+
///
18+
/// WARNING: the genesis file is constructed from information provided by an untrusted query
19+
/// service. It cannot be verified automatically because the genesis file itself is required to
20+
/// verify chain data. If this program is used to generate a light client genesis file, it is
21+
/// extremely important that a human reviews and verifies the accuracy of the generated file.
22+
#[derive(Debug, Parser)]
23+
struct Options {
24+
/// Destination file path for genesis file (default stdout).
25+
#[clap(short, long, env = "LIGHT_CLIENT_GENESIS")]
26+
output: Option<PathBuf>,
27+
28+
/// URL for a trusted Espresso query service.
29+
#[clap(short, long, env = "LIGHT_CLIENT_ESPRESSO_URL")]
30+
espresso_url: Url,
31+
32+
/// Enable testnet-only configuration.
33+
#[cfg(feature = "decaf")]
34+
#[clap(long, env = "LIGHT_CLIENT_DECAF")]
35+
decaf: bool,
36+
37+
/// Formatting options for tracing.
38+
#[clap(long, env = "RUST_LOG_FORMAT")]
39+
log_format: Option<LogFormat>,
40+
}
41+
42+
impl Options {
43+
#[instrument(skip(self))]
44+
async fn find_genesis(&self) -> Result<Genesis> {
45+
let client = Client::<hotshot_query_service::Error, SequencerApiVersion>::new(
46+
self.espresso_url.clone(),
47+
);
48+
49+
// Find the epoch height.
50+
let config: PublicNetworkConfig = client
51+
.get("config/hotshot")
52+
.send()
53+
.await
54+
.context("fetching HotShot config")?;
55+
let epoch_height = config.hotshot_config().blocks_per_epoch();
56+
57+
// Check if we are enabling Decaf-specific options.
58+
#[cfg(feature = "decaf")]
59+
let decaf = self.decaf;
60+
#[cfg(not(feature = "decaf"))]
61+
let decaf = false;
62+
63+
// Get a lower bound on the block height where the upgrade to version 0.4 occurred.
64+
let lower_bound_0_4 = if decaf {
65+
// For Decaf, we know that the first PoS epoch happened before 0.4, since Decaf deployed 0.3.
66+
config.hotshot_config().epoch_start_block()
67+
} else {
68+
// Mainnet went straight to 0.4, so we don't have a useful lower bound.
69+
0
70+
};
71+
72+
// Get an upper bound on the block height where the upgrade to version 0.4 occurred.
73+
let upper_bound_0_4 = if decaf {
74+
// On Decaf we don't necessarily know the upper bound, except that it occurred before
75+
// this service was implemented, and so before the current block height.
76+
client
77+
.get("node/block-height")
78+
.send()
79+
.await
80+
.context("getting chain height")?
81+
} else {
82+
// On Mainnet, PoS was only enabled after the version 0.4 upgrade, so we know the
83+
// upgrade occurred before the first epoch.
84+
config.hotshot_config().epoch_start_block()
85+
};
86+
87+
// Through binary search, find the first block where the upgrade to version 0.4 occurred.
88+
let target_version = DrbAndHeaderUpgradeVersion::VERSION;
89+
let mut start = lower_bound_0_4;
90+
let mut end = upper_bound_0_4;
91+
tracing::info!(start, end, "searching for upgrade to version 0.4 in range");
92+
93+
// Search invariants:
94+
// * `start` is a block strictly before the upgrade (i.e. with version < 0.4)
95+
// * `end` is a block after the upgrade (i.e. with version >= 0.4)
96+
while start + 1 < end {
97+
let midpoint = (start + end) / 2;
98+
let header: Header = client
99+
.get(&format!("availability/header/{midpoint}"))
100+
.send()
101+
.await
102+
.context(format!("fetching header {midpoint}"))?;
103+
tracing::debug!(
104+
start,
105+
midpoint,
106+
end,
107+
version = %header.version(),
108+
"test midpoint"
109+
);
110+
if header.version() < target_version {
111+
start = midpoint;
112+
} else {
113+
end = midpoint;
114+
}
115+
}
116+
let upgrade_block = start + 1;
117+
let start_epoch = epoch_from_block_number(upgrade_block, epoch_height);
118+
tracing::info!(
119+
"found upgrade to version 0.4 at block {upgrade_block}, epoch {start_epoch}"
120+
);
121+
122+
// Start from the third epoch, since we need the prior epoch's root to have the upgraded
123+
// header with the stake table hash.
124+
let start_epoch = start_epoch + 2;
125+
126+
Ok(Genesis {
127+
epoch_height,
128+
first_epoch_with_dynamic_stake_table: EpochNumber::new(start_epoch),
129+
stake_table: config
130+
.hotshot_config()
131+
.known_nodes_with_stake()
132+
.iter()
133+
.map(|node| node.stake_table_entry.clone())
134+
.collect(),
135+
#[cfg(feature = "decaf")]
136+
decaf_first_pos_epoch: decaf.then_some(EpochNumber::new(
137+
epoch_from_block_number(lower_bound_0_4, epoch_height) + 2,
138+
)),
139+
})
140+
}
141+
}
142+
143+
async fn run() -> Result<()> {
144+
let opt = Options::parse();
145+
init_logging(opt.log_format);
146+
147+
let genesis = opt.find_genesis().await?;
148+
let toml = toml::to_string_pretty(&genesis).context("serializing genesis")?;
149+
if let Some(output) = opt.output {
150+
fs::write(output, toml).context("writing genesis file")?;
151+
} else {
152+
println!("{toml}");
153+
}
154+
155+
Ok(())
156+
}
157+
158+
#[tokio::main]
159+
async fn main() -> ExitCode {
160+
if let Err(err) = run().await {
161+
tracing::error!("{err:#}");
162+
ExitCode::FAILURE
163+
} else {
164+
ExitCode::SUCCESS
165+
}
166+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
use clap::ValueEnum;
2+
use log_panics::BacktraceMode;
3+
use tracing_subscriber::{fmt::format::FmtSpan, EnvFilter};
4+
5+
/// Controls how logs are displayed and how backtraces are logged on panic.
6+
///
7+
/// The values here match the possible values of `RUST_LOG_FORMAT`, and their corresponding behavior
8+
/// on backtrace logging is:
9+
/// * `full`: print a prettified dump of the stack trace and span trace to stdout, optimized for
10+
/// human readability rather than machine parsing
11+
/// * `compact`: output the default panic message, with backtraces controlled by `RUST_BACKTRACE`
12+
/// * `json`: output the panic message and stack trace as a tracing event. This in turn works with
13+
/// the behavior of the tracing subscriber with `RUST_LOG_FORMAT=json` to output the event in a
14+
/// machine-parseable, JSON format.
15+
#[derive(Clone, Copy, Debug, Default, ValueEnum)]
16+
pub enum LogFormat {
17+
#[default]
18+
Full,
19+
Compact,
20+
Json,
21+
}
22+
23+
pub fn init_logging(fmt: Option<LogFormat>) {
24+
// Parse the `RUST_LOG_SPAN_EVENTS` environment variable
25+
let span_event_filter = match std::env::var("RUST_LOG_SPAN_EVENTS") {
26+
Ok(val) => val
27+
.split(',')
28+
.map(|s| match s.trim() {
29+
"new" => FmtSpan::NEW,
30+
"enter" => FmtSpan::ENTER,
31+
"exit" => FmtSpan::EXIT,
32+
"close" => FmtSpan::CLOSE,
33+
"active" => FmtSpan::ACTIVE,
34+
"full" => FmtSpan::FULL,
35+
_ => FmtSpan::NONE,
36+
})
37+
.fold(FmtSpan::NONE, |acc, x| acc | x),
38+
Err(_) => FmtSpan::NONE,
39+
};
40+
41+
let subscriber = tracing_subscriber::fmt()
42+
.with_env_filter(EnvFilter::from_default_env())
43+
.with_span_events(span_event_filter);
44+
45+
// Conditionally initialize in `json` mode
46+
if let LogFormat::Json = fmt.unwrap_or_default() {
47+
let _ = subscriber.json().try_init();
48+
log_panics::Config::new()
49+
.backtrace_mode(BacktraceMode::Resolved)
50+
.install_panic_hook();
51+
} else {
52+
let _ = subscriber.try_init();
53+
}
54+
}

0 commit comments

Comments
 (0)