Coverage Report

Created: 2026-02-04 05:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/runner/work/lyquor/lyquor/crypto/src/ed25519.rs
Line
Count
Source
1
use crate::EcdsaCipher;
2
use lyquor_primitives::alloy_primitives::{self, U256, uint};
3
4
// The prime modulus: 2^255 - 19 ("p_0" in ed25519.sol)
5
pub const P: U256 = uint!(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed_U256);
6
// Edwards 'd' coefficient: -121665 * inv(121666) mod P ("d_0" in ed25519.sol)
7
pub const D_0: U256 = uint!(0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3_U256);
8
// SCL Mapping Constant 'delta' = A_mont / 3 ("delta" in ed25519.sol)
9
pub const DELTA: U256 = uint!(0x2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad2451_U256);
10
// SCL Mapping Constant 'c' ("c" in ed25519.sol)
11
pub const C_VAL: U256 = uint!(0x70d9120b9f5ff9442d84f723fc03b0813a5e2c2eb482e57d3391fb5500ba81e7_U256);
12
// Target Curve Coefficient 'a' = 1 - A^2/3 ("a_0" in ed25519.sol)
13
pub const WEI_A: U256 = uint!(19298681539552699237261830834781317975544997444273427339909597334573241639236_U256);
14
// Target Curve Coefficient 'b' = (2A^3 - 9A)/27 ("b_0" in ed25519.sol)
15
pub const WEI_B: U256 = uint!(55751746669818908907645289078257140818241103727901012315294400837956729358436_U256);
16
17
/// Convert from an ed25519 public key from the Rust library to SCL's format (see
18
/// src/eth/lib/ed25519.sol of lyquor-seq crate).
19
#[derive(Debug, Clone)]
20
pub struct SCLPubkey {
21
    pub qx: U256,
22
    pub qy: U256,
23
    pub compressed: U256,
24
}
25
26
impl SCLPubkey {
27
    /// Converts Ed25519 bytes -> SCL Weierstrass Inputs
28
1
    pub fn new(key: [u8; 32]) -> SCLPubkey {
29
1
        let (ed_x, ed_y) = recover_edwards_x(key);
30
1
        let (qx, qy) = map_to_weierstrass(ed_x, ed_y);
31
1
        let compressed = U256::from_be_bytes(key);
32
33
1
        SCLPubkey { qx, qy, compressed }
34
1
    }
35
36
0
    pub fn to_ed25519(&self) -> [U256; 5] {
37
0
        [
38
0
            self.qx,         // Index 0: Mapped Weirstrass X
39
0
            self.qy,         // Index 1: Mapped Weirstrass Y
40
0
            U256::ZERO,      // Index 2: Unused/Padding
41
0
            U256::ZERO,      // Index 3: Unused/Padding
42
0
            self.compressed, // Index 4: Original Ed25519 Compressed Y (for hash validation)
43
0
        ]
44
0
    }
45
}
46
47
/// Decompresses an Ed25519 public key (recovers `x` from `y`).
48
1
fn recover_edwards_x(mut y_bytes: [u8; 32]) -> (U256, U256) {
49
1
    let p = P;
50
51
    // Parse Y and Sign Bit (Little Endian)
52
1
    let sign_bit = (y_bytes[31] & 0x80) >> 7;
53
1
    y_bytes[31] &= 0x7F;
54
1
    let y = U256::from_le_bytes(y_bytes);
55
56
    // u = y^2 - 1
57
1
    let y_sq = y.mul_mod(y, p);
58
1
    let one = U256::from(1);
59
1
    let u = if y_sq >= one { y_sq - one } else { 
p + y_sq - one0
};
60
61
    // v = d*y^2 + 1
62
1
    let d_times_y_sq = D_0.mul_mod(y_sq, p);
63
1
    let v = d_times_y_sq.add_mod(one, p);
64
65
    // x^2 = u * v^-1
66
1
    let v_inv = v.inv_mod(p).expect("Invalid Key: v is zero");
67
1
    let x_sq = u.mul_mod(v_inv, p);
68
69
    // sqrt(x^2) using p = 5 mod 8
70
1
    let three = U256::from(3);
71
1
    let eight = U256::from(8);
72
1
    let exp = p.wrapping_add(three).checked_div(eight).unwrap();
73
1
    let mut x = x_sq.pow_mod(exp, p);
74
75
    // Check root, multiply by sqrt(-1) if needed
76
1
    if x.mul_mod(x, p) != x_sq {
77
1
        let four = U256::from(4);
78
1
        let exp_i = p.wrapping_sub(one).checked_div(four).unwrap();
79
1
        let i = U256::from(2).pow_mod(exp_i, p);
80
1
        x = x.mul_mod(i, p);
81
1
    
}0
82
83
    // Adjust sign
84
1
    let is_odd = x.bit(0);
85
1
    let sign_is_set = sign_bit == 1;
86
1
    if is_odd != sign_is_set {
87
1
        x = p.wrapping_sub(x);
88
1
    
}0
89
90
1
    (x, y)
91
1
}
92
93
/// Maps an Ed25519 point $(x_{ed}, y_{ed})$ to a Short Weierstrass point $(X_w, Y_w)$.
94
1
fn map_to_weierstrass(ed_x: U256, ed_y: U256) -> (U256, U256) {
95
1
    let p = P;
96
1
    let one = U256::from(1);
97
98
    // (1 + y)
99
1
    let one_plus_y = one.add_mod(ed_y, p);
100
101
    // (1 - y)^-1
102
1
    let one_minus_y = if one >= ed_y {
103
0
        one - ed_y
104
    } else {
105
1
        p.wrapping_add(one).wrapping_sub(ed_y)
106
    };
107
1
    let one_minus_y_inv = one_minus_y.inv_mod(p).expect("Point at infinity?");
108
109
    // X_w = delta + (1 + y)/(1 - y)
110
1
    let term_x = one_plus_y.mul_mod(one_minus_y_inv, p);
111
1
    let x_w = DELTA.add_mod(term_x, p);
112
113
    // Y_w = C * (1 + y) / ((1 - y) * x)
114
1
    let num_y = C_VAL.mul_mod(one_plus_y, p);
115
1
    let den_base = one_minus_y.mul_mod(ed_x, p);
116
1
    let den_y = den_base.inv_mod(p).expect("Divide by zero in map");
117
1
    let y_w = num_y.mul_mod(den_y, p);
118
119
1
    (x_w, y_w)
120
1
}
121
122
0
pub fn set_ed25519_address_args(kp: &crate::ed25519_compact::KeyPair) -> Vec<String> {
123
0
    let scl_pub = SCLPubkey::new(*kp.pk);
124
0
    let ecdsa_cipher = EcdsaCipher::new(kp);
125
0
    let addr = ecdsa_cipher.address();
126
0
    let mut msg = Vec::new();
127
0
    msg.extend_from_slice(b"lyquor_set_ed25519_address\0");
128
0
    msg.extend_from_slice(addr.as_slice());
129
130
0
    let sig = kp.sk.sign(&msg, None);
131
0
    let sig_bytes = sig.as_ref();
132
133
0
    let r = alloy_primitives::U256::from_be_bytes::<32>(sig_bytes[0..32].try_into().unwrap());
134
0
    let s = alloy_primitives::U256::from_be_bytes::<32>(sig_bytes[32..64].try_into().unwrap());
135
0
    let pubkey = alloy_primitives::B256::from(scl_pub.compressed.to_be_bytes::<32>());
136
137
0
    vec![
138
0
        addr.to_string(),
139
0
        format!("0x{:x}", pubkey),
140
0
        format!("0x{:x}", scl_pub.qx),
141
0
        format!("0x{:x}", scl_pub.qy),
142
0
        format!("0x{:x}", r),
143
0
        format!("0x{:x}", s),
144
    ]
145
0
}
146
147
#[cfg(test)]
148
mod tests {
149
    use super::*;
150
    use lyquor_primitives::alloy_primitives::hex;
151
152
    #[test]
153
1
    fn test_rfc8032_example() {
154
        // RFC 8032 Test 1: https://datatracker.ietf.org/doc/html/rfc8032#section-7.1
155
1
        let pubkey_hex = hex::decode("0xd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a").unwrap();
156
1
        let mut bytes = [0u8; 32];
157
1
        bytes.copy_from_slice(&pubkey_hex);
158
1
        let result = SCLPubkey::new(bytes);
159
160
        // GOLDEN VALUES (tested in sequencer/src/eth/ed25519_test.sol)
161
1
        let qx = uint!(0x5961a13b250782afee75256fdba2e6bec4d810f89f6ce1c033585acd96b48329_U256);
162
1
        let qy = uint!(0x53373f33d468fee07fb2e53496849c8e52b3db37af7729999b2c3d372aca8de5_U256);
163
164
1
        assert_eq!(result.qx, qx, "X-coordinate mismatch");
165
1
        assert_eq!(result.qy, qy, "Y-coordinate mismatch");
166
1
    }
167
}