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.

§Errors

Returns Error::UnknownMember when member is not part of this transaction’s artifact set.

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.

§Errors

Returns Err only for lock contention (Error::UpdateInProgress) and for setup failures such as Error::Io that occur while the mutation lock is being taken. Every failure reached once the lock is held — an ownership conflict, a staged-digest mismatch, a failed rename, a failed rollback — is reported through the returned TransactionReceipt, whose disposition is RolledBack or RecoveryRequired, and never through Err.

The direction is fail-closed: Owned is never inferred, and a verifier’s ambiguity is Unknown, which denies mutation. Every member must carry verified SHA-256 evidence, so a member declared None can never reach this call. SHA-256 is integrity evidence only, never a publisher-trust or signature claim.

See architecture/core-transaction.md §8 “Failure semantics”.

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.

§Errors

The same as commit: only lock contention and setup failures return Err. A failing post-commit check is not an error — it is reported in the receipt’s post_commit_failure. With PostCommitFailurePolicy::KeepInstalled the disposition stays Committed; with PostCommitFailurePolicy::RollBack the previous generation is restored and the disposition becomes RolledBack. A panic in the callback is caught and reported the same way.

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.

§Errors

Returns RecoveryError::Core carrying Error::UpdateInProgress when the lock is contended, when the caller’s verifier does not return StaleLockDecision::ProvenStale for the exact record core observed, or when another writer wins the race after a claim; and carrying a setup failure such as Error::Io otherwise.

Returns RecoveryError::RecoveryRequired when a stale record was displaced but could not be resolved automatically; the real retained evidence path is in RecoveryError::evidence.

Every failure reached once the lock is held is reported through the returned TransactionReceipt, never through Err.

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.

§Errors

The union of commit_with_post_commit and commit_with_stale_lock_recovery: only lock contention and setup failures return Err, a failing post-commit check is reported in the receipt’s post_commit_failure, and displaced stale evidence that could not be resolved is reported as RecoveryError::RecoveryRequired with its real retained path.

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.