Coverage Report

Created: 2026-05-23 17:32

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/lyquor/lyquor/lyquid-agent/src/harness/auth.rs
Line
Count
Source
1
//! Caller authentication helpers for service-shaped Lyquids.
2
//!
3
//! Service Lyquids expose state-changing methods to two distinct categories of caller:
4
//!
5
//! 1. **Lyquid orchestrator** — another Lyquid (the caller in a UPC chain) is allowed to
6
//!    submit intents. Authenticate with [`assert_authorized_lyquid_caller`], passing a
7
//!    `RequiredLyquid` field from the service's state.
8
//! 2. **EOA driver** — an off-chain runtime holding an EVM key records plans and drives
9
//!    operations. Authenticate with [`assert_authorized_eoa_caller`], passing an
10
//!    `Address` field.
11
//!
12
//! Both helpers reject the zero/default address; services should refuse state mutation
13
//! until init explicitly seats the authorized identity. The `role` argument shows up in
14
//! the error message ("expected authorized orchestrator") for grep-friendly debugging.
15
//!
16
//! These are deliberately plain functions rather than macros — trust boundaries should
17
//! be visible at the call site in code review, not hidden inside an attribute.
18
19
use lyquor_primitives::{Address, LyquidID, RequiredLyquid};
20
21
/// Returns `Ok(())` iff `caller` matches the Lyquid pinned in `expected`. Rejects an
22
/// uninitialized (`RequiredLyquid::default()`) `expected` so a service refuses caller-
23
/// gated methods until init has explicitly seated the orchestrator.
24
3
pub fn assert_authorized_lyquid_caller(caller: Address, expected: &RequiredLyquid, role: &str) -> Result<(), String> {
25
3
    if expected.0 == LyquidID::default() {
26
1
        return Err(format!("{role} not initialized"));
27
2
    }
28
2
    let expected_addr = Address::from(expected.0);
29
2
    if caller != expected_addr {
30
1
        return Err(format!(
31
1
            "unauthorized caller {caller}; expected authorized {role} {expected_addr}"
32
1
        ));
33
1
    }
34
1
    Ok(())
35
3
}
36
37
/// Returns `Ok(())` iff `caller == expected`. Rejects `Address::default()` so a service
38
/// refuses caller-gated methods until init has explicitly seated the EOA.
39
3
pub fn assert_authorized_eoa_caller(caller: Address, expected: Address, role: &str) -> Result<(), String> {
40
3
    if expected == Address::default() {
41
1
        return Err(format!("{role} not initialized"));
42
2
    }
43
2
    if caller != expected {
44
1
        return Err(format!(
45
1
            "unauthorized caller {caller}; expected authorized {role} {expected}"
46
1
        ));
47
1
    }
48
1
    Ok(())
49
3
}
50
51
/// Returns `Ok(())` iff `caller == deployer`. Used to gate admin-style ABIs
52
/// (`set_self_dispatch_enabled`, `set_authorized_orchestrator`, etc.) so only the
53
/// account that constructed the Lyquid can flip them. `action` shows up in the error
54
/// message ("only deployer can configure self dispatch") for grep-friendly debugging.
55
2
pub fn assert_deployer_caller(caller: Address, deployer: Address, action: &str) -> Result<(), String> {
56
2
    if caller != deployer {
57
1
        return Err(format!(
58
1
            "only deployer can {action}; caller={caller}, deployer={deployer}"
59
1
        ));
60
1
    }
61
1
    Ok(())
62
2
}
63
64
#[cfg(test)]
65
mod tests {
66
    use super::*;
67
68
2
    fn lyquid(b: u8) -> RequiredLyquid {
69
2
        RequiredLyquid(LyquidID([b; 20]))
70
2
    }
71
72
9
    fn addr_from_byte(b: u8) -> Address {
73
9
        Address::from(LyquidID([b; 20]))
74
9
    }
75
76
    #[test]
77
1
    fn lyquid_caller_rejects_uninitialized() {
78
1
        let caller = addr_from_byte(0x11);
79
1
        let err = assert_authorized_lyquid_caller(caller, &RequiredLyquid::default(), "orchestrator")
80
1
            .expect_err("default expected must be rejected");
81
1
        assert!(err.contains("orchestrator not initialized"), "got: {err}");
82
1
    }
83
84
    #[test]
85
1
    fn lyquid_caller_rejects_mismatch() {
86
1
        let expected = lyquid(0xaa);
87
1
        let wrong = addr_from_byte(0xbb);
88
1
        let err =
89
1
            assert_authorized_lyquid_caller(wrong, &expected, "orchestrator").expect_err("mismatch must be rejected");
90
1
        assert!(err.contains("unauthorized caller"), "got: {err}");
91
1
        assert!(err.contains("orchestrator"), "role missing from error: {err}");
92
1
    }
93
94
    #[test]
95
1
    fn lyquid_caller_accepts_match() {
96
1
        let expected = lyquid(0xcc);
97
1
        let matching = Address::from(expected.0);
98
1
        assert_authorized_lyquid_caller(matching, &expected, "orchestrator").expect("match must accept");
99
1
    }
100
101
    #[test]
102
1
    fn eoa_caller_rejects_uninitialized() {
103
1
        let caller = addr_from_byte(0x11);
104
1
        let err = assert_authorized_eoa_caller(caller, Address::default(), "driver")
105
1
            .expect_err("default expected must be rejected");
106
1
        assert!(err.contains("driver not initialized"), "got: {err}");
107
1
    }
108
109
    #[test]
110
1
    fn eoa_caller_rejects_mismatch() {
111
1
        let expected = addr_from_byte(0x11);
112
1
        let wrong = addr_from_byte(0x22);
113
1
        let err = assert_authorized_eoa_caller(wrong, expected, "driver").expect_err("mismatch must be rejected");
114
1
        assert!(err.contains("unauthorized caller"), "got: {err}");
115
1
        assert!(err.contains("driver"), "role missing from error: {err}");
116
1
    }
117
118
    #[test]
119
1
    fn eoa_caller_accepts_match() {
120
1
        let expected = addr_from_byte(0x33);
121
1
        assert_authorized_eoa_caller(expected, expected, "driver").expect("match must accept");
122
1
    }
123
124
    #[test]
125
1
    fn deployer_caller_rejects_mismatch() {
126
1
        let deployer = addr_from_byte(0x44);
127
1
        let wrong = addr_from_byte(0x55);
128
1
        let err =
129
1
            assert_deployer_caller(wrong, deployer, "configure self dispatch").expect_err("mismatch must be rejected");
130
1
        assert!(err.contains("only deployer can configure self dispatch"), "got: {err}");
131
1
        assert!(err.contains("caller="), "caller missing from error: {err}");
132
1
    }
133
134
    #[test]
135
1
    fn deployer_caller_accepts_match() {
136
1
        let deployer = addr_from_byte(0x66);
137
1
        assert_deployer_caller(deployer, deployer, "configure").expect("match must accept");
138
1
    }
139
}