use crate::bootstrap::{
compute_source_root_hash, validate_source_manifest, SourceFileEntry, SourceManifest,
};
use crate::checkpoint::{
build_checkpoint_manifest, canonical_checkpoint_manifest_payload_bytes,
compute_checkpoint_binding_hash, CheckpointArtifact, CheckpointManifest,
};
use crate::content::RangeProofPackage;
use crate::delivery_proofs::{build_delivery_proof_package, DeliveryProofPackage};
use crate::envelope::{
make_attestation, ArtifactRecord, Attestation, PublishEnvelope, TransformEnvelope,
};
use crate::error::{TrazaeoError, TrazaeoResult};
use crate::publish_pipeline::{build_publish_envelope, PublishInput};
use crate::transform_pipeline::{build_transform_envelope, TransformStageInput};
use crate::trust::TrustPolicy;
use crate::verification::{verify_publish_envelope, VerificationMode, VerificationReport};
use serde::{Deserialize, Serialize};
const SOURCE_ARTIFACT_ID: &str = "source-manifest";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DatasetPublishedArtifactInput {
pub artifact_id: String,
pub artifact_ref: String,
pub content_root_hash: String,
pub media_type: String,
pub content_descriptor_ref: Option<String>,
pub content_descriptor_hash: Option<String>,
pub snapshot_id: Option<String>,
pub commit_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DatasetWorkflowCommonInput {
pub schema_version: String,
pub issued_at: String,
pub signed_at: String,
pub signer_id: String,
pub signing_key_hex: String,
pub source_manifest_id: String,
pub source_dataset_id: String,
pub source_files: Vec<SourceFileEntry>,
pub transform_subject_id: String,
pub transform_job_id: String,
pub transform_stage: String,
pub publish_subject_id: String,
pub dataset_id: String,
pub dataset_version: String,
pub published_artifact: DatasetPublishedArtifactInput,
pub toolchain: String,
pub parameters_ref: String,
pub parameters_hash: String,
pub determinism_profile: String,
pub runtime_env_ref: Option<String>,
pub runtime_env_hash: Option<String>,
pub transform_spec_ref: String,
pub transform_spec_hash: String,
pub chunking_profile_ref: Option<String>,
pub chunking_profile_hash: Option<String>,
pub execution_manifest_ref: Option<String>,
pub execution_manifest_hash: Option<String>,
pub runtime_manifest_ref: Option<String>,
pub runtime_manifest_hash: Option<String>,
pub checkpoint_id: String,
pub checkpoint_time_window: String,
pub verification_policy_id: String,
pub stac_refs: Vec<String>,
pub reward_context_ref: Option<String>,
pub reward_context_hash: Option<String>,
pub reward_eligible: bool,
pub trust_policy: TrustPolicy,
pub verification_mode: VerificationMode,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DatasetBootstrapWorkflowInput {
#[serde(flatten)]
pub common: DatasetWorkflowCommonInput,
pub bootstrap_origin_label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DatasetIncrementalWorkflowInput {
#[serde(flatten)]
pub common: DatasetWorkflowCommonInput,
pub prior_bundle: DatasetProvenanceBundle,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DatasetProvenanceBundle {
pub source_manifest: SourceManifest,
pub source_manifest_ref: String,
pub source_manifest_hash: String,
pub transform_envelope: TransformEnvelope,
pub checkpoint_manifest: CheckpointManifest,
pub checkpoint_manifest_ref: String,
pub checkpoint_manifest_hash: String,
pub publish_envelope: PublishEnvelope,
pub verification_report: VerificationReport,
pub lineage_envelopes: Vec<String>,
pub signature_bundle: Vec<String>,
pub store_ref: String,
pub snapshot_id: Option<String>,
pub commit_id: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DatasetWorkflowMode {
Bootstrap,
Incremental,
}
impl DatasetWorkflowMode {
fn start_mode(self) -> &'static str {
match self {
Self::Bootstrap => "dataset_bootstrap",
Self::Incremental => "dataset_incremental",
}
}
}
fn validate_envelope<E>(
result: Result<(), Vec<String>>,
context: &'static str,
_envelope: &E,
) -> TrazaeoResult<()> {
result.map_err(|errors| TrazaeoError::validation(context, errors))
}
fn manifest_hash_hex(manifest: &SourceManifest) -> String {
let payload =
serde_json::to_vec(manifest).expect("source manifest serialization should succeed");
hex::encode(blake3::hash(&payload).as_bytes())
}
fn source_manifest_ref(manifest: &SourceManifest) -> String {
format!("manifest://{}", manifest.manifest_id)
}
fn checkpoint_manifest_ref(dataset_id: &str, dataset_version: &str, checkpoint_id: &str) -> String {
format!("checkpoint://{dataset_id}/{dataset_version}/{checkpoint_id}")
}
fn serialized_attestation(attestation: &Attestation) -> String {
serde_json::to_string(attestation).expect("attestation serialization should succeed")
}
fn signed_seed_attestation(
signer_id: &str,
signing_key_hex: &str,
signed_at: &str,
) -> TrazaeoResult<Attestation> {
let seed = make_attestation(signer_id, signing_key_hex, signed_at, b"")?;
Ok(Attestation {
signer_id: signer_id.to_string(),
key_id: seed.key_id,
signature: String::new(),
signed_at: signed_at.to_string(),
})
}
fn sign_transform_envelope(
mut input: TransformStageInput,
signer_id: &str,
signing_key_hex: &str,
signed_at: &str,
) -> TrazaeoResult<TransformEnvelope> {
let seed = signed_seed_attestation(signer_id, signing_key_hex, signed_at)?;
input.key_id = seed.key_id.clone();
let mut envelope = build_transform_envelope(&input, seed);
let attestation = make_attestation(
signer_id,
signing_key_hex,
signed_at,
&envelope.canonical_attestation_payload_bytes(),
)?;
envelope.key_id = attestation.key_id.clone();
envelope.attestations = vec![attestation];
validate_envelope(
envelope.validate(),
"validate dataset transform envelope",
&envelope,
)?;
Ok(envelope)
}
fn sign_publish_envelope(
mut input: PublishInput,
signer_id: &str,
signing_key_hex: &str,
signed_at: &str,
) -> TrazaeoResult<PublishEnvelope> {
let seed = signed_seed_attestation(signer_id, signing_key_hex, signed_at)?;
input.key_id = seed.key_id.clone();
let mut envelope = build_publish_envelope(&input, seed);
let attestation = make_attestation(
signer_id,
signing_key_hex,
signed_at,
&envelope.canonical_attestation_payload_bytes(),
)?;
envelope.key_id = attestation.key_id.clone();
envelope.attestations = vec![attestation];
validate_envelope(
envelope.validate(),
"validate dataset publish envelope",
&envelope,
)?;
Ok(envelope)
}
fn signed_checkpoint_manifest(
checkpoint_id: &str,
checkpoint_time_window: &str,
prior_checkpoint_ref: Option<&str>,
published_artifacts: Vec<CheckpointArtifact>,
lineage_refs: Vec<String>,
signer_id: &str,
signing_key_hex: &str,
signed_at: &str,
) -> TrazaeoResult<CheckpointManifest> {
let mut manifest = build_checkpoint_manifest(
checkpoint_id,
checkpoint_time_window,
prior_checkpoint_ref,
Vec::new(),
published_artifacts,
lineage_refs,
);
let attestation = make_attestation(
signer_id,
signing_key_hex,
signed_at,
&canonical_checkpoint_manifest_payload_bytes(&manifest),
)?;
manifest.checkpoint_signature_bundle = vec![serialized_attestation(&attestation)];
crate::checkpoint::validate_checkpoint_manifest(&manifest)?;
Ok(manifest)
}
fn build_source_manifest(
manifest_id: &str,
issued_at: &str,
source_dataset_id: &str,
source_files: Vec<SourceFileEntry>,
) -> TrazaeoResult<SourceManifest> {
let manifest = SourceManifest {
manifest_id: manifest_id.to_string(),
manifest_created_at: issued_at.to_string(),
source_dataset_id: source_dataset_id.to_string(),
source_file_count: source_files.len(),
source_root_hash: hex::encode(compute_source_root_hash(&source_files).0),
source_files,
};
validate_source_manifest(&manifest)?;
Ok(manifest)
}
fn primary_artifact(envelope: &PublishEnvelope) -> TrazaeoResult<&CheckpointArtifact> {
envelope
.published_artifacts
.iter()
.find(|artifact| artifact.artifact_id == envelope.primary_artifact_id)
.ok_or_else(|| {
TrazaeoError::invalid_input(
"dataset workflow primary artifact",
"publish_envelope primary_artifact_id not found in published_artifacts",
)
})
}
fn lineage_signature_bundle(lineage_envelopes: &[String]) -> TrazaeoResult<Vec<String>> {
let mut signatures = Vec::new();
for envelope_json in lineage_envelopes {
let value: serde_json::Value = serde_json::from_str(envelope_json).map_err(|err| {
TrazaeoError::serialization(
"dataset workflow signature bundle",
format!("invalid lineage envelope json: {err}"),
)
})?;
let attestations = value
.get("attestations")
.and_then(serde_json::Value::as_array)
.ok_or_else(|| {
TrazaeoError::serialization(
"dataset workflow signature bundle",
"lineage envelope missing attestations",
)
})?;
for attestation in attestations {
signatures.push(serde_json::to_string(attestation).map_err(|err| {
TrazaeoError::serialization(
"dataset workflow signature bundle",
format!("failed to serialize attestation: {err}"),
)
})?);
}
}
Ok(signatures)
}
fn checkpoint_artifact(input: &DatasetPublishedArtifactInput) -> CheckpointArtifact {
CheckpointArtifact {
artifact_id: input.artifact_id.clone(),
content_root_hash: input.content_root_hash.clone(),
content_descriptor_ref: input.content_descriptor_ref.clone(),
content_descriptor_hash: input.content_descriptor_hash.clone(),
media_type: input.media_type.clone(),
}
}
fn build_bundle(
common: DatasetWorkflowCommonInput,
mode: DatasetWorkflowMode,
bootstrap_origin_label: Option<String>,
prior_bundle: Option<DatasetProvenanceBundle>,
) -> TrazaeoResult<DatasetProvenanceBundle> {
let source_manifest = build_source_manifest(
&common.source_manifest_id,
&common.issued_at,
&common.source_dataset_id,
common.source_files,
)?;
let source_manifest_ref = source_manifest_ref(&source_manifest);
let source_manifest_hash = manifest_hash_hex(&source_manifest);
let mut input_refs = vec![format!("source://{}", common.source_dataset_id)];
let mut input_artifacts = vec![ArtifactRecord {
artifact_id: SOURCE_ARTIFACT_ID.to_string(),
artifact_ref: source_manifest_ref.clone(),
content_root_hash: source_manifest.source_root_hash.clone(),
}];
let mut lineage_refs = Vec::new();
let mut lineage_envelopes = Vec::new();
let mut prior_checkpoint_ref = None;
if let Some(prior) = prior_bundle.as_ref() {
let prior_artifact = primary_artifact(&prior.publish_envelope)?;
input_refs.push(prior.store_ref.clone());
input_artifacts.push(ArtifactRecord {
artifact_id: prior_artifact.artifact_id.clone(),
artifact_ref: prior.store_ref.clone(),
content_root_hash: prior_artifact.content_root_hash.clone(),
});
lineage_refs.push(format!("publish://{}", prior.publish_envelope.subject_id));
lineage_envelopes.push(
serde_json::to_string(&prior.publish_envelope).expect("publish envelope serialization"),
);
prior_checkpoint_ref = Some(prior.checkpoint_manifest_ref.as_str());
}
let output_artifacts = vec![ArtifactRecord {
artifact_id: common.published_artifact.artifact_id.clone(),
artifact_ref: common.published_artifact.artifact_ref.clone(),
content_root_hash: common.published_artifact.content_root_hash.clone(),
}];
let transform_envelope = sign_transform_envelope(
TransformStageInput {
schema_version: common.schema_version.clone(),
issued_at: common.issued_at.clone(),
subject_id: common.transform_subject_id.clone(),
transform_job_id: common.transform_job_id.clone(),
transform_stage: common.transform_stage.clone(),
input_refs,
output_refs: vec![common.published_artifact.artifact_ref.clone()],
input_artifacts,
output_artifacts,
toolchain: common.toolchain.clone(),
parameters_ref: common.parameters_ref.clone(),
parameters_hash: common.parameters_hash.clone(),
determinism_profile: common.determinism_profile.clone(),
runtime_env_ref: common.runtime_env_ref.clone(),
runtime_env_hash: common.runtime_env_hash.clone(),
provenance_start_mode: mode.start_mode().to_string(),
source_manifest_ref: Some(source_manifest_ref.clone()),
source_manifest_hash: Some(source_manifest_hash.clone()),
source_root_hash: Some(source_manifest.source_root_hash.clone()),
transform_spec_ref: Some(common.transform_spec_ref.clone()),
transform_spec_hash: Some(common.transform_spec_hash.clone()),
chunking_profile_ref: common.chunking_profile_ref.clone(),
chunking_profile_hash: common.chunking_profile_hash.clone(),
execution_manifest_ref: common.execution_manifest_ref.clone(),
execution_manifest_hash: common.execution_manifest_hash.clone(),
runtime_manifest_ref: common.runtime_manifest_ref.clone(),
runtime_manifest_hash: common.runtime_manifest_hash.clone(),
key_id: String::new(),
},
&common.signer_id,
&common.signing_key_hex,
&common.signed_at,
)?;
lineage_refs.push(format!("transform://{}", transform_envelope.subject_id));
lineage_envelopes.push(
serde_json::to_string(&transform_envelope).expect("transform envelope serialization"),
);
let checkpoint_manifest = signed_checkpoint_manifest(
&common.checkpoint_id,
&common.checkpoint_time_window,
prior_checkpoint_ref,
vec![checkpoint_artifact(&common.published_artifact)],
lineage_refs.clone(),
&common.signer_id,
&common.signing_key_hex,
&common.signed_at,
)?;
let checkpoint_manifest_ref = checkpoint_manifest_ref(
&common.dataset_id,
&common.dataset_version,
&common.checkpoint_id,
);
let checkpoint_manifest_hash =
hex::encode(compute_checkpoint_binding_hash(&checkpoint_manifest).0);
let publish_envelope = sign_publish_envelope(
PublishInput {
schema_version: common.schema_version.clone(),
issued_at: common.issued_at.clone(),
subject_id: common.publish_subject_id.clone(),
dataset_id: common.dataset_id.clone(),
dataset_version: common.dataset_version.clone(),
input_refs: vec![common.published_artifact.artifact_ref.clone()],
output_refs: vec![common.published_artifact.artifact_ref.clone()],
published_artifacts: vec![checkpoint_artifact(&common.published_artifact)],
primary_artifact_id: common.published_artifact.artifact_id.clone(),
checkpoint_manifest_ref: checkpoint_manifest_ref.clone(),
checkpoint_manifest_hash: checkpoint_manifest_hash.clone(),
checkpoint_id: common.checkpoint_id.clone(),
checkpoint_log_root_hash: checkpoint_manifest.checkpoint_log_root_hash.clone(),
lineage_refs,
verification_policy_id: common.verification_policy_id.clone(),
key_id: String::new(),
stac_refs: common.stac_refs.clone(),
reward_context_ref: common.reward_context_ref.clone(),
reward_context_hash: common.reward_context_hash.clone(),
provenance_start_mode: mode.start_mode().to_string(),
bootstrap_origin_label,
reward_eligible: common.reward_eligible,
},
&common.signer_id,
&common.signing_key_hex,
&common.signed_at,
)?;
let verification_report = verify_publish_envelope(
&publish_envelope,
common.verification_mode,
&common.trust_policy,
);
let signature_bundle = lineage_signature_bundle(&lineage_envelopes)?;
Ok(DatasetProvenanceBundle {
source_manifest,
source_manifest_ref,
source_manifest_hash,
transform_envelope,
checkpoint_manifest,
checkpoint_manifest_ref,
checkpoint_manifest_hash,
publish_envelope,
verification_report,
lineage_envelopes,
signature_bundle,
store_ref: common.published_artifact.artifact_ref.clone(),
snapshot_id: common.published_artifact.snapshot_id,
commit_id: common.published_artifact.commit_id,
})
}
pub fn build_dataset_bootstrap_bundle(
input: DatasetBootstrapWorkflowInput,
) -> TrazaeoResult<DatasetProvenanceBundle> {
build_bundle(
input.common,
DatasetWorkflowMode::Bootstrap,
Some(input.bootstrap_origin_label),
None,
)
}
pub fn build_dataset_incremental_bundle(
input: DatasetIncrementalWorkflowInput,
) -> TrazaeoResult<DatasetProvenanceBundle> {
build_bundle(
input.common,
DatasetWorkflowMode::Incremental,
None,
Some(input.prior_bundle),
)
}
pub fn build_dataset_delivery_proof_package(
bundle: &DatasetProvenanceBundle,
range_proof_package: &RangeProofPackage,
) -> TrazaeoResult<DeliveryProofPackage> {
build_delivery_proof_package(
range_proof_package,
bundle.lineage_envelopes.clone(),
bundle.checkpoint_manifest.clone(),
bundle.signature_bundle.clone(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::content::{
build_content_descriptor, build_full_root_proof_package, compute_content_descriptor_hash,
ContentDescriptorInput,
};
use crate::utils::Hash;
use crate::verification::{is_report_success, verify_publish_envelope, VerificationState};
const TEST_SIGNING_KEY_HEX: &str =
"4f3edf983ac636a65a842ce7c78d9aa706d3b113bce036f9a4f5762b76f70f18";
fn trust_policy_for(key_id: &str) -> TrustPolicy {
let mut policy = TrustPolicy::new();
policy.allow_key(key_id);
policy
}
fn source_files() -> Vec<SourceFileEntry> {
vec![SourceFileEntry {
source_uri: "s3://bucket/a.nc".to_string(),
content_hash: "abcd".repeat(16),
byte_length: 128,
observed_mtime: Some("2026-01-01T00:00:00Z".to_string()),
}]
}
fn artifact_input(version: &str, root: &str) -> DatasetPublishedArtifactInput {
DatasetPublishedArtifactInput {
artifact_id: format!("artifact-{version}"),
artifact_ref: format!("icechunk://eo/sst-{version}.zarr"),
content_root_hash: root.to_string(),
media_type: "application/vnd+zarr".to_string(),
content_descriptor_ref: Some(format!("descriptor://sst-{version}")),
content_descriptor_hash: Some("beef".repeat(16)),
snapshot_id: Some(format!("snap-{version}")),
commit_id: Some(format!("commit-{version}")),
}
}
fn common_input(version: &str, root: &str) -> DatasetWorkflowCommonInput {
DatasetWorkflowCommonInput {
schema_version: "1.0.0".to_string(),
issued_at: "2026-01-01T00:00:00Z".to_string(),
signed_at: "2026-01-01T00:00:00Z".to_string(),
signer_id: "publisher".to_string(),
signing_key_hex: TEST_SIGNING_KEY_HEX.to_string(),
source_manifest_id: format!("sst-{version}-manifest"),
source_dataset_id: "sst".to_string(),
source_files: source_files(),
transform_subject_id: format!("transform-sst-{version}"),
transform_job_id: format!("job-sst-{version}"),
transform_stage: "nc_collection_to_icechunk".to_string(),
publish_subject_id: format!("publish-sst-{version}"),
dataset_id: "sst".to_string(),
dataset_version: version.to_string(),
published_artifact: artifact_input(version, root),
toolchain: "python-xarray".to_string(),
parameters_ref: "params://nc-to-icechunk-v1".to_string(),
parameters_hash: "1234".repeat(16),
determinism_profile: "strict-v1".to_string(),
runtime_env_ref: Some("oci://eo/runtime".to_string()),
runtime_env_hash: Some("3456".repeat(16)),
transform_spec_ref: "spec://eo/nc-to-icechunk-v1".to_string(),
transform_spec_hash: "5678".repeat(16),
chunking_profile_ref: Some("chunk://eo/default-v1".to_string()),
chunking_profile_hash: Some("7890".repeat(16)),
execution_manifest_ref: None,
execution_manifest_hash: None,
runtime_manifest_ref: None,
runtime_manifest_hash: None,
checkpoint_id: format!("checkpoint-sst-{version}"),
checkpoint_time_window: "2026-01-01T00:00:00Z/2026-01-01T01:00:00Z".to_string(),
verification_policy_id: "verify-default".to_string(),
stac_refs: vec![],
reward_context_ref: None,
reward_context_hash: None,
reward_eligible: false,
trust_policy: trust_policy_for(
&make_attestation(
"publisher",
TEST_SIGNING_KEY_HEX,
"2026-01-01T00:00:00Z",
b"",
)
.expect("seed")
.key_id,
),
verification_mode: VerificationMode::Full,
}
}
#[test]
fn dataset_bootstrap_bundle_builds_signed_artifacts() {
let bundle = build_dataset_bootstrap_bundle(DatasetBootstrapWorkflowInput {
common: common_input("v1", &"aa".repeat(32)),
bootstrap_origin_label: "nc-collection:sst".to_string(),
})
.expect("bootstrap bundle");
assert_eq!(
bundle.publish_envelope.provenance_start_mode,
"dataset_bootstrap"
);
assert_eq!(
bundle.transform_envelope.provenance_start_mode,
"dataset_bootstrap"
);
assert_eq!(
bundle.publish_envelope.bootstrap_origin_label.as_deref(),
Some("nc-collection:sst")
);
assert_eq!(
bundle.checkpoint_manifest.lineage_refs,
vec!["transform://transform-sst-v1"]
);
assert_eq!(bundle.lineage_envelopes.len(), 1);
assert_eq!(bundle.signature_bundle.len(), 1);
assert!(is_report_success(&bundle.verification_report));
}
#[test]
fn dataset_incremental_bundle_chains_prior_publish_state() {
let bootstrap = build_dataset_bootstrap_bundle(DatasetBootstrapWorkflowInput {
common: common_input("v1", &"aa".repeat(32)),
bootstrap_origin_label: "nc-collection:sst".to_string(),
})
.expect("bootstrap bundle");
let incremental = build_dataset_incremental_bundle(DatasetIncrementalWorkflowInput {
common: common_input("v2", &"bb".repeat(32)),
prior_bundle: bootstrap.clone(),
})
.expect("incremental bundle");
assert_eq!(
incremental.publish_envelope.provenance_start_mode,
"dataset_incremental"
);
assert_eq!(
incremental.transform_envelope.provenance_start_mode,
"dataset_incremental"
);
assert_eq!(
incremental
.checkpoint_manifest
.prior_checkpoint_ref
.as_deref(),
Some(bootstrap.checkpoint_manifest_ref.as_str())
);
assert_eq!(
incremental.publish_envelope.lineage_refs,
vec![
format!("publish://{}", bootstrap.publish_envelope.subject_id),
"transform://transform-sst-v2".to_string()
]
);
assert_eq!(incremental.lineage_envelopes.len(), 2);
assert_eq!(incremental.signature_bundle.len(), 2);
}
#[test]
fn dataset_bundle_builds_delivery_proof_package() {
let root = Hash(*blake3::hash(b"dataset-bytes").as_bytes());
let descriptor = build_content_descriptor(ContentDescriptorInput {
artifact_id: "artifact-v1",
root,
chunk_size: 4,
leaf_count: 3,
byte_length: 13,
media_type: "application/vnd+zarr",
created_at: "2026-01-01T00:00:00Z",
});
let descriptor_hash = hex::encode(compute_content_descriptor_hash(&descriptor).0);
let mut common = common_input("v1", &descriptor.content_root_hash);
common.published_artifact.content_descriptor_hash = Some(descriptor_hash);
let bundle = build_dataset_bootstrap_bundle(DatasetBootstrapWorkflowInput {
common,
bootstrap_origin_label: "nc-collection:sst".to_string(),
})
.expect("bundle");
let proof = build_full_root_proof_package(&descriptor).expect("proof package");
let delivery = build_dataset_delivery_proof_package(&bundle, &proof).expect("delivery");
assert_eq!(delivery.artifact_id, "artifact-v1");
assert_eq!(
delivery.checkpoint_manifest.checkpoint_id,
"checkpoint-sst-v1"
);
assert_eq!(delivery.lineage_envelopes.len(), 1);
assert_eq!(delivery.signature_bundle.len(), 1);
}
#[test]
fn bootstrap_bundle_verifies_publish_envelope() {
let bundle = build_dataset_bootstrap_bundle(DatasetBootstrapWorkflowInput {
common: common_input("v1", &"aa".repeat(32)),
bootstrap_origin_label: "nc-collection:sst".to_string(),
})
.expect("bundle");
let report = verify_publish_envelope(
&bundle.publish_envelope,
VerificationMode::Full,
&trust_policy_for(&bundle.publish_envelope.key_id),
);
assert_eq!(report.signature_state, VerificationState::Passed);
assert_eq!(report.lineage_state, VerificationState::Passed);
}
}