Coverage Report

Created: 2026-03-22 03:56

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