-
Notifications
You must be signed in to change notification settings - Fork 41
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
jpraynaud
wants to merge
33
commits into
main
Choose a base branch
from
jpraynaud/2214-client-library-cardano-database-restore
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,713
−281
Draft
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 2eaac11
feat: add 'DownloadUnpackOptions' struct in Cardano database client
jpraynaud 842c1b1
chore: add discriminant for 'ImmutablesLocation'
jpraynaud 5075cc3
feat: implement 'MultiFilesUri' template expansion
jpraynaud acbcbff
feat: scaffold file downloader module
jpraynaud 0201197
feat: implement a file downloader resolver trait
jpraynaud fa827cf
feat: implement immutable files download in Cardano database client
jpraynaud ec998c3
chore: add discriminant for 'DigestLocation'
jpraynaud 5b6097e
feat: implement a file downloader resolver for digests
jpraynaud 9c646c0
feat: implement digests file download in Cardano database client
jpraynaud d8311ca
chore: add discriminant for 'AncillaryLocation'
jpraynaud 89bd1c3
feat: implement a file downloader resolver for ancillary file
jpraynaud d4b86bb
feat: implement ancillary file download in Cardano database client
jpraynaud 678b310
refactor: simplify tests with 'MockFileDownloaderBuilder' in Cardano …
jpraynaud b6c2436
refactor: extend 'ImmutableDigester' trait
jpraynaud 348effb
fix: add missing conversion for 'MKTreeNode'
jpraynaud a93f5c6
feat: implement Merkle proof computation in Cardano database client
jpraynaud 1fa78d0
feat: compute Cardano database messages in message service
jpraynaud a8ef100
refactor: 'download_unpack' function signature in Cardano database cl…
jpraynaud 774026a
feat: add support for feedback sender in Cardano database client
jpraynaud f97869c
fix: add catchall in handling of feedback events in client CLI
jpraynaud 9043a19
fix: add catchall in handling of feedback events in client example
jpraynaud 0979bea
feat: add feedback events for immutable downloads in Cardano database…
jpraynaud da7b3c7
refactor: 'FileUploader' uses a feedback event builder
jpraynaud 26c1d03
feat: add feedback events for ancillary downloads in Cardano database…
jpraynaud 6caff21
feat: add feedback events for digest download in Cardano database client
jpraynaud 9e74f5f
refactor: make immutable files download more readable
jpraynaud fe2186e
docs: add examples in Cardano database client
jpraynaud 91f0125
feat: implement parallel download for immutable files download
jpraynaud e2576ac
wip: implement HTTP file downloader
jpraynaud c34e1d0
wip: add file downloader dependency injection
jpraynaud 6c4cb3a
wip: integration test
jpraynaud 3c2ca6b
wip: add example
jpraynaud File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
target/ | ||
client-cardano-database | ||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# Mithril client library example: Cardano database | ||
|
||
TODO: Write this README. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
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, | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?