Skip to main content

ValidatedTransaction

Struct ValidatedTransaction 

Source
pub struct ValidatedTransaction { /* private fields */ }
Expand description

A transaction whose integrity-verified candidates passed a consumer validator.

Implementations§

Source§

impl ValidatedTransaction

Source

pub fn artifacts(&self) -> &ArtifactSet

Returns the validated artifact set.

Source

pub fn staged_path(&self, member: &MemberId) -> Result<PathBuf>

Returns a validated member’s private staged path.

Exposed so consumers and tests can inspect staged bytes; mutating the staged copy after validation causes the final digest revalidation in Self::commit to fail before any live mutation.

Source

pub fn commit( self, ownership: CommitOwnership<'_>, ) -> Result<TransactionReceipt>

Commits only after integrity and candidate validation have passed.

Destination ownership is proven again under the mutation lock via ownership, and every staged member is re-hashed against the digest recorded at VerifiedTransaction time before any live destination is mutated. NotRequired integrity state can never reach this path: validation rejects it, and the final revalidation requires a verified digest for every member.

Examples found in repository?
examples/custom_validator.rs (lines 70-73)
46fn main() -> Result<(), Box<dyn std::error::Error>> {
47    let base = temp_root("eggup-custom-validator");
48    let inputs = base.join("inputs");
49    let root = base.join("install");
50    fs::create_dir_all(&inputs)?;
51    fs::create_dir_all(root.join("bin"))?;
52    fs::write(inputs.join("app"), b"release-marker-42")?;
53    let digest = eggup_core::hash_file(&inputs.join("app"))?;
54    let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
55        .with_integrity(IntegrityRequirement::Sha256(digest));
56    let plan = InstallPlan::new(
57        ProductId::new("demo")?,
58        ReleaseId::new("3.0.0")?,
59        &root,
60        ArtifactSet::single(member)?,
61    )?;
62    let validator = ContainsMarker {
63        member: MemberId::new("main")?,
64        marker: b"marker-42".to_vec(),
65    };
66    let receipt = plan
67        .prepare()?
68        .verify_integrity()?
69        .validate(&validator)?
70        .commit(CommitOwnership::new(
71            &ExistingAsOwnedVerifier,
72            AbsentPolicy::AllowCreate,
73        ))?;
74    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
75    let _ = fs::remove_dir_all(&base);
76    println!("custom validator commit: {:?}", receipt.disposition());
77    Ok(())
78}
More examples
Hide additional examples
examples/one_member.rs (lines 56-59)
30fn main() -> Result<(), Box<dyn std::error::Error>> {
31    let base = temp_root("eggup-quickstart");
32    let (inputs, root) = (base.join("inputs"), base.join("install"));
33    fs::create_dir_all(inputs.join("bin"))?;
34    fs::create_dir_all(root.join("bin"))?;
35    fs::write(inputs.join("app"), b"new-app")?;
36
37    // 1. Describe one member, with the SHA-256 you expect to verify.
38    let source = inputs.join("app");
39    let digest = eggup_core::hash_file(&source)?;
40    let member = ArtifactMember::new(MemberId::new("main")?, source, "bin/app")?
41        .with_integrity(IntegrityRequirement::Sha256(digest));
42
43    // 2. Name the product, release, install root, and artifact set.
44    let plan = InstallPlan::new(
45        ProductId::new("demo")?,
46        ReleaseId::new("1.0.0")?,
47        &root,
48        ArtifactSet::single(member)?,
49    )?;
50
51    // 3. Stage, verify, validate, then commit with proven ownership.
52    let receipt = plan
53        .prepare()?
54        .verify_integrity()?
55        .validate(&AllValidators::new())?
56        .commit(CommitOwnership::new(
57            &ExistingAsOwnedVerifier,
58            AbsentPolicy::AllowCreate,
59        ))?;
60
61    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
62    assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
63    let _ = fs::remove_dir_all(&base);
64    println!("committed: {:?}", receipt.disposition());
65    Ok(())
66}
examples/ownership.rs (line 49)
24fn main() -> Result<(), Box<dyn std::error::Error>> {
25    let base = temp_root("eggup-ownership");
26    let inputs = base.join("inputs");
27    let root = base.join("install");
28    fs::create_dir_all(&inputs)?;
29    fs::create_dir_all(root.join("bin"))?;
30    fs::write(inputs.join("app"), b"new-app")?;
31    fs::write(root.join("bin/app"), b"old-app")?;
32    let new_digest = eggup_core::hash_file(&inputs.join("app"))?;
33    let old_digest = eggup_core::hash_file(&root.join("bin/app"))?;
34    let member = ArtifactMember::new(MemberId::new("main")?, inputs.join("app"), "bin/app")?
35        .with_integrity(IntegrityRequirement::Sha256(new_digest));
36    let plan = InstallPlan::new(
37        ProductId::new("demo")?,
38        ReleaseId::new("4.0.0")?,
39        &root,
40        ArtifactSet::single(member)?,
41    )?;
42    // Prove the live file is the expected old deployment; deny creation of
43    // absent destinations (replacement-only policy).
44    let verifier = ExactDigestVerifier::new(vec![(MemberId::new("main")?, old_digest)]);
45    let receipt = plan
46        .prepare()?
47        .verify_integrity()?
48        .validate(&AllValidators::new())?
49        .commit(CommitOwnership::new(&verifier, AbsentPolicy::DenyCreate))?;
50    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
51    assert_eq!(fs::read(root.join("bin/app"))?, b"new-app");
52    let _ = fs::remove_dir_all(&base);
53    println!("ownership-verified commit: {:?}", receipt.disposition());
54    Ok(())
55}
examples/multi_member.rs (lines 56-59)
24fn main() -> Result<(), Box<dyn std::error::Error>> {
25    let base = temp_root("eggup-multi-member");
26    let inputs = base.join("inputs");
27    let root = base.join("install");
28    fs::create_dir_all(&inputs)?;
29    fs::create_dir_all(root.join("bin"))?;
30    fs::write(inputs.join("main"), b"new-main")?;
31    fs::write(inputs.join("helper"), b"new-helper")?;
32    fs::write(root.join("bin/main"), b"old-main")?;
33    fs::write(root.join("bin/helper"), b"old-helper")?;
34    let main_digest = eggup_core::hash_file(&inputs.join("main"))?;
35    let helper_digest = eggup_core::hash_file(&inputs.join("helper"))?;
36    let members = ArtifactSet::new(vec![
37        ArtifactMember::new(MemberId::new("main")?, inputs.join("main"), "bin/main")?
38            .with_integrity(IntegrityRequirement::Sha256(main_digest)),
39        ArtifactMember::new(
40            MemberId::new("helper")?,
41            inputs.join("helper"),
42            "bin/helper",
43        )?
44        .with_integrity(IntegrityRequirement::Sha256(helper_digest)),
45    ])?;
46    let plan = InstallPlan::new(
47        ProductId::new("bundle")?,
48        ReleaseId::new("2.0.0")?,
49        &root,
50        members,
51    )?;
52    let receipt = plan
53        .prepare()?
54        .verify_integrity()?
55        .validate(&AllValidators::new())?
56        .commit(CommitOwnership::new(
57            &ExistingAsOwnedVerifier,
58            AbsentPolicy::AllowCreate,
59        ))?;
60    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
61    assert_eq!(fs::read(root.join("bin/main"))?, b"new-main");
62    assert_eq!(fs::read(root.join("bin/helper"))?, b"new-helper");
63    let _ = fs::remove_dir_all(&base);
64    println!("multi-member commit: {:?}", receipt.disposition());
65    Ok(())
66}
examples/receipts.rs (lines 68-71)
61fn main() -> Result<(), Box<dyn std::error::Error>> {
62    // 1. Committed: success.
63    let (base, plan) = build(b"new", Some(b"old"))?;
64    let receipt = plan
65        .prepare()?
66        .verify_integrity()?
67        .validate(&AllValidators::new())?
68        .commit(CommitOwnership::new(
69            &ExistingAsOwnedVerifier,
70            AbsentPolicy::AllowCreate,
71        ))?;
72    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
73    assert!(receipt.failure().is_none());
74    println!("committed: {:?}", receipt.disposition());
75    let _ = fs::remove_dir_all(&base);
76
77    // 2. RolledBack: ownership denied, live state unchanged, failure explains why.
78    let (base, plan) = build(b"new", Some(b"old"))?;
79    let root = base.join("install");
80    let receipt = plan
81        .prepare()?
82        .verify_integrity()?
83        .validate(&AllValidators::new())?
84        .commit(CommitOwnership::new(&DenyAll, AbsentPolicy::AllowCreate))?;
85    assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
86    assert!(receipt.rollback_verified());
87    let failure = receipt.failure().expect("rolled back carries a cause");
88    println!(
89        "rolled back: phase={} category={} detail={}",
90        failure.phase().as_str(),
91        failure.category().as_str(),
92        failure.detail()
93    );
94    assert_eq!(fs::read(root.join("bin/app"))?, b"old");
95    assert_ne!(
96        receipt.disposition(),
97        TransactionDisposition::Committed,
98        "rollback is not success"
99    );
100    let _ = fs::remove_dir_all(&base);
101
102    // 3. RecoveryRequired is distinct and high severity: it means Eggup cannot
103    // prove either generation is coherent. It is produced by rollback failure
104    // or unclean finalization; operators must inspect `recovery_path` and the
105    // preserved lock. See `docs/transaction.md` for the full contract.
106    println!("recovery-required: inspect recovery_path + lock; do not retry blindly");
107
108    // 4. A failed post-commit check can keep the coherent new generation,
109    // while remaining machine-readable as a failed check.
110    let (base, plan) = build(b"new", Some(b"old"))?;
111    let receipt = plan
112        .prepare()?
113        .verify_integrity()?
114        .validate(&AllValidators::new())?
115        .commit_with_post_commit(
116            CommitOwnership::new(&ExistingAsOwnedVerifier, AbsentPolicy::AllowCreate),
117            PostCommitFailurePolicy::KeepInstalled,
118            || Err("example health check failed"),
119        )?;
120    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
121    assert!(receipt.post_commit_failure().is_some());
122    println!(
123        "kept installed after failed check: {:?}",
124        receipt.disposition()
125    );
126    let _ = fs::remove_dir_all(base);
127    Ok(())
128}
Source

pub fn commit_with_post_commit<F, E>( self, ownership: CommitOwnership<'_>, policy: PostCommitFailurePolicy, check: F, ) -> Result<TransactionReceipt>
where F: FnOnce() -> Result<(), E>, E: Display,

Commits the complete artifact set, runs one caller check while the mutation lock and rollback evidence remain held, then resolves its result according to policy.

The callback runs only after all members are live. Its error is copied into bounded core-owned evidence. Panics are caught and recorded as a failed check, so RollBack still restores the prior generation during unwinding. The callback must itself bound its work; core invents no timeout. Process termination or power loss is outside this guarantee.

Examples found in repository?
examples/receipts.rs (lines 115-119)
61fn main() -> Result<(), Box<dyn std::error::Error>> {
62    // 1. Committed: success.
63    let (base, plan) = build(b"new", Some(b"old"))?;
64    let receipt = plan
65        .prepare()?
66        .verify_integrity()?
67        .validate(&AllValidators::new())?
68        .commit(CommitOwnership::new(
69            &ExistingAsOwnedVerifier,
70            AbsentPolicy::AllowCreate,
71        ))?;
72    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
73    assert!(receipt.failure().is_none());
74    println!("committed: {:?}", receipt.disposition());
75    let _ = fs::remove_dir_all(&base);
76
77    // 2. RolledBack: ownership denied, live state unchanged, failure explains why.
78    let (base, plan) = build(b"new", Some(b"old"))?;
79    let root = base.join("install");
80    let receipt = plan
81        .prepare()?
82        .verify_integrity()?
83        .validate(&AllValidators::new())?
84        .commit(CommitOwnership::new(&DenyAll, AbsentPolicy::AllowCreate))?;
85    assert_eq!(receipt.disposition(), TransactionDisposition::RolledBack);
86    assert!(receipt.rollback_verified());
87    let failure = receipt.failure().expect("rolled back carries a cause");
88    println!(
89        "rolled back: phase={} category={} detail={}",
90        failure.phase().as_str(),
91        failure.category().as_str(),
92        failure.detail()
93    );
94    assert_eq!(fs::read(root.join("bin/app"))?, b"old");
95    assert_ne!(
96        receipt.disposition(),
97        TransactionDisposition::Committed,
98        "rollback is not success"
99    );
100    let _ = fs::remove_dir_all(&base);
101
102    // 3. RecoveryRequired is distinct and high severity: it means Eggup cannot
103    // prove either generation is coherent. It is produced by rollback failure
104    // or unclean finalization; operators must inspect `recovery_path` and the
105    // preserved lock. See `docs/transaction.md` for the full contract.
106    println!("recovery-required: inspect recovery_path + lock; do not retry blindly");
107
108    // 4. A failed post-commit check can keep the coherent new generation,
109    // while remaining machine-readable as a failed check.
110    let (base, plan) = build(b"new", Some(b"old"))?;
111    let receipt = plan
112        .prepare()?
113        .verify_integrity()?
114        .validate(&AllValidators::new())?
115        .commit_with_post_commit(
116            CommitOwnership::new(&ExistingAsOwnedVerifier, AbsentPolicy::AllowCreate),
117            PostCommitFailurePolicy::KeepInstalled,
118            || Err("example health check failed"),
119        )?;
120    assert_eq!(receipt.disposition(), TransactionDisposition::Committed);
121    assert!(receipt.post_commit_failure().is_some());
122    println!(
123        "kept installed after failed check: {:?}",
124        receipt.disposition()
125    );
126    let _ = fs::remove_dir_all(base);
127    Ok(())
128}
Source

pub fn commit_with_stale_lock_recovery( self, ownership: CommitOwnership<'_>, recovery: &dyn StaleLockVerifier, ) -> RecoveryResult<TransactionReceipt>

Commits with caller-authorized stale-lock recovery.

Identical to commit except that a pre-existing lock record may be displaced — but only when recovery returns StaleLockDecision::ProvenStale for the exact record Core observed. Every other verdict, and every record Core cannot prove safe, keeps the fail-closed contention result.

Core never decides staleness from PID liveness, record age, executable name, or service state; that evidence belongs to the caller.

Source

pub fn commit_with_post_commit_and_stale_lock_recovery<F, E>( self, ownership: CommitOwnership<'_>, policy: PostCommitFailurePolicy, recovery: &dyn StaleLockVerifier, check: F, ) -> RecoveryResult<TransactionReceipt>
where F: FnOnce() -> Result<(), E>, E: Display,

Commits with a caller post-commit check and caller-authorized stale-lock recovery, combining both opt-in policies.

Trait Implementations§

Source§

impl Debug for ValidatedTransaction

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.