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: client library for incremental Cardano database #2289

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b487cdc
feat: add 'ImmutableFileRange' enum in Cardano database client
jpraynaud Jan 28, 2025
2eaac11
feat: add 'DownloadUnpackOptions' struct in Cardano database client
jpraynaud Jan 28, 2025
842c1b1
chore: add discriminant for 'ImmutablesLocation'
jpraynaud Jan 31, 2025
5075cc3
feat: implement 'MultiFilesUri' template expansion
jpraynaud Jan 31, 2025
acbcbff
feat: scaffold file downloader module
jpraynaud Jan 31, 2025
0201197
feat: implement a file downloader resolver trait
jpraynaud Jan 31, 2025
fa827cf
feat: implement immutable files download in Cardano database client
jpraynaud Jan 31, 2025
ec998c3
chore: add discriminant for 'DigestLocation'
jpraynaud Jan 31, 2025
5b6097e
feat: implement a file downloader resolver for digests
jpraynaud Jan 31, 2025
9c646c0
feat: implement digests file download in Cardano database client
jpraynaud Jan 31, 2025
d8311ca
chore: add discriminant for 'AncillaryLocation'
jpraynaud Feb 4, 2025
89bd1c3
feat: implement a file downloader resolver for ancillary file
jpraynaud Feb 3, 2025
d4b86bb
feat: implement ancillary file download in Cardano database client
jpraynaud Feb 3, 2025
678b310
refactor: simplify tests with 'MockFileDownloaderBuilder' in Cardano …
jpraynaud Feb 3, 2025
b6c2436
refactor: extend 'ImmutableDigester' trait
jpraynaud Feb 4, 2025
348effb
fix: add missing conversion for 'MKTreeNode'
jpraynaud Feb 4, 2025
a93f5c6
feat: implement Merkle proof computation in Cardano database client
jpraynaud Feb 4, 2025
1fa78d0
feat: compute Cardano database messages in message service
jpraynaud Feb 7, 2025
a8ef100
refactor: 'download_unpack' function signature in Cardano database cl…
jpraynaud Feb 6, 2025
774026a
feat: add support for feedback sender in Cardano database client
jpraynaud Feb 5, 2025
f97869c
fix: add catchall in handling of feedback events in client CLI
jpraynaud Feb 5, 2025
9043a19
fix: add catchall in handling of feedback events in client example
jpraynaud Feb 5, 2025
0979bea
feat: add feedback events for immutable downloads in Cardano database…
jpraynaud Feb 5, 2025
da7b3c7
refactor: 'FileUploader' uses a feedback event builder
jpraynaud Feb 5, 2025
26c1d03
feat: add feedback events for ancillary downloads in Cardano database…
jpraynaud Feb 5, 2025
6caff21
feat: add feedback events for digest download in Cardano database client
jpraynaud Feb 5, 2025
9e74f5f
refactor: make immutable files download more readable
jpraynaud Feb 5, 2025
fe2186e
docs: add examples in Cardano database client
jpraynaud Feb 7, 2025
91f0125
feat: implement parallel download for immutable files download
jpraynaud Feb 7, 2025
e2576ac
wip: implement HTTP file downloader
jpraynaud Jan 31, 2025
c34e1d0
wip: add file downloader dependency injection
jpraynaud Feb 6, 2025
6c4cb3a
wip: integration test
jpraynaud Feb 4, 2025
3c2ca6b
wip: add example
jpraynaud Feb 6, 2025
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
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"examples/client-cardano-transaction",
"examples/client-mithril-stake-distribution",
"examples/client-snapshot",
"examples/client-cardano-database",
"internal/mithril-build-script",
"internal/mithril-doc",
"internal/mithril-doc-derive",
Expand Down
3 changes: 3 additions & 0 deletions examples/client-cardano-database/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
target/
client-cardano-database
.DS_Store
19 changes: 19 additions & 0 deletions examples/client-cardano-database/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "client-cardano-database"
description = "Mithril client Cardano database example"
version = "0.0.1"
authors = ["[email protected]", "[email protected]"]
documentation = "https://mithril.network/doc"
edition = "2021"
homepage = "https://mithril.network"
license = "Apache-2.0"
repository = "https://github.com/input-output-hk/mithril/"

[dependencies]
anyhow = "1.0.95"
async-trait = "0.1.86"
clap = { version = "4.5.28", features = ["derive", "env"] }
futures = "0.3.31"
indicatif = "0.17.11"
mithril-client = { path = "../../mithril-client", features = ["fs", "unstable"] }
tokio = { version = "1.43.0", features = ["full"] }
3 changes: 3 additions & 0 deletions examples/client-cardano-database/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Mithril client library example: Cardano database

TODO: Write this README.
252 changes: 252 additions & 0 deletions examples/client-cardano-database/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
//! This example shows how to implement a Mithril client and use its features.
//!
//! In this example, the client interacts by default with a real aggregator (`release-preprod`) to get the data.
//!
//! A [FeedbackReceiver] using [indicatif] is used to nicely report the progress to the console.

use anyhow::{anyhow, Context};
use async_trait::async_trait;
use clap::Parser;
use futures::Future;
use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle};
use mithril_client::cardano_database_client::{DownloadUnpackOptions, ImmutableFileRange};
use std::fmt::Write;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;

use mithril_client::feedback::{FeedbackReceiver, MithrilEvent};
use mithril_client::{ClientBuilder, MessageBuilder, MithrilResult};

#[derive(Parser, Debug)]
#[command(version)]
pub struct Args {
/// Genesis verification key.
#[clap(
long,
env = "GENESIS_VERIFICATION_KEY",
default_value = "5b33322c3235332c3138362c3230312c3137372c31312c3131372c3133352c3138372c3136372c3138312c3138382c32322c35392c3230362c3130352c3233312c3135302c3231352c33302c37382c3231322c37362c31362c3235322c3138302c37322c3133342c3133372c3234372c3136312c36385d"
)]
genesis_verification_key: String,

/// Aggregator endpoint URL.
#[clap(
long,
env = "AGGREGATOR_ENDPOINT",
default_value = "http://localhost:8080/aggregator"
)]
aggregator_endpoint: String,
}

#[tokio::main]
async fn main() -> MithrilResult<()> {
let args = Args::parse();
let work_dir = get_temp_dir()?;
let progress_bar = indicatif::MultiProgress::new();
let client =
ClientBuilder::aggregator(&args.aggregator_endpoint, &args.genesis_verification_key)
.add_feedback_receiver(Arc::new(IndicatifFeedbackReceiver::new(&progress_bar)))
.build()?;

let cardano_database_snapshots = client.cardano_database().list().await?;

let latest_hash = cardano_database_snapshots
.first()
.ok_or(anyhow!(
"No Cardano database snapshot could be listed from aggregator: '{}'",
args.aggregator_endpoint
))?
.hash
.as_ref();

let cardano_database_snapshot =
client
.cardano_database()
.get(latest_hash)
.await?
.ok_or(anyhow!(
"A Cardano database should exist for hash '{latest_hash}'"
))?;

let unpacked_dir = work_dir.join("unpack");
std::fs::create_dir(&unpacked_dir).unwrap();

let certificate = client
.certificate()
.verify_chain(&cardano_database_snapshot.certificate_hash)
.await?;

let immutable_file_range = ImmutableFileRange::Range(3, 6);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would downloading with ImmutableFileRange::Range be the most common use case?

let download_unpack_options = DownloadUnpackOptions {
allow_override: true,
include_ancillary: true,
};
client
.cardano_database()
.download_unpack(
&cardano_database_snapshot,
&immutable_file_range,
&unpacked_dir,
download_unpack_options,
)
.await?;

let merkle_proof = client
.cardano_database()
.compute_merkle_proof(&certificate, &immutable_file_range, &unpacked_dir)
.await?;
merkle_proof
.verify()
.with_context(|| "Merkle proof verification failed")?;

//TODO: Add statistics

println!(
"Computing Cardano database snapshot '{}' message ...",
cardano_database_snapshot.hash
);
let message = wait_spinner(
&progress_bar,
MessageBuilder::new().compute_cardano_database_message(&certificate, &merkle_proof),
)
.await?;

if certificate.match_message(&message) {
Ok(())
} else {
Err(anyhow::anyhow!(
"Certificate and message did not match:\ncertificate_message: '{}'\n computed_message: '{}'",
certificate.signed_message,
message.compute_hash()
))
}
}

pub struct IndicatifFeedbackReceiver {
progress_bar: MultiProgress,
download_pb: RwLock<Option<ProgressBar>>,
certificate_validation_pb: RwLock<Option<ProgressBar>>,
}

impl IndicatifFeedbackReceiver {
pub fn new(progress_bar: &MultiProgress) -> Self {
Self {
progress_bar: progress_bar.clone(),
download_pb: RwLock::new(None),
certificate_validation_pb: RwLock::new(None),
}
}
}

#[async_trait]
impl FeedbackReceiver for IndicatifFeedbackReceiver {
async fn handle_event(&self, event: MithrilEvent) {
match event {
MithrilEvent::ImmutableDownloadStarted {
immutable_file_number,
download_id: _,
} => {
let size = 1_000_000;
println!("Starting download of immutable files '{immutable_file_number:0>5}'");
let pb = ProgressBar::new(size);
pb.set_style(ProgressStyle::with_template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})")
.unwrap()
.with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap())
.progress_chars("#>-"));
self.progress_bar.add(pb.clone());
let mut download_pb = self.download_pb.write().await;
*download_pb = Some(pb);
}
MithrilEvent::ImmutableDownloadProgress {
download_id: _,
downloaded_bytes,
size: _,
} => {
let download_pb = self.download_pb.read().await;
if let Some(progress_bar) = download_pb.as_ref() {
progress_bar.set_position(downloaded_bytes);
}
}
MithrilEvent::ImmutableDownloadCompleted { download_id: _ } => {
let mut download_pb = self.download_pb.write().await;
if let Some(progress_bar) = download_pb.as_ref() {
progress_bar.finish_with_message("Immutable download completed");
}
*download_pb = None;
}
MithrilEvent::CertificateChainValidationStarted {
certificate_chain_validation_id: _,
} => {
println!("Validating certificate chain ...");
let pb = ProgressBar::new_spinner();
self.progress_bar.add(pb.clone());
let mut certificate_validation_pb = self.certificate_validation_pb.write().await;
*certificate_validation_pb = Some(pb);
}
MithrilEvent::CertificateValidated {
certificate_chain_validation_id: _,
certificate_hash,
} => {
let certificate_validation_pb = self.certificate_validation_pb.read().await;
if let Some(progress_bar) = certificate_validation_pb.as_ref() {
progress_bar.set_message(format!("Certificate '{certificate_hash}' is valid"));
progress_bar.inc(1);
}
}
MithrilEvent::CertificateFetchedFromCache {
certificate_chain_validation_id: _,
certificate_hash,
} => {
let certificate_validation_pb = self.certificate_validation_pb.read().await;
if let Some(progress_bar) = certificate_validation_pb.as_ref() {
progress_bar.set_message(format!("Cached '{certificate_hash}'"));
progress_bar.inc(1);
}
}
MithrilEvent::CertificateChainValidated {
certificate_chain_validation_id: _,
} => {
let mut certificate_validation_pb = self.certificate_validation_pb.write().await;
if let Some(progress_bar) = certificate_validation_pb.as_ref() {
progress_bar.finish_with_message("Certificate chain validated");
}
*certificate_validation_pb = None;
}
_ => {
//println!("Unexpected event: {:?}", event)
}
}
}
}

fn get_temp_dir() -> MithrilResult<PathBuf> {
let dir = std::env::temp_dir()
.join("mithril_examples")
.join("cardano_database_snapshot");

if dir.exists() {
std::fs::remove_dir_all(&dir).with_context(|| format!("Could not remove dir {dir:?}"))?;
}
std::fs::create_dir_all(&dir).with_context(|| format!("Could not create dir {dir:?}"))?;

Ok(dir)
}

async fn wait_spinner<T>(
progress_bar: &MultiProgress,
future: impl Future<Output = MithrilResult<T>>,
) -> MithrilResult<T> {
let pb = progress_bar.add(ProgressBar::new_spinner());
let spinner = async move {
loop {
pb.tick();
tokio::time::sleep(Duration::from_millis(50)).await;
}
};

tokio::select! {
_ = spinner => Err(anyhow!("timeout")),
res = future => res,
}
}
1 change: 1 addition & 0 deletions examples/client-snapshot/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ impl FeedbackReceiver for IndicatifFeedbackReceiver {
}
*certificate_validation_pb = None;
}
_ => panic!("Unexpected event: {:?}", event),
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion mithril-client-cli/src/utils/feedback_receiver.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use async_trait::async_trait;
use indicatif::{ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle};
use slog::Logger;
use slog::{warn, Logger};
use std::fmt::Write;
use tokio::sync::RwLock;

Expand Down Expand Up @@ -111,6 +111,10 @@ impl FeedbackReceiver for IndicatifFeedbackReceiver {
}
*certificate_validation_pb = None;
}
_ => {
// TODO: Handle other events from Cardano database client and remove this catchall
warn!(self.logger, "Unhandled event"; "event" => ?event);
}
}
}
}
Loading
Loading