rpki/src/validation/run_tree_from_tal.rs

2492 lines
82 KiB
Rust

use url::Url;
use crate::analysis::timing::TimingHandle;
use crate::audit::PublicationPointAudit;
use crate::audit_downloads::DownloadLogHandle;
use crate::ccr::CcrAccumulator;
use crate::current_repo_index::{CurrentRepoIndexHandle, CurrentRepoObject};
use crate::data_model::ta::TrustAnchor;
use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
use crate::parallel::repo_runtime::{Phase1RepoSyncRuntime, RepoSyncRuntime};
use crate::parallel::repo_worker::{
LiveRepoTransportExecutor, RepoTransportWorkerPool, RepoWorkerPoolConfig,
};
use crate::parallel::run_coordinator::GlobalRunCoordinator;
use crate::parallel::types::{TalInputSpec, TalSource};
use crate::replay::archive::ReplayArchiveIndex;
use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
use crate::replay::delta_fetch_http::PayloadDeltaReplayHttpFetcher;
use crate::replay::delta_fetch_rsync::{
PayloadDeltaReplayCurrentStoreRsyncFetcher, PayloadDeltaReplayRsyncFetcher,
};
use crate::replay::fetch_http::PayloadReplayHttpFetcher;
use crate::replay::fetch_rsync::PayloadReplayRsyncFetcher;
use crate::sync::rrdp::Fetcher;
use crate::validation::from_tal::{
DiscoveredRootCaInstance, FromTalError, canonical_tal_rsync_uri_from_bytes,
discover_root_ca_instance_from_tal_and_ta_der,
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name,
discover_root_ca_instance_from_tal_url,
discover_root_ca_instance_from_tal_url_with_strict_name,
discover_root_ca_instance_from_tal_with_fetchers,
discover_root_ca_instance_from_tal_with_fetchers_strict_name,
};
use crate::validation::objects::ParallelRoaWorkerPool;
use crate::validation::tree::{
CaInstanceHandle, TreeRunAuditOutput, TreeRunConfig, TreeRunError, TreeRunOutput,
run_tree_serial, run_tree_serial_audit, run_tree_serial_audit_multi_root,
};
use crate::validation::tree_parallel::{
run_tree_parallel_phase2_audit, run_tree_parallel_phase2_audit_multi_root,
};
use crate::validation::tree_runner::Rpkiv1PublicationPointRunner;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
fn tal_id_from_url_like(s: &str) -> Option<String> {
let url = Url::parse(s).ok()?;
if let Some(last) = url
.path_segments()
.and_then(|segments| segments.filter(|seg| !seg.is_empty()).next_back())
{
let stem = last.rsplit_once('.').map(|(stem, _)| stem).unwrap_or(last);
let trimmed = stem.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
url.host_str().map(|host| host.to_string())
}
fn derive_tal_id(discovery: &DiscoveredRootCaInstance) -> String {
discovery
.tal_url
.as_deref()
.and_then(tal_id_from_url_like)
.or_else(|| {
discovery
.trust_anchor
.resolved_ta_uri
.as_ref()
.and_then(|uri| tal_id_from_url_like(uri.as_str()))
})
.or_else(|| {
discovery
.trust_anchor
.tal
.ta_uris
.first()
.and_then(|uri| tal_id_from_url_like(uri.as_str()))
})
.unwrap_or_else(|| "unknown-tal".to_string())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunTreeFromTalOutput {
pub discovery: DiscoveredRootCaInstance,
pub tree: TreeRunOutput,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunTreeFromTalAuditOutput {
pub discovery: DiscoveredRootCaInstance,
pub discoveries: Vec<DiscoveredRootCaInstance>,
pub successful_tal_inputs: Vec<TalInputSpec>,
pub tree: TreeRunOutput,
pub publication_points: Vec<PublicationPointAudit>,
pub downloads: Vec<crate::audit::AuditDownloadEvent>,
pub download_stats: crate::audit::AuditDownloadStats,
pub current_repo_objects: Vec<CurrentRepoObject>,
pub ccr_accumulator: Option<CcrAccumulator>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TalRootDiscovery {
pub tal_input: TalInputSpec,
pub discovery: DiscoveredRootCaInstance,
pub root_handle: CaInstanceHandle,
}
fn snapshot_current_repo_objects(
current_repo_index: Option<&CurrentRepoIndexHandle>,
collect: bool,
) -> Vec<CurrentRepoObject> {
if !collect {
return Vec::new();
}
current_repo_index
.and_then(|handle| handle.lock().ok().map(|idx| idx.snapshot_objects()))
.unwrap_or_default()
}
fn make_live_runner<'a>(
store: &'a crate::storage::RocksStore,
policy: &'a crate::policy::Policy,
http_fetcher: &'a dyn Fetcher,
rsync_fetcher: &'a dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
timing: Option<TimingHandle>,
download_log: Option<DownloadLogHandle>,
current_repo_index: Option<CurrentRepoIndexHandle>,
repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
parallel_phase2_config: Option<ParallelPhase2Config>,
ccr_accumulator: Option<CcrAccumulator>,
persist_vcir: bool,
) -> Rpkiv1PublicationPointRunner<'a> {
let parallel_roa_worker_pool = parallel_phase2_config
.as_ref()
.and_then(|config| ParallelRoaWorkerPool::new(config).ok());
Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing,
download_log,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index,
repo_sync_runtime,
parallel_phase2_config,
parallel_roa_worker_pool,
ccr_accumulator: ccr_accumulator.map(Mutex::new),
persist_vcir,
}
}
fn build_phase1_repo_sync_runtime<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
http_fetcher: &H,
rsync_fetcher: &R,
parallel_config: ParallelPhase1Config,
timing: Option<TimingHandle>,
download_log: Option<DownloadLogHandle>,
tal_inputs: Vec<crate::parallel::types::TalInputSpec>,
) -> Result<(Arc<dyn RepoSyncRuntime>, CurrentRepoIndexHandle), RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let coordinator = GlobalRunCoordinator::new(parallel_config.clone(), tal_inputs);
let current_repo_index = coordinator.current_repo_index_handle();
let rsync_fetcher_arc = Arc::new(rsync_fetcher.clone());
let executor = LiveRepoTransportExecutor::new(
Arc::clone(&store),
current_repo_index.clone(),
Arc::new(http_fetcher.clone()),
Arc::clone(&rsync_fetcher_arc),
timing,
download_log,
);
let pool = RepoTransportWorkerPool::new(RepoWorkerPoolConfig::from(&parallel_config), executor)
.map_err(RunTreeFromTalError::Replay)?;
let resolver: Arc<dyn Fn(&str) -> String + Send + Sync> =
Arc::new(move |base: &str| rsync_fetcher_arc.dedup_key(base));
let _ = policy; // policy reserved for later runtime-level decisions
let runtime = Arc::new(Phase1RepoSyncRuntime::new(
coordinator,
pool,
resolver,
policy.sync_preference,
));
Ok((runtime, current_repo_index))
}
fn root_discovery_from_tal_input(
tal_input: &TalInputSpec,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
strict_name: bool,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
match &tal_input.source {
TalSource::Url(url) => {
if strict_name {
discover_root_ca_instance_from_tal_url_with_strict_name(http_fetcher, url)
} else {
discover_root_ca_instance_from_tal_url(http_fetcher, url)
}
}
TalSource::DerBytes {
tal_bytes, ta_der, ..
} => {
if strict_name {
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
tal_bytes, ta_der, None,
)
} else {
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, None)
}
}
TalSource::FilePath(path) => {
let tal_bytes = std::fs::read(path).map_err(|e| {
FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", path.display()))
})?;
let tal = crate::data_model::tal::Tal::decode_bytes(&tal_bytes)
.map_err(FromTalError::from)?;
if strict_name {
discover_root_ca_instance_from_tal_with_fetchers_strict_name(
http_fetcher,
rsync_fetcher,
tal,
None,
)
} else {
discover_root_ca_instance_from_tal_with_fetchers(
http_fetcher,
rsync_fetcher,
tal,
None,
)
}
}
TalSource::FilePathWithTa { tal_path, ta_path } => {
let tal_bytes = std::fs::read(tal_path).map_err(|e| {
FromTalError::TalFetch(format!("read TAL file failed: {}: {e}", tal_path.display()))
})?;
let ta_der = std::fs::read(ta_path).map_err(|e| {
FromTalError::TaFetch(format!("read TA file failed: {}: {e}", ta_path.display()))
})?;
let resolved_ta_uri = canonical_tal_rsync_uri_from_bytes(&tal_bytes)?;
if strict_name {
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
&tal_bytes,
&ta_der,
Some(&resolved_ta_uri),
)
} else {
discover_root_ca_instance_from_tal_and_ta_der(
&tal_bytes,
&ta_der,
Some(&resolved_ta_uri),
)
}
}
}
}
fn discover_root_ca_instance_from_tal_url_with_policy(
policy: &crate::policy::Policy,
http_fetcher: &dyn Fetcher,
tal_url: &str,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
if policy.strict.name {
discover_root_ca_instance_from_tal_url_with_strict_name(http_fetcher, tal_url)
} else {
discover_root_ca_instance_from_tal_url(http_fetcher, tal_url)
}
}
fn discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
) -> Result<DiscoveredRootCaInstance, FromTalError> {
if policy.strict.name {
discover_root_ca_instance_from_tal_and_ta_der_with_strict_name(
tal_bytes,
ta_der,
resolved_ta_uri,
)
} else {
discover_root_ca_instance_from_tal_and_ta_der(tal_bytes, ta_der, resolved_ta_uri)
}
}
fn discover_multiple_roots_from_tal_inputs(
tal_inputs: &[TalInputSpec],
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
strict_name: bool,
) -> Result<Vec<TalRootDiscovery>, RunTreeFromTalError> {
let mut roots = Vec::with_capacity(tal_inputs.len());
for tal_input in tal_inputs {
let discovery = match root_discovery_from_tal_input(
tal_input,
http_fetcher,
rsync_fetcher,
strict_name,
) {
Ok(discovery) => discovery,
Err(error)
if should_isolate_multi_tal_strict_name_failure(
tal_inputs.len(),
strict_name,
&error,
) =>
{
eprintln!(
"warning: skipping TAL '{}' because strict name validation failed during trust anchor discovery: {error}",
tal_input.tal_id
);
continue;
}
Err(error) => return Err(error.into()),
};
let root_handle = root_handle_from_trust_anchor(
&discovery.trust_anchor,
tal_input.tal_id.clone(),
None,
&discovery.ca_instance,
);
roots.push(TalRootDiscovery {
tal_input: tal_input.clone(),
discovery,
root_handle,
});
}
if roots.is_empty() {
return Err(RunTreeFromTalError::Replay(
"multi-TAL root discovery returned no usable roots after strict name filtering"
.to_string(),
));
}
Ok(roots)
}
fn should_isolate_multi_tal_strict_name_failure(
tal_input_count: usize,
strict_name: bool,
error: &FromTalError,
) -> bool {
strict_name
&& tal_input_count > 1
&& error.to_string().contains("Name strict validation failed")
}
#[derive(Debug, thiserror::Error)]
pub enum RunTreeFromTalError {
#[error("{0}")]
FromTal(#[from] FromTalError),
#[error("payload replay setup failed: {0}")]
Replay(String),
#[error("{0}")]
Tree(#[from] TreeRunError),
}
pub fn root_handle_from_trust_anchor(
trust_anchor: &TrustAnchor,
tal_id: String,
ca_certificate_rsync_uri: Option<String>,
ca_instance: &crate::validation::ca_instance::CaInstanceUris,
) -> CaInstanceHandle {
let ta_rc = trust_anchor.ta_certificate.rc_ca.clone();
CaInstanceHandle {
depth: 0,
tal_id,
parent_manifest_rsync_uri: None,
ca_certificate_der: trust_anchor.ta_certificate.raw_der.clone(),
ca_certificate_rsync_uri,
effective_ip_resources: ta_rc.tbs.extensions.ip_resources.clone(),
effective_as_resources: ta_rc.tbs.extensions.as_resources.clone(),
rsync_base_uri: ca_instance.rsync_base_uri.clone(),
manifest_rsync_uri: ca_instance.manifest_rsync_uri.clone(),
publication_point_rsync_uri: ca_instance.publication_point_rsync_uri.clone(),
rrdp_notification_uri: ca_instance.rrdp_notification_uri.clone(),
}
}
pub fn run_tree_from_tal_url_serial(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_url: &str,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
let discovery =
discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?;
let runner = make_live_runner(
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
None,
None,
None,
None,
None,
None,
config.persist_vcir,
);
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let tree = run_tree_serial(root, &runner, config)?;
Ok(RunTreeFromTalOutput { discovery, tree })
}
pub fn run_tree_from_tal_url_serial_audit(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_url: &str,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let discovery =
discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?;
let download_log = DownloadLogHandle::new();
let runner = make_live_runner(
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
None,
Some(download_log.clone()),
None,
None,
None,
None,
config.persist_vcir,
);
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_url_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_url: &str,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let discovery =
discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?;
drop(_tal);
let download_log = DownloadLogHandle::new();
let runner = make_live_runner(
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
Some(timing.clone()),
Some(download_log.clone()),
None,
None,
None,
None,
config.persist_vcir,
);
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let _tree = timing.span_phase("tree_run_total");
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
fn run_single_root_parallel_audit_inner<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
discovery: DiscoveredRootCaInstance,
tal_inputs: Vec<TalInputSpec>,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
phase2_config: Option<ParallelPhase2Config>,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let phase2_enabled = phase2_config.is_some();
let download_log = DownloadLogHandle::new();
let (runtime, current_repo_index) = build_phase1_repo_sync_runtime(
Arc::clone(&store),
policy,
http_fetcher,
rsync_fetcher,
parallel_config,
None,
Some(download_log.clone()),
tal_inputs,
)?;
let current_repo_index_for_output = current_repo_index.clone();
let runner = make_live_runner(
store.as_ref(),
policy,
http_fetcher,
rsync_fetcher,
validation_time,
None,
Some(download_log.clone()),
Some(current_repo_index),
Some(runtime),
phase2_config,
(phase2_enabled && config.build_ccr_accumulator)
.then(|| CcrAccumulator::new(vec![discovery.trust_anchor.clone()])),
config.persist_vcir,
);
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let TreeRunAuditOutput {
tree,
publication_points,
} = if phase2_enabled {
run_tree_parallel_phase2_audit(root, &runner, config)?
} else {
run_tree_serial_audit(root, &runner, config)?
};
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: snapshot_current_repo_objects(
Some(&current_repo_index_for_output),
collect_current_repo_objects,
),
ccr_accumulator: runner.ccr_accumulator_snapshot(),
})
}
fn run_multi_root_parallel_audit_inner<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_inputs: Vec<TalInputSpec>,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
phase2_config: Option<ParallelPhase2Config>,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let phase2_enabled = phase2_config.is_some();
if tal_inputs.is_empty() {
return Err(RunTreeFromTalError::Replay(
"multi-TAL run requires at least one TAL input".to_string(),
));
}
let roots = discover_multiple_roots_from_tal_inputs(
&tal_inputs,
http_fetcher,
rsync_fetcher,
policy.strict.name,
)?;
let primary = roots.first().cloned().ok_or_else(|| {
RunTreeFromTalError::Replay("multi-TAL root discovery returned no roots".to_string())
})?;
let discoveries = roots
.iter()
.map(|item| item.discovery.clone())
.collect::<Vec<_>>();
let successful_tal_inputs = roots
.iter()
.map(|item| item.tal_input.clone())
.collect::<Vec<_>>();
let root_handles = roots
.iter()
.map(|item| item.root_handle.clone())
.collect::<Vec<_>>();
let download_log = DownloadLogHandle::new();
let (runtime, current_repo_index) = build_phase1_repo_sync_runtime(
Arc::clone(&store),
policy,
http_fetcher,
rsync_fetcher,
parallel_config,
None,
Some(download_log.clone()),
successful_tal_inputs.clone(),
)?;
let current_repo_index_for_output = current_repo_index.clone();
let runner = make_live_runner(
store.as_ref(),
policy,
http_fetcher,
rsync_fetcher,
validation_time,
None,
Some(download_log.clone()),
Some(current_repo_index),
Some(runtime),
phase2_config,
(phase2_enabled && config.build_ccr_accumulator).then(|| {
CcrAccumulator::new(
discoveries
.iter()
.map(|item| item.trust_anchor.clone())
.collect::<Vec<_>>(),
)
}),
config.persist_vcir,
);
let TreeRunAuditOutput {
tree,
publication_points,
} = if phase2_enabled {
run_tree_parallel_phase2_audit_multi_root(root_handles, &runner, config)?
} else {
run_tree_serial_audit_multi_root(root_handles, &runner, config)?
};
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: primary.discovery.clone(),
discoveries,
successful_tal_inputs,
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: snapshot_current_repo_objects(
Some(&current_repo_index_for_output),
collect_current_repo_objects,
),
ccr_accumulator: runner.ccr_accumulator_snapshot(),
})
}
pub fn run_tree_from_tal_url_parallel_phase1_audit<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_url: &str,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let discovery =
discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?;
run_single_root_parallel_audit_inner(
store,
policy,
discovery,
vec![TalInputSpec::from_url(tal_url.to_string())],
http_fetcher,
rsync_fetcher,
validation_time,
config,
parallel_config,
None,
collect_current_repo_objects,
)
}
pub fn run_tree_from_tal_and_ta_der_parallel_phase1_audit<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&url::Url>,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
let derived_tal_id = derive_tal_id(&discovery);
let tal_inputs = vec![TalInputSpec {
tal_id: derived_tal_id.clone(),
rir_id: derived_tal_id,
source: TalSource::DerBytes {
tal_url: discovery
.tal_url
.clone()
.unwrap_or_else(|| "embedded-tal".to_string()),
tal_bytes: tal_bytes.to_vec(),
ta_der: ta_der.to_vec(),
},
}];
run_single_root_parallel_audit_inner(
store,
policy,
discovery,
tal_inputs,
http_fetcher,
rsync_fetcher,
validation_time,
config,
parallel_config,
None,
collect_current_repo_objects,
)
}
pub fn run_tree_from_multiple_tals_parallel_phase1_audit<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_inputs: Vec<TalInputSpec>,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
run_multi_root_parallel_audit_inner(
store,
policy,
tal_inputs,
http_fetcher,
rsync_fetcher,
validation_time,
config,
parallel_config,
None,
collect_current_repo_objects,
)
}
pub fn run_tree_from_tal_url_parallel_phase2_audit<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_url: &str,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
phase2_config: ParallelPhase2Config,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let discovery =
discover_root_ca_instance_from_tal_url_with_policy(policy, http_fetcher, tal_url)?;
run_single_root_parallel_audit_inner(
store,
policy,
discovery,
vec![TalInputSpec::from_url(tal_url.to_string())],
http_fetcher,
rsync_fetcher,
validation_time,
config,
parallel_config,
Some(phase2_config),
collect_current_repo_objects,
)
}
pub fn run_tree_from_tal_and_ta_der_parallel_phase2_audit<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&url::Url>,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
phase2_config: ParallelPhase2Config,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
let derived_tal_id = derive_tal_id(&discovery);
let tal_inputs = vec![TalInputSpec {
tal_id: derived_tal_id.clone(),
rir_id: derived_tal_id,
source: TalSource::DerBytes {
tal_url: discovery
.tal_url
.clone()
.unwrap_or_else(|| "embedded-tal".to_string()),
tal_bytes: tal_bytes.to_vec(),
ta_der: ta_der.to_vec(),
},
}];
run_single_root_parallel_audit_inner(
store,
policy,
discovery,
tal_inputs,
http_fetcher,
rsync_fetcher,
validation_time,
config,
parallel_config,
Some(phase2_config),
collect_current_repo_objects,
)
}
pub fn run_tree_from_multiple_tals_parallel_phase2_audit<H, R>(
store: Arc<crate::storage::RocksStore>,
policy: &crate::policy::Policy,
tal_inputs: Vec<TalInputSpec>,
http_fetcher: &H,
rsync_fetcher: &R,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
parallel_config: ParallelPhase1Config,
phase2_config: ParallelPhase2Config,
collect_current_repo_objects: bool,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError>
where
H: Fetcher + Clone + 'static,
R: crate::fetch::rsync::RsyncFetcher + Clone + 'static,
{
run_multi_root_parallel_audit_inner(
store,
policy,
tal_inputs,
http_fetcher,
rsync_fetcher,
validation_time,
config,
parallel_config,
Some(phase2_config),
collect_current_repo_objects,
)
}
pub fn run_tree_from_tal_and_ta_der_serial(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
download_log: None,
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let tree = run_tree_serial(root, &runner, config)?;
Ok(RunTreeFromTalOutput { discovery, tree })
}
pub fn run_tree_from_tal_bytes_serial_audit(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
tal_uri: Option<String>,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let tal = crate::data_model::tal::Tal::decode_bytes(tal_bytes).map_err(FromTalError::from)?;
let discovery = discover_root_ca_instance_from_tal_with_fetchers(
http_fetcher,
rsync_fetcher,
tal,
tal_uri,
)?;
let download_log = DownloadLogHandle::new();
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
download_log: Some(download_log.clone()),
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_bytes_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
tal_uri: Option<String>,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let tal = crate::data_model::tal::Tal::decode_bytes(tal_bytes).map_err(FromTalError::from)?;
let discovery = discover_root_ca_instance_from_tal_with_fetchers(
http_fetcher,
rsync_fetcher,
tal,
tal_uri,
)?;
drop(_tal);
let download_log = DownloadLogHandle::new();
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: Some(timing.clone()),
download_log: Some(download_log.clone()),
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let _tree = timing.span_phase("tree_run");
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
drop(_tree);
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_and_ta_der_serial_audit(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
let download_log = DownloadLogHandle::new();
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: None,
download_log: Some(download_log.clone()),
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
http_fetcher: &dyn Fetcher,
rsync_fetcher: &dyn crate::fetch::rsync::RsyncFetcher,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
drop(_tal);
let download_log = DownloadLogHandle::new();
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing: Some(timing.clone()),
download_log: Some(download_log.clone()),
replay_archive_index: None,
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let _tree = timing.span_phase("tree_run_total");
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_and_ta_der_payload_replay_serial(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
payload_archive_root: &std::path::Path,
payload_locks_path: &std::path::Path,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalOutput, RunTreeFromTalError> {
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
let replay_index = Arc::new(
ReplayArchiveIndex::load_allow_missing_rsync_modules(
payload_archive_root,
payload_locks_path,
)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
);
let http_fetcher = PayloadReplayHttpFetcher::new(replay_index.clone())
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let rsync_fetcher = PayloadReplayRsyncFetcher::new(replay_index.clone());
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher: &http_fetcher,
rsync_fetcher: &rsync_fetcher,
validation_time,
timing: None,
download_log: None,
replay_archive_index: Some(replay_index),
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let tree = run_tree_serial(root, &runner, config)?;
Ok(RunTreeFromTalOutput { discovery, tree })
}
pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
payload_archive_root: &std::path::Path,
payload_locks_path: &std::path::Path,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
let replay_index = Arc::new(
ReplayArchiveIndex::load_allow_missing_rsync_modules(
payload_archive_root,
payload_locks_path,
)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
);
let http_fetcher = PayloadReplayHttpFetcher::new(replay_index.clone())
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let rsync_fetcher = PayloadReplayRsyncFetcher::new(replay_index.clone());
let download_log = DownloadLogHandle::new();
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher: &http_fetcher,
rsync_fetcher: &rsync_fetcher,
validation_time,
timing: None,
download_log: Some(download_log.clone()),
replay_archive_index: Some(replay_index),
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
payload_archive_root: &std::path::Path,
payload_locks_path: &std::path::Path,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
drop(_tal);
let replay_index = Arc::new(
ReplayArchiveIndex::load_allow_missing_rsync_modules(
payload_archive_root,
payload_locks_path,
)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
);
let http_fetcher = PayloadReplayHttpFetcher::new(replay_index.clone())
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let rsync_fetcher = PayloadReplayRsyncFetcher::new(replay_index.clone());
let download_log = DownloadLogHandle::new();
let runner = Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher: &http_fetcher,
rsync_fetcher: &rsync_fetcher,
validation_time,
timing: Some(timing.clone()),
download_log: Some(download_log.clone()),
replay_archive_index: Some(replay_index),
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
};
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let _tree = timing.span_phase("tree_run_total");
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &runner, config)?;
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
fn build_payload_replay_runner<'a>(
store: &'a crate::storage::RocksStore,
policy: &'a crate::policy::Policy,
replay_index: Arc<ReplayArchiveIndex>,
http_fetcher: &'a PayloadReplayHttpFetcher,
rsync_fetcher: &'a PayloadReplayRsyncFetcher,
validation_time: time::OffsetDateTime,
timing: Option<TimingHandle>,
download_log: Option<DownloadLogHandle>,
) -> Rpkiv1PublicationPointRunner<'a> {
Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing,
download_log,
replay_archive_index: Some(replay_index),
replay_delta_index: None,
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
}
}
fn build_payload_delta_replay_runner<'a>(
store: &'a crate::storage::RocksStore,
policy: &'a crate::policy::Policy,
delta_index: Arc<ReplayDeltaArchiveIndex>,
http_fetcher: &'a PayloadDeltaReplayHttpFetcher,
rsync_fetcher: &'a PayloadDeltaReplayRsyncFetcher,
validation_time: time::OffsetDateTime,
timing: Option<TimingHandle>,
download_log: Option<DownloadLogHandle>,
) -> Rpkiv1PublicationPointRunner<'a> {
Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing,
download_log,
replay_archive_index: None,
replay_delta_index: Some(delta_index),
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
}
}
fn build_payload_delta_replay_current_store_runner<'a>(
store: &'a crate::storage::RocksStore,
policy: &'a crate::policy::Policy,
delta_index: Arc<ReplayDeltaArchiveIndex>,
http_fetcher: &'a PayloadDeltaReplayHttpFetcher,
rsync_fetcher: &'a PayloadDeltaReplayCurrentStoreRsyncFetcher<'a>,
validation_time: time::OffsetDateTime,
timing: Option<TimingHandle>,
download_log: Option<DownloadLogHandle>,
) -> Rpkiv1PublicationPointRunner<'a> {
Rpkiv1PublicationPointRunner {
store,
policy,
http_fetcher,
rsync_fetcher,
validation_time,
timing,
download_log,
replay_archive_index: None,
replay_delta_index: Some(delta_index),
rrdp_dedup: true,
rrdp_repo_cache: Mutex::new(HashMap::new()),
rsync_dedup: true,
rsync_repo_cache: Mutex::new(HashMap::new()),
current_repo_index: None,
repo_sync_runtime: None,
parallel_phase2_config: None,
parallel_roa_worker_pool: None,
ccr_accumulator: None,
persist_vcir: true,
}
}
fn run_payload_delta_replay_audit_inner(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
discovery: DiscoveredRootCaInstance,
base_payload_archive_root: &std::path::Path,
base_locks_path: &std::path::Path,
delta_payload_archive_root: &std::path::Path,
delta_locks_path: &std::path::Path,
base_validation_time: time::OffsetDateTime,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: Option<TimingHandle>,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let base_index = Arc::new(
ReplayArchiveIndex::load_allow_missing_rsync_modules(
base_payload_archive_root,
base_locks_path,
)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
);
let delta_index = Arc::new(
ReplayDeltaArchiveIndex::load(delta_payload_archive_root, delta_locks_path)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
);
delta_index
.validate_base_locks_sha256_file(base_locks_path)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let base_http_fetcher = PayloadReplayHttpFetcher::new(base_index.clone())
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let base_rsync_fetcher = PayloadReplayRsyncFetcher::new(base_index.clone());
if let Some(t) = timing.as_ref() {
let _phase = t.span_phase("payload_delta_replay_base_total");
let base_runner = build_payload_replay_runner(
store,
policy,
base_index.clone(),
&base_http_fetcher,
&base_rsync_fetcher,
base_validation_time,
Some(t.clone()),
None,
);
let _base = run_tree_serial(root.clone(), &base_runner, config)?;
} else {
let base_runner = build_payload_replay_runner(
store,
policy,
base_index.clone(),
&base_http_fetcher,
&base_rsync_fetcher,
base_validation_time,
None,
None,
);
let _base = run_tree_serial(root.clone(), &base_runner, config)?;
}
let delta_http_fetcher = PayloadDeltaReplayHttpFetcher::from_index(delta_index.clone())
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let delta_rsync_fetcher = PayloadDeltaReplayRsyncFetcher::new(base_index, delta_index.clone());
let download_log = DownloadLogHandle::new();
let (tree, publication_points) = if let Some(t) = timing.as_ref() {
let _phase = t.span_phase("payload_delta_replay_target_total");
let delta_runner = build_payload_delta_replay_runner(
store,
policy,
delta_index,
&delta_http_fetcher,
&delta_rsync_fetcher,
base_validation_time,
Some(t.clone()),
Some(download_log.clone()),
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &delta_runner, config)?;
(tree, publication_points)
} else {
let delta_runner = build_payload_delta_replay_runner(
store,
policy,
delta_index,
&delta_http_fetcher,
&delta_rsync_fetcher,
validation_time,
None,
Some(download_log.clone()),
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &delta_runner, config)?;
(tree, publication_points)
};
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
base_payload_archive_root: &std::path::Path,
base_locks_path: &std::path::Path,
delta_payload_archive_root: &std::path::Path,
delta_locks_path: &std::path::Path,
base_validation_time: time::OffsetDateTime,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
run_payload_delta_replay_audit_inner(
store,
policy,
discovery,
base_payload_archive_root,
base_locks_path,
delta_payload_archive_root,
delta_locks_path,
base_validation_time,
validation_time,
config,
None,
)
}
pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timing(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
base_payload_archive_root: &std::path::Path,
base_locks_path: &std::path::Path,
delta_payload_archive_root: &std::path::Path,
delta_locks_path: &std::path::Path,
base_validation_time: time::OffsetDateTime,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: &TimingHandle,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let _tal = timing.span_phase("tal_bootstrap");
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
drop(_tal);
run_payload_delta_replay_audit_inner(
store,
policy,
discovery,
base_payload_archive_root,
base_locks_path,
delta_payload_archive_root,
delta_locks_path,
base_validation_time,
validation_time,
config,
Some(timing.clone()),
)
}
fn run_payload_delta_replay_step_audit_inner(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
discovery: DiscoveredRootCaInstance,
delta_payload_archive_root: &std::path::Path,
previous_locks_path: &std::path::Path,
delta_locks_path: &std::path::Path,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
timing: Option<TimingHandle>,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let delta_index = Arc::new(
ReplayDeltaArchiveIndex::load(delta_payload_archive_root, delta_locks_path)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?,
);
delta_index
.validate_base_locks_sha256_file(previous_locks_path)
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let root = root_handle_from_trust_anchor(
&discovery.trust_anchor,
derive_tal_id(&discovery),
None,
&discovery.ca_instance,
);
let delta_http_fetcher = PayloadDeltaReplayHttpFetcher::from_index(delta_index.clone())
.map_err(|e| RunTreeFromTalError::Replay(e.to_string()))?;
let delta_rsync_fetcher =
PayloadDeltaReplayCurrentStoreRsyncFetcher::new(store, delta_index.clone());
let download_log = DownloadLogHandle::new();
let (tree, publication_points) = if let Some(t) = timing.as_ref() {
let _phase = t.span_phase("payload_delta_replay_step_total");
let delta_runner = build_payload_delta_replay_current_store_runner(
store,
policy,
delta_index,
&delta_http_fetcher,
&delta_rsync_fetcher,
validation_time,
Some(t.clone()),
Some(download_log.clone()),
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &delta_runner, config)?;
(tree, publication_points)
} else {
let delta_runner = build_payload_delta_replay_current_store_runner(
store,
policy,
delta_index,
&delta_http_fetcher,
&delta_rsync_fetcher,
validation_time,
None,
Some(download_log.clone()),
);
let TreeRunAuditOutput {
tree,
publication_points,
} = run_tree_serial_audit(root, &delta_runner, config)?;
(tree, publication_points)
};
let downloads = download_log.snapshot_events();
let download_stats = DownloadLogHandle::stats_from_events(&downloads);
Ok(RunTreeFromTalAuditOutput {
discovery: discovery.clone(),
discoveries: vec![discovery],
successful_tal_inputs: Vec::new(),
tree,
publication_points,
downloads,
download_stats,
current_repo_objects: Vec::new(),
ccr_accumulator: None,
})
}
pub fn run_tree_from_tal_and_ta_der_payload_delta_replay_step_serial_audit(
store: &crate::storage::RocksStore,
policy: &crate::policy::Policy,
tal_bytes: &[u8],
ta_der: &[u8],
resolved_ta_uri: Option<&Url>,
delta_payload_archive_root: &std::path::Path,
previous_locks_path: &std::path::Path,
delta_locks_path: &std::path::Path,
validation_time: time::OffsetDateTime,
config: &TreeRunConfig,
) -> Result<RunTreeFromTalAuditOutput, RunTreeFromTalError> {
let discovery = discover_root_ca_instance_from_tal_and_ta_der_with_policy(
policy,
tal_bytes,
ta_der,
resolved_ta_uri,
)?;
run_payload_delta_replay_step_audit_inner(
store,
policy,
discovery,
delta_payload_archive_root,
previous_locks_path,
delta_locks_path,
validation_time,
config,
None,
)
}
#[cfg(test)]
mod multi_tal_tests {
use super::*;
use crate::current_repo_index::CurrentRepoIndex;
use crate::storage::{RepositoryViewEntry, RepositoryViewState};
struct RejectingHttpFetcher;
impl Fetcher for RejectingHttpFetcher {
fn fetch(&self, uri: &str) -> Result<Vec<u8>, String> {
Err(format!("unexpected http fetch: {uri}"))
}
}
struct RejectingRsyncFetcher;
impl crate::fetch::rsync::RsyncFetcher for RejectingRsyncFetcher {
fn fetch_objects(
&self,
_rsync_base_uri: &str,
) -> crate::fetch::rsync::RsyncFetchResult<Vec<(String, Vec<u8>)>> {
Err(crate::fetch::rsync::RsyncFetchError::Fetch(
"unexpected rsync fetch".to_string(),
))
}
fn dedup_key(&self, base_uri: &str) -> String {
base_uri.to_string()
}
}
#[test]
fn snapshot_current_repo_objects_is_on_demand() {
let handle = CurrentRepoIndex::shared();
handle
.lock()
.expect("lock index")
.apply_repository_view_entries(&[RepositoryViewEntry {
rsync_uri: "rsync://example.test/repo/a.roa".to_string(),
current_hash: Some("11".repeat(32)),
repository_source: Some("rsync://example.test/repo/".to_string()),
object_type: Some("roa".to_string()),
state: RepositoryViewState::Present,
}])
.expect("apply present entry");
assert!(
snapshot_current_repo_objects(Some(&handle), false).is_empty(),
"collection should be skipped when disabled"
);
let collected = snapshot_current_repo_objects(Some(&handle), true);
assert_eq!(collected.len(), 1);
assert_eq!(collected[0].rsync_uri, "rsync://example.test/repo/a.roa");
}
#[test]
fn discover_multiple_roots_from_tal_inputs_builds_multiple_root_handles() {
let apnic_tal =
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal");
let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
let arin_tal = std::fs::read("tests/fixtures/tal/arin.tal").expect("read arin tal");
let arin_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");
let tal_inputs = vec![
TalInputSpec::from_ta_der("https://example.test/apnic.tal", apnic_tal, apnic_ta),
TalInputSpec::from_ta_der("https://example.test/arin.tal", arin_tal, arin_ta),
];
let roots = discover_multiple_roots_from_tal_inputs(
&tal_inputs,
&RejectingHttpFetcher,
&RejectingRsyncFetcher,
false,
)
.expect("discover roots");
assert_eq!(roots.len(), 2);
assert_eq!(roots[0].tal_input.tal_id, "apnic");
assert_eq!(roots[1].tal_input.tal_id, "arin");
assert_eq!(roots[0].root_handle.tal_id, "apnic");
assert_eq!(roots[1].root_handle.tal_id, "arin");
assert_ne!(
roots[0].root_handle.manifest_rsync_uri,
roots[1].root_handle.manifest_rsync_uri
);
}
#[test]
fn discover_multiple_roots_isolates_strict_name_failure() {
let apnic_tal =
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal");
let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
let arin_tal = std::fs::read("tests/fixtures/tal/arin.tal").expect("read arin tal");
let arin_ta = std::fs::read("tests/fixtures/ta/arin-ta.cer").expect("read arin ta");
let tal_inputs = vec![
TalInputSpec::from_ta_der("https://example.test/apnic.tal", apnic_tal, apnic_ta),
TalInputSpec::from_ta_der("https://example.test/arin.tal", arin_tal, arin_ta),
];
let roots = discover_multiple_roots_from_tal_inputs(
&tal_inputs,
&RejectingHttpFetcher,
&RejectingRsyncFetcher,
true,
)
.expect("strict discovery should keep usable roots");
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].tal_input.tal_id, "arin");
assert_eq!(roots[0].root_handle.tal_id, "arin");
}
#[test]
fn discover_single_root_keeps_strict_name_failure_fatal() {
let apnic_tal =
std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal").expect("read apnic tal");
let apnic_ta = std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta");
let tal_inputs = vec![TalInputSpec::from_ta_der(
"https://example.test/apnic.tal",
apnic_tal,
apnic_ta,
)];
let error = discover_multiple_roots_from_tal_inputs(
&tal_inputs,
&RejectingHttpFetcher,
&RejectingRsyncFetcher,
true,
)
.expect_err("single-TAL strict failure should remain fatal");
assert!(error.to_string().contains("Name strict validation failed"));
}
}
#[cfg(test)]
mod replay_api_tests {
use super::*;
use crate::analysis::timing::{TimingHandle, TimingMeta};
use time::format_description::well_known::Rfc3339;
fn apnic_replay_inputs() -> (
Vec<u8>,
Vec<u8>,
std::path::PathBuf,
std::path::PathBuf,
time::OffsetDateTime,
) {
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der =
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let archive_root = std::path::PathBuf::from("target/live/payload_replay/payload-archive");
let locks_path = std::path::PathBuf::from("target/live/payload_replay/locks.json");
let validation_time = time::OffsetDateTime::parse("2026-03-13T02:30:00Z", &Rfc3339)
.expect("parse validation time");
(tal_bytes, ta_der, archive_root, locks_path, validation_time)
}
fn apnic_multi_rir_replay_inputs() -> (
Vec<u8>,
Vec<u8>,
std::path::PathBuf,
std::path::PathBuf,
time::OffsetDateTime,
) {
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der =
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let archive_root = std::path::PathBuf::from(
"../../rpki/target/live/20260316-112341-multi-final3/apnic/base-payload-archive",
);
let locks_path = std::path::PathBuf::from(
"../../rpki/target/live/20260316-112341-multi-final3/apnic/base-locks.json",
);
let validation_time = time::OffsetDateTime::parse("2026-03-16T11:49:48+08:00", &Rfc3339)
.expect("parse validation time");
(tal_bytes, ta_der, archive_root, locks_path, validation_time)
}
fn apnic_delta_replay_inputs() -> (
Vec<u8>,
Vec<u8>,
std::path::PathBuf,
std::path::PathBuf,
std::path::PathBuf,
std::path::PathBuf,
time::OffsetDateTime,
) {
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der =
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let root =
std::path::PathBuf::from("target/live/apnic_delta_demo/20260315-170223-autoplay");
let base_archive = root.join("base-payload-archive");
let base_locks = root.join("base-locks.json");
let delta_archive = root.join("payload-delta-archive");
let delta_locks = root.join("locks-delta.json");
let validation_time = time::OffsetDateTime::parse("2026-03-15T10:00:00Z", &Rfc3339)
.expect("parse validation time");
(
tal_bytes,
ta_der,
base_archive,
base_locks,
delta_archive,
delta_locks,
validation_time,
)
}
#[test]
fn payload_replay_api_reports_setup_error_for_missing_archive() {
let temp = tempfile::tempdir().expect("tempdir");
let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der =
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let err = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
std::path::Path::new("tests/fixtures/missing-payload-archive"),
std::path::Path::new("tests/fixtures/missing-locks.json"),
time::OffsetDateTime::now_utc(),
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
)
.unwrap_err();
assert!(matches!(err, RunTreeFromTalError::Replay(_)), "{err}");
}
#[test]
fn payload_replay_api_root_only_apnic_archive_runs() {
let temp = tempfile::tempdir().expect("tempdir");
let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
let (tal_bytes, ta_der, archive_root, locks_path, validation_time) = apnic_replay_inputs();
if !archive_root.is_dir() || !locks_path.is_file() {
eprintln!(
"skipping payload replay api test; missing fixtures: archive={} locks={}",
archive_root.display(),
locks_path.display()
);
return;
}
let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
&archive_root,
&locks_path,
validation_time,
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
)
.expect("run replay root-only audit");
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert_eq!(out.publication_points.len(), 1);
assert_eq!(out.discovery.trust_anchor.resolved_ta_uri, None);
assert!(!out.downloads.is_empty());
assert!(
out.downloads.iter().all(|d| d.success),
"expected successful replay downloads"
);
}
#[test]
fn payload_replay_api_root_only_apnic_multi_rir_bundle_runs_with_lenient_rsync_modules() {
let temp = tempfile::tempdir().expect("tempdir");
let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
let (tal_bytes, ta_der, archive_root, locks_path, validation_time) =
apnic_multi_rir_replay_inputs();
if !archive_root.is_dir() || !locks_path.is_file() {
eprintln!(
"skipping multi-rir payload replay api test; missing fixtures: archive={} locks={}",
archive_root.display(),
locks_path.display()
);
return;
}
let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
&archive_root,
&locks_path,
validation_time,
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
)
.expect("run replay root-only audit");
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert_eq!(out.publication_points.len(), 1);
}
#[test]
fn payload_replay_api_root_only_apnic_archive_runs_with_timing() {
let temp = tempfile::tempdir().expect("tempdir");
let db_path = temp.path().join("db");
let store = crate::storage::RocksStore::open(&db_path).expect("open db");
let (tal_bytes, ta_der, archive_root, locks_path, validation_time) = apnic_replay_inputs();
if !archive_root.is_dir() || !locks_path.is_file() {
eprintln!(
"skipping payload replay api timing test; missing fixtures: archive={} locks={}",
archive_root.display(),
locks_path.display()
);
return;
}
let timing = TimingHandle::new(TimingMeta {
recorded_at_utc_rfc3339: "2026-03-13T03:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-03-13T02:30:00Z".to_string(),
tal_url: None,
db_path: Some(db_path.to_string_lossy().into_owned()),
});
let out = run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
&archive_root,
&locks_path,
validation_time,
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
&timing,
)
.expect("run replay root-only audit with timing");
assert_eq!(out.tree.instances_processed, 1);
let timing_json = temp.path().join("timing_replay.json");
timing
.write_json(&timing_json, 20)
.expect("write timing json");
let json: serde_json::Value =
serde_json::from_slice(&std::fs::read(&timing_json).expect("read timing json"))
.expect("parse timing json");
let counts = json.get("counts").expect("counts");
assert!(
counts
.get("repo_sync_rrdp_ok_total")
.and_then(|v| v.as_u64())
.unwrap_or(0)
>= 1
);
}
#[test]
fn payload_delta_replay_api_rejects_base_locks_sha_mismatch() {
let temp = tempfile::tempdir().expect("tempdir");
let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
let (
tal_bytes,
ta_der,
base_archive,
_base_locks,
delta_archive,
delta_locks,
validation_time,
) = apnic_delta_replay_inputs();
let wrong_base_locks = temp.path().join("wrong-base-locks.json");
std::fs::write(&wrong_base_locks, b"wrong-base-locks").expect("write wrong base locks");
let err = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
&base_archive,
&wrong_base_locks,
&delta_archive,
&delta_locks,
validation_time,
validation_time,
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
)
.unwrap_err();
assert!(matches!(err, RunTreeFromTalError::Replay(_)), "{err}");
}
#[test]
fn payload_delta_replay_api_reports_setup_error_for_missing_inputs() {
let temp = tempfile::tempdir().expect("tempdir");
let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
let tal_bytes = std::fs::read("tests/fixtures/tal/apnic-rfc7730-https.tal")
.expect("read apnic tal fixture");
let ta_der =
std::fs::read("tests/fixtures/ta/apnic-ta.cer").expect("read apnic ta fixture");
let err = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
std::path::Path::new("tests/fixtures/missing-base-archive"),
std::path::Path::new("tests/fixtures/missing-base-locks.json"),
std::path::Path::new("tests/fixtures/missing-delta-archive"),
std::path::Path::new("tests/fixtures/missing-delta-locks.json"),
time::OffsetDateTime::now_utc(),
time::OffsetDateTime::now_utc(),
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
)
.unwrap_err();
assert!(matches!(err, RunTreeFromTalError::Replay(_)), "{err}");
}
#[test]
fn payload_delta_replay_api_root_only_apnic_bundle_runs() {
let temp = tempfile::tempdir().expect("tempdir");
let store = crate::storage::RocksStore::open(&temp.path().join("db")).expect("open db");
let (
tal_bytes,
ta_der,
base_archive,
base_locks,
delta_archive,
delta_locks,
validation_time,
) = apnic_delta_replay_inputs();
if !base_archive.is_dir()
|| !base_locks.is_file()
|| !delta_archive.is_dir()
|| !delta_locks.is_file()
{
eprintln!(
"skipping payload delta replay api test; missing fixtures: base_archive={} base_locks={} delta_archive={} delta_locks={}",
base_archive.display(),
base_locks.display(),
delta_archive.display(),
delta_locks.display()
);
return;
}
let out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
&base_archive,
&base_locks,
&delta_archive,
&delta_locks,
validation_time,
validation_time,
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
)
.expect("run delta replay root-only audit");
assert_eq!(out.tree.instances_processed, 1);
assert_eq!(out.tree.instances_failed, 0);
assert_eq!(out.publication_points.len(), 1);
}
#[test]
fn payload_delta_replay_api_root_only_apnic_bundle_runs_with_timing() {
let temp = tempfile::tempdir().expect("tempdir");
let db_path = temp.path().join("db");
let store = crate::storage::RocksStore::open(&db_path).expect("open db");
let (
tal_bytes,
ta_der,
base_archive,
base_locks,
delta_archive,
delta_locks,
validation_time,
) = apnic_delta_replay_inputs();
if !base_archive.is_dir()
|| !base_locks.is_file()
|| !delta_archive.is_dir()
|| !delta_locks.is_file()
{
eprintln!(
"skipping payload delta replay timing test; missing fixtures: base_archive={} base_locks={} delta_archive={} delta_locks={}",
base_archive.display(),
base_locks.display(),
delta_archive.display(),
delta_locks.display()
);
return;
}
let timing = TimingHandle::new(TimingMeta {
recorded_at_utc_rfc3339: "2026-03-16T00:00:00Z".to_string(),
validation_time_utc_rfc3339: "2026-03-15T10:00:00Z".to_string(),
tal_url: None,
db_path: Some(db_path.to_string_lossy().into_owned()),
});
let out = run_tree_from_tal_and_ta_der_payload_delta_replay_serial_audit_with_timing(
&store,
&crate::policy::Policy::default(),
&tal_bytes,
&ta_der,
None,
&base_archive,
&base_locks,
&delta_archive,
&delta_locks,
validation_time,
validation_time,
&TreeRunConfig {
max_depth: Some(0),
max_instances: Some(1),
compact_audit: false,
persist_vcir: true,
build_ccr_accumulator: true,
},
&timing,
)
.expect("run delta replay root-only audit with timing");
assert_eq!(out.tree.instances_processed, 1);
let timing_json = temp.path().join("timing_delta_replay.json");
timing
.write_json(&timing_json, 20)
.expect("write timing json");
let json: serde_json::Value =
serde_json::from_slice(&std::fs::read(&timing_json).expect("read timing json"))
.expect("parse timing json");
assert_eq!(
json["phases"]["payload_delta_replay_base_total"]["count"].as_u64(),
Some(1)
);
assert_eq!(
json["phases"]["payload_delta_replay_target_total"]["count"].as_u64(),
Some(1)
);
}
}