Skip to main content

qualia_core_db/services/swarm/
dispatch.rs

1//! Dispatch ties the pieces into the one path that matters: **execute → verify →
2//! settle**, with payment impossible without verification.
3
4use super::executor::JobExecutor;
5use super::job::{JobMode, JobResult, JobSpec};
6use super::settlement::{Escrow, SettlementOutcome};
7use super::verify::{verify, VerificationVerdict, VerifyPolicy};
8use super::SwarmError;
9
10/// The full outcome of running a swarm job.
11#[derive(Debug, Clone)]
12pub struct DispatchOutcome {
13    /// The computed result. Retained regardless of verdict so a caller can inspect a
14    /// rejected result — but a rejected result never yields a payment.
15    pub result: JobResult,
16    pub verdict: VerificationVerdict,
17    /// Present only for `Paid` jobs: the settlement (a payment instruction or a refund).
18    /// `None` for `Personal`/`Collaborative` jobs (no money involved).
19    pub settlement: Option<SettlementOutcome>,
20}
21
22/// Run a job: execute it on `executor`, independently verify the result, and — for a
23/// `Paid` job — settle the supplied `escrow` by the verdict. The escrow must already be
24/// `Held` (funds committed) for a paid job; if it is missing for a paid job, the job is
25/// still executed and verified but no settlement is produced (a caller error surfaced as
26/// `None`, never an unguarded payment).
27///
28/// **Invariant:** a provider payment instruction is emitted only when verification
29/// returns `Verified`. There is no code path from a `Rejected` verdict to a `Pay`.
30pub fn run_job(
31    spec: &JobSpec,
32    executor: &dyn JobExecutor,
33    policy: VerifyPolicy,
34    escrow: Option<&mut Escrow>,
35) -> Result<DispatchOutcome, SwarmError> {
36    if !spec.input.is_well_formed() {
37        return Err(SwarmError::InvalidJob);
38    }
39
40    // 1. Execute on the (untrusted) executor.
41    let result = executor.execute(&spec.input)?;
42
43    // 2. Independently verify against the trusted reference.
44    let verdict = verify(&spec.input, &result, policy);
45
46    // 3. Settle, only for paid jobs and only via the verdict.
47    let settlement = match (&spec.mode, escrow) {
48        (JobMode::Paid { .. }, Some(esc)) => Some(esc.settle(verdict)?),
49        _ => None,
50    };
51
52    Ok(DispatchOutcome {
53        result,
54        verdict,
55        settlement,
56    })
57}
58
59#[cfg(test)]
60mod tests {
61    use super::super::executor::{JobExecutor, LocalKernelExecutor};
62    use super::super::job::{JobInput, JobMode, JobResult, JobSpec};
63    use super::super::settlement::{Escrow, EscrowState, SettlementOutcome};
64    use super::super::SwarmError;
65    use super::*;
66
67    fn dense_job(mode: JobMode) -> JobSpec {
68        JobSpec::new(
69            mode,
70            JobInput::DenseLinearProduct {
71                m: 2,
72                k: 2,
73                n: 2,
74                a: vec![1.0, 2.0, 3.0, 4.0],
75                b: vec![5.0, 6.0, 7.0, 8.0],
76            },
77        )
78    }
79
80    /// A dishonest executor that returns a wrong product — the adversary the
81    /// verify-before-pay gate exists to stop.
82    struct LyingExecutor;
83    impl JobExecutor for LyingExecutor {
84        fn execute(&self, input: &JobInput) -> Result<JobResult, SwarmError> {
85            match input {
86                JobInput::DenseLinearProduct { m, n, .. } => {
87                    Ok(JobResult::DenseLinearProduct {
88                        c: vec![0.0; m * n],
89                    }) // all zeros — wrong
90                }
91                _ => Err(SwarmError::InvalidJob),
92            }
93        }
94    }
95
96    fn paid_mode() -> JobMode {
97        JobMode::Paid {
98            requester_did: 1,
99            provider_did: 2,
100            price_micro_units: 500,
101        }
102    }
103
104    #[test]
105    fn honest_paid_job_verifies_and_pays() {
106        let spec = dense_job(paid_mode());
107        let mut escrow = Escrow::offer(spec.id, 1, 2, 500, "$ilp.solar/pay", false);
108        escrow.hold().unwrap();
109        let out = run_job(
110            &spec,
111            &LocalKernelExecutor,
112            VerifyPolicy::default(),
113            Some(&mut escrow),
114        )
115        .unwrap();
116        assert!(out.verdict.is_verified());
117        assert!(matches!(out.settlement, Some(SettlementOutcome::Pay(_))));
118        assert_eq!(escrow.state, EscrowState::ReleasedToProvider);
119    }
120
121    #[test]
122    fn lying_paid_job_is_rejected_and_refunded_never_paid() {
123        let spec = dense_job(paid_mode());
124        let mut escrow = Escrow::offer(spec.id, 1, 2, 500, "$ilp.solar/pay", false);
125        escrow.hold().unwrap();
126        let out = run_job(
127            &spec,
128            &LyingExecutor,
129            VerifyPolicy::default(),
130            Some(&mut escrow),
131        )
132        .unwrap();
133        assert!(!out.verdict.is_verified());
134        assert!(
135            matches!(out.settlement, Some(SettlementOutcome::Refund { .. })),
136            "a lying provider must never be paid"
137        );
138        assert_eq!(escrow.state, EscrowState::RefundedToRequester);
139    }
140
141    #[test]
142    fn personal_job_runs_with_no_settlement() {
143        let spec = dense_job(JobMode::Personal);
144        let out = run_job(&spec, &LocalKernelExecutor, VerifyPolicy::default(), None).unwrap();
145        assert!(out.verdict.is_verified());
146        assert!(out.settlement.is_none());
147    }
148
149    #[test]
150    fn collaborative_job_runs_with_no_settlement() {
151        let spec = dense_job(JobMode::Collaborative { peers: vec![7, 8] });
152        let out = run_job(&spec, &LocalKernelExecutor, VerifyPolicy::default(), None).unwrap();
153        assert!(out.verdict.is_verified());
154        assert!(out.settlement.is_none());
155    }
156}