Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7c1f487
feat(aggregator): add IPFS gateway configuration
Alenar Aug 18, 2026
3753757
feat(aggregator): introduce Kubo RPC client with query and response h…
Alenar Aug 19, 2026
3c3b120
feat(aggregator): add IPFS query implementations for Kubo RPC client
Alenar Aug 20, 2026
2320f83
feat(aggregator): add IPFS file uploader
Alenar Aug 20, 2026
e15e7c7
feat(aggregator): integrate IPFS uploader into dependency injection a…
Alenar Aug 20, 2026
d07b266
refactor(aggregator): simplify IPFS file upload
Alenar Aug 20, 2026
6eb9769
fix(aggregator): missing error backtrace in archive uploads
Alenar Aug 21, 2026
fccb5aa
refactor(aggregator): replace `PathBuf` with `IpfsMfsDirPath` for IPF…
Alenar Aug 21, 2026
afb170e
refactor(aggregator): harden `IpfsBackendUploader::file_exists` to su…
Alenar Aug 24, 2026
ef77bce
feat(aggregator): preliminary `no_copy` support to `IpfsAddQuery` wit…
Alenar Aug 24, 2026
56acee8
feat(aggregator): enforce CID determinism in `IpfsAddQuery` by specif…
Alenar Aug 24, 2026
cf5d7fe
feat: introduce support for IPFS locations in Cardano database artifacts
Alenar Aug 24, 2026
4003efe
refactor(aggregator): rework IPFS server RPC configuration, adding mf…
Alenar Aug 24, 2026
8eddeb3
refactor(aggregator): replace `from` method with `From` trait impleme…
Alenar Aug 25, 2026
f4e6908
feat(aggregator): dynamic timeout in `IpfsAddQuery` based on file size
Alenar Aug 25, 2026
bd1cc12
feat(aggregator): ensure IPFS directory creation is done only once
Alenar Aug 26, 2026
f5413b3
feat(aggregator): add `IpfsFilesLsQuery` for listing MFS directories …
Alenar Aug 27, 2026
1a4d1a0
refactor(aggregator): add `IpfsMfsDirPath.join_file_name_from` to cen…
Alenar Aug 27, 2026
d7100c0
feat(aggregator): add caching for IPFS batch uploads with MFS directo…
Alenar Aug 27, 2026
12b008f
refactor(aggregator): simplify IPFS uploader by using a per batch cache
Alenar Aug 27, 2026
c8c961e
refactor(aggregator): extract retry logic into a reusable utility fun…
Alenar Aug 28, 2026
cc1d539
feat(aggregator): add retry policy support for IPFS file uploads
Alenar Aug 28, 2026
a94c053
refactor(aggregator): rename `Cid` to `IpfsCid`
Alenar Aug 28, 2026
a20529b
fix(aggregator): correctly handle IPFS ls of empty directories
Alenar Aug 28, 2026
af7c482
chore: update changelog
Alenar Aug 28, 2026
7c72430
chore: upgrade crate versions
Alenar Aug 28, 2026
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
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ As a minor extension, we have adopted a slightly different versioning convention
- Support for bytes encoding of the SNARK aggregate signatures in the certificates.
- Reduced the encoded size of the SNARK proofs by serializing their bytes as CBOR byte strings.
- Support for reading on-chain protocol configurations for both Aggregator and Signer.

- **UNSTABLE** Reworked the Mithril aggregator's file archiver to produce byte-stable archives across systems.
- Existing archives must be regenerated by the Mithril aggregator to ensure byte stability.
- Reworked the Mithril aggregator's file archiver to produce byte-stable archives across systems.
- Existing archives must be regenerated by the Mithril aggregator to ensure byte stability.
- Support IPFS for uploads of immutable files in the Mithril aggregator.

- **REMOVED** support for `Gzip` compression/decompression in the Mithril aggregator and client:
- The aggregator no longer produces or supports `Gzip` compression for snapshot-related artifacts: immutable files and ancillaries.
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ anyhow = { workspace = true }
async-trait = { workspace = true }
digest = { workspace = true }
hex = { workspace = true }
mithril-common = { path = "../../../mithril-common", version = "0.7.18" }
mithril-common = { path = "../../../mithril-common", version = "0.7.19" }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = "0.10.9"
Expand Down
2 changes: 1 addition & 1 deletion internal/mithril-aggregator-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ include = ["**/*.rs", "Cargo.toml", "README.md"]
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
mithril-common = { path = "../../mithril-common", version = "0.7.18" }
mithril-common = { path = "../../mithril-common", version = "0.7.19" }
reqwest = { workspace = true }
semver = { workspace = true }
serde = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion internal/mithril-aggregator-discovery/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ include = ["**/*.rs", "Cargo.toml", "README.md", ".gitignore"]
anyhow = { workspace = true }
async-trait = { workspace = true }
mithril-aggregator-client = { path = "../mithril-aggregator-client", version = "0.2.4" }
mithril-common = { path = "../../mithril-common", version = "0.7.18" }
mithril-common = { path = "../../mithril-common", version = "0.7.19" }
rand = { version = "0.10.2" }
reqwest = { workspace = true }
serde = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion mithril-aggregator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mithril-aggregator"
version = "0.10.0"
version = "0.10.1"
description = "A Mithril Aggregator server"
authors = { workspace = true }
edition = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,8 @@ impl AncillaryArtifactBuilder {
}
Err(e) => {
error!(
self.logger,
"Failed to upload ancillary archive";
"error" => e.to_string()
self.logger, "Failed to upload ancillary archive";
"error" => ?e
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,8 @@ impl DigestArtifactBuilder {
}
Err(e) => {
error!(
self.logger,
"Failed to upload digest file";
"error" => e.to_string()
self.logger, "Failed to upload digest file";
"error" => ?e
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ use slog::{Logger, error};

use mithril_common::{
StdResult,
entities::{CompressionAlgorithm, ImmutableFileNumber, ImmutablesLocation, MultiFilesUri},
entities::{
CompressionAlgorithm, ImmutableFileNumber, ImmutablesLocation, MultiFilesUri, TemplateUri,
},
logging::LoggerExtensions,
};

use crate::{
DumbUploader, FileUploader,
file_uploaders::{CloudUploader, LocalUploader},
file_uploaders::{CloudUploader, IpfsUploader, LocalUploader},
services::Snapshotter,
};

Expand Down Expand Up @@ -99,6 +101,24 @@ impl ImmutableFilesUploader for LocalUploader {
}
}

#[async_trait]
impl ImmutableFilesUploader for IpfsUploader {
async fn batch_upload(
Comment thread
Alenar marked this conversation as resolved.
&self,
filepaths: &[PathBuf],
compression_algorithm: Option<CompressionAlgorithm>,
) -> StdResult<ImmutablesLocation> {
let directory_cid = self.batch_upload_to_dir(filepaths).await?;

Ok(ImmutablesLocation::Ipfs {
uri: MultiFilesUri::Template(TemplateUri(format!(
"{directory_cid}/{{immutable_file_number}}.tar.zst"
))),
compression_algorithm,
})
}
}

#[async_trait]
impl ImmutableFilesUploader for CloudUploader {
async fn batch_upload(
Expand Down Expand Up @@ -241,9 +261,8 @@ impl ImmutableArtifactBuilder {
}
Err(e) => {
error!(
self.logger,
"Failed to upload immutable archive";
"error" => e.to_string()
self.logger, "Failed to upload immutable archive";
"error" => ?e
);
}
}
Expand Down
87 changes: 87 additions & 0 deletions mithril-aggregator/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ pub trait ConfigurationSource {
panic!("snapshot_use_cdn_domain is not implemented.");
}

/// Configuration parameters to connect to a Kubo IPFS RPC API, setting this will enable IPFS upload for immutable snapshots
fn ipfs_rpc_server_config(&self) -> Option<IpfsRpcServerConfig> {
panic!("ipfs_rpc_server_config is not implemented.");
}
Comment thread
Alenar marked this conversation as resolved.

/// Server listening IP
fn server_ip(&self) -> String {
panic!("server_ip is not implemented.");
Expand Down Expand Up @@ -534,6 +539,20 @@ pub struct ServeCommandConfiguration {
/// Use CDN domain to construct snapshot urls if snapshot_uploader_type is Gcp
pub snapshot_use_cdn_domain: bool,

/// URL of a Kubo IPFS RPC API, setting this will enable IPFS upload for immutable snapshots
///
/// `mfs_folder_name` (optional) allows overriding the name of the folder in the IPFS MFS (Mutable File System)
/// where the snapshots will be stored. Defaults to "mithril" if not specified.
#[example = "\
`{ \"url\": \"http://localhost:5001/\" }`\
or `{ \"url\": \"http://localhost:5001/\", \"mfs_folder_name\": \"custom-folder\" }`\
"]
#[serde(
default,
deserialize_with = "serde_deserialization::string_or_struct_optional"
)]
pub ipfs_rpc_server_config: Option<IpfsRpcServerConfig>,

/// Server listening IP
pub server_ip: String,

Expand Down Expand Up @@ -716,6 +735,44 @@ pub enum SnapshotUploaderType {
Local,
}

/// Configuration for connecting to a Kubo IPFS RPC server.
///
/// This struct holds the connection details and settings for uploading
/// immutable snapshots to IPFS via the Kubo RPC API.
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
pub struct IpfsRpcServerConfig {
/// URL of the Kubo IPFS RPC API endpoint.
///
/// Example: `http://localhost:5001/`
pub url: String,

/// Name of the folder in the MFS (Mutable File System) where snapshots will be stored.
///
/// Defaults to "mithril" if not specified.
#[serde(default = "default_mfs_folder_name")]
pub mfs_folder_name: String,
}

impl IpfsRpcServerConfig {
/// Parsed URL of a Kubo IPFS RPC API
pub fn sanitized_url(&self) -> StdResult<SanitizedUrlWithTrailingSlash> {
SanitizedUrlWithTrailingSlash::parse(&self.url)
.with_context(|| "Invalid IPFS RPC server URL")
}
}

fn default_mfs_folder_name() -> String {
"mithril".to_string()
}

impl FromStr for IpfsRpcServerConfig {
type Err = serde_json::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}

/// Configuration to connect to the Blockfrost API.
///
/// Currently only used to fetch the ticker and name for registered pools.
Expand Down Expand Up @@ -793,6 +850,7 @@ impl ServeCommandConfiguration {
snapshot_uploader_type: SnapshotUploaderType::Local,
snapshot_bucket_name: None,
snapshot_use_cdn_domain: false,
ipfs_rpc_server_config: None,
server_ip: "0.0.0.0".to_string(),
server_port: 8000,
public_server_url: None,
Expand Down Expand Up @@ -911,6 +969,10 @@ impl ConfigurationSource for ServeCommandConfiguration {
self.snapshot_use_cdn_domain
}

fn ipfs_rpc_server_config(&self) -> Option<IpfsRpcServerConfig> {
self.ipfs_rpc_server_config.clone()
}

fn server_ip(&self) -> String {
self.server_ip.clone()
}
Expand Down Expand Up @@ -1519,6 +1581,31 @@ mod test {
assert!(!config.is_follower_aggregator());
}

#[test]
fn deserializing_ipfs_rpc_server_parameters() {
let deserialized_without_mfs_dir: IpfsRpcServerConfig =
serde_json::from_str(r#"{ "url": "http://localhost:5001/" }"#).unwrap();
assert_eq!(
deserialized_without_mfs_dir,
IpfsRpcServerConfig {
url: "http://localhost:5001/".to_string(),
mfs_folder_name: default_mfs_folder_name(),
}
);

let deserialized_with_mfs_dir: IpfsRpcServerConfig = serde_json::from_str(
r#"{ "url": "http://localhost:5001/", "mfs_folder_name": "altered" }"#,
)
.unwrap();
assert_eq!(
deserialized_with_mfs_dir,
IpfsRpcServerConfig {
url: "http://localhost:5001/".to_string(),
mfs_folder_name: "altered".to_string(),
}
);
}

#[test]
fn deserializing_blockfrost_parameters() {
let deserialized_without_base_url: BlockfrostParameters =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ use crate::artifact_builder::{
DigestSnapshotter, ImmutableArtifactBuilder, ImmutableFilesUploader,
MithrilStakeDistributionArtifactBuilder,
};
use crate::configuration::AncillaryFilesSignerConfig;
use crate::configuration::{AncillaryFilesSignerConfig, IpfsRpcServerConfig};
use crate::dependency_injection::builder::SNAPSHOT_ARTIFACTS_DIR;
use crate::dependency_injection::{DependenciesBuilder, DependenciesBuilderError, Result};
use crate::file_uploaders::{
CloudRemotePath, CloudUploader, FileUploadRetryPolicy, GCloudBackendUploader, LocalUploader,
CloudRemotePath, CloudUploader, FileUploadRetryPolicy, GCloudBackendUploader, IpfsUploader,
LocalUploader,
};
use crate::get_dependency;
use crate::http_server::{CARDANO_DATABASE_DOWNLOAD_PATH, SNAPSHOT_DOWNLOAD_PATH};
Expand All @@ -29,6 +30,7 @@ use crate::services::{
SignedEntityServiceArtifactsDependencies, Snapshotter,
};
use crate::tools::DEFAULT_GCP_CREDENTIALS_JSON_ENV_VAR;
use crate::tools::kubo_rpc_client::{IpfsMfsDirPath, KuboRpcClient};
use crate::{DumbUploader, ExecutionEnvironment, FileUploader, SnapshotUploaderType};

impl DependenciesBuilder {
Expand Down Expand Up @@ -253,6 +255,20 @@ impl DependenciesBuilder {
))
}

async fn build_ipfs_uploader(
&self,
ipfs_rpc_config: IpfsRpcServerConfig,
) -> Result<IpfsUploader> {
let rpc_api_client =
KuboRpcClient::new(ipfs_rpc_config.sanitized_url()?, self.root_logger())?;
Ok(IpfsUploader::new(
Arc::new(rpc_api_client),
IpfsMfsDirPath::from(ipfs_rpc_config.mfs_folder_name),
FileUploadRetryPolicy::default(),
&self.root_logger(),
))
}

async fn build_cardano_database_ancillary_uploaders(
&self,
) -> Result<Vec<Arc<dyn AncillaryFileUploader>>> {
Expand Down Expand Up @@ -301,28 +317,36 @@ impl DependenciesBuilder {
) -> Result<Vec<Arc<dyn ImmutableFilesUploader>>> {
let logger = self.root_logger();
if self.configuration.environment() == ExecutionEnvironment::Production {
match self.configuration.snapshot_uploader_type() {
SnapshotUploaderType::Gcp => {
let allow_overwrite = false;
let remote_folder_path =
CloudRemotePath::new("cardano-database").join("immutable");

Ok(vec![Arc::new(
self.build_gcp_uploader(remote_folder_path, allow_overwrite).await?,
)])
}
SnapshotUploaderType::Local => {
let server_url_prefix = self.configuration.get_server_url()?;
let immutable_url_prefix = server_url_prefix
.sanitize_join(&format!("{CARDANO_DATABASE_DOWNLOAD_PATH}/immutable/"))?;

Ok(vec![Arc::new(LocalUploader::new_without_copy(
immutable_url_prefix,
FileUploadRetryPolicy::default(),
logger,
))])
}
let mut uploaders: Vec<Arc<dyn ImmutableFilesUploader>> =
match self.configuration.snapshot_uploader_type() {
SnapshotUploaderType::Gcp => {
let allow_overwrite = false;
let remote_folder_path =
CloudRemotePath::new("cardano-database").join("immutable");

vec![Arc::new(
self.build_gcp_uploader(remote_folder_path, allow_overwrite).await?,
)]
}
SnapshotUploaderType::Local => {
let server_url_prefix = self.configuration.get_server_url()?;
let immutable_url_prefix = server_url_prefix.sanitize_join(&format!(
"{CARDANO_DATABASE_DOWNLOAD_PATH}/immutable/"
))?;

vec![Arc::new(LocalUploader::new_without_copy(
immutable_url_prefix,
FileUploadRetryPolicy::default(),
logger,
))]
}
};

if let Some(ipfs_rpc_config) = self.configuration.ipfs_rpc_server_config() {
uploaders.push(Arc::new(self.build_ipfs_uploader(ipfs_rpc_config).await?));
Comment thread
Alenar marked this conversation as resolved.
}

Ok(uploaders)
} else {
Ok(vec![Arc::new(DumbUploader::new(
FileUploadRetryPolicy::never(),
Expand Down
Loading
Loading