Skip to main content

rialo_s_ed25519_program/
lib.rs

1//! Instructions for the [ed25519 native program][np].
2//!
3//! [np]: https://docs.solanalabs.com/runtime/programs#ed25519-program
4
5use bytemuck::bytes_of;
6use bytemuck_derive::{Pod, Zeroable};
7use ed25519_dalek1::{ed25519::signature::Signature, Signer, Verifier};
8use rialo_s_feature_set::{ed25519_precompile_verify_strict, FeatureSet};
9use rialo_s_instruction::Instruction;
10use rialo_s_precompile_error::PrecompileError;
11
12pub const PUBKEY_SERIALIZED_SIZE: usize = 32;
13pub const SIGNATURE_SERIALIZED_SIZE: usize = 64;
14pub const SIGNATURE_OFFSETS_SERIALIZED_SIZE: usize = 14;
15// bytemuck requires structures to be aligned
16pub const SIGNATURE_OFFSETS_START: usize = 2;
17pub const DATA_START: usize = SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START;
18
19#[derive(Default, Debug, Copy, Clone, Zeroable, Pod, Eq, PartialEq)]
20#[repr(C)]
21pub struct Ed25519SignatureOffsets {
22    signature_offset: u16,             // offset to ed25519 signature of 64 bytes
23    signature_instruction_index: u16,  // instruction index to find signature
24    public_key_offset: u16,            // offset to public key of 32 bytes
25    public_key_instruction_index: u16, // instruction index to find public key
26    message_data_offset: u16,          // offset to start of message data
27    message_data_size: u16,            // size of message data
28    message_instruction_index: u16,    // index of instruction data to get message data
29}
30
31/// Encode just the signature offsets in a single ed25519 instruction.
32///
33/// This is a convenience function for rare cases where we wish to verify multiple messages in
34/// the same instruction. The verification data can be stored in a separate instruction specified
35/// by the `*_instruction_index` fields of `offsets`, or in this instruction by extending the data
36/// buffer.
37///
38/// Note: If the signer for these messages are the same, it is cheaper to concatenate the messages
39/// and have the signer sign the single buffer and use [`new_ed25519_instruction`].
40pub fn offsets_to_ed25519_instruction(offsets: &[Ed25519SignatureOffsets]) -> Instruction {
41    let mut instruction_data = Vec::with_capacity(
42        SIGNATURE_OFFSETS_START
43            .saturating_add(SIGNATURE_OFFSETS_SERIALIZED_SIZE.saturating_mul(offsets.len())),
44    );
45
46    let num_signatures = offsets.len() as u16;
47    instruction_data.extend_from_slice(&num_signatures.to_le_bytes());
48
49    for offsets in offsets {
50        instruction_data.extend_from_slice(bytes_of(offsets));
51    }
52
53    Instruction {
54        program_id: rialo_s_sdk_ids::ed25519_program::id(),
55        accounts: vec![],
56        data: instruction_data,
57    }
58}
59
60pub fn new_ed25519_instruction(keypair: &ed25519_dalek1::Keypair, message: &[u8]) -> Instruction {
61    let signature = keypair.sign(message).to_bytes();
62    let pubkey = keypair.public.to_bytes();
63
64    assert_eq!(pubkey.len(), PUBKEY_SERIALIZED_SIZE);
65    assert_eq!(signature.len(), SIGNATURE_SERIALIZED_SIZE);
66
67    let mut instruction_data = Vec::with_capacity(
68        DATA_START
69            .saturating_add(SIGNATURE_SERIALIZED_SIZE)
70            .saturating_add(PUBKEY_SERIALIZED_SIZE)
71            .saturating_add(message.len()),
72    );
73
74    let num_signatures: u8 = 1;
75    let public_key_offset = DATA_START;
76    let signature_offset = public_key_offset.saturating_add(PUBKEY_SERIALIZED_SIZE);
77    let message_data_offset = signature_offset.saturating_add(SIGNATURE_SERIALIZED_SIZE);
78
79    // add padding byte so that offset structure is aligned
80    instruction_data.extend_from_slice(bytes_of(&[num_signatures, 0]));
81
82    let offsets = Ed25519SignatureOffsets {
83        signature_offset: signature_offset as u16,
84        signature_instruction_index: u16::MAX,
85        public_key_offset: public_key_offset as u16,
86        public_key_instruction_index: u16::MAX,
87        message_data_offset: message_data_offset as u16,
88        message_data_size: message.len() as u16,
89        message_instruction_index: u16::MAX,
90    };
91
92    instruction_data.extend_from_slice(bytes_of(&offsets));
93
94    debug_assert_eq!(instruction_data.len(), public_key_offset);
95
96    instruction_data.extend_from_slice(&pubkey);
97
98    debug_assert_eq!(instruction_data.len(), signature_offset);
99
100    instruction_data.extend_from_slice(&signature);
101
102    debug_assert_eq!(instruction_data.len(), message_data_offset);
103
104    instruction_data.extend_from_slice(message);
105
106    Instruction {
107        program_id: rialo_s_sdk_ids::ed25519_program::id(),
108        accounts: vec![],
109        data: instruction_data,
110    }
111}
112
113pub fn verify(
114    data: &[u8],
115    instruction_datas: &[&[u8]],
116    feature_set: &FeatureSet,
117) -> Result<(), PrecompileError> {
118    if data.len() < SIGNATURE_OFFSETS_START {
119        return Err(PrecompileError::InvalidInstructionDataSize);
120    }
121    let num_signatures = data[0] as usize;
122    if num_signatures == 0 && data.len() > SIGNATURE_OFFSETS_START {
123        return Err(PrecompileError::InvalidInstructionDataSize);
124    }
125    let expected_data_size = num_signatures
126        .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE)
127        .saturating_add(SIGNATURE_OFFSETS_START);
128    // We do not check or use the byte at data[1]
129    if data.len() < expected_data_size {
130        return Err(PrecompileError::InvalidInstructionDataSize);
131    }
132    for i in 0..num_signatures {
133        let start = i
134            .saturating_mul(SIGNATURE_OFFSETS_SERIALIZED_SIZE)
135            .saturating_add(SIGNATURE_OFFSETS_START);
136        let end = start.saturating_add(SIGNATURE_OFFSETS_SERIALIZED_SIZE);
137
138        // bytemuck wants structures aligned
139        let offsets: &Ed25519SignatureOffsets = bytemuck::try_from_bytes(&data[start..end])
140            .map_err(|_| PrecompileError::InvalidDataOffsets)?;
141
142        // Parse out signature
143        let signature = get_data_slice(
144            data,
145            instruction_datas,
146            offsets.signature_instruction_index,
147            offsets.signature_offset,
148            SIGNATURE_SERIALIZED_SIZE,
149        )?;
150
151        let signature =
152            Signature::from_bytes(signature).map_err(|_| PrecompileError::InvalidSignature)?;
153
154        // Parse out pubkey
155        let pubkey = get_data_slice(
156            data,
157            instruction_datas,
158            offsets.public_key_instruction_index,
159            offsets.public_key_offset,
160            PUBKEY_SERIALIZED_SIZE,
161        )?;
162
163        let publickey = ed25519_dalek1::PublicKey::from_bytes(pubkey)
164            .map_err(|_| PrecompileError::InvalidPublicKey)?;
165
166        // Parse out message
167        let message = get_data_slice(
168            data,
169            instruction_datas,
170            offsets.message_instruction_index,
171            offsets.message_data_offset,
172            offsets.message_data_size as usize,
173        )?;
174
175        if feature_set.is_active(&ed25519_precompile_verify_strict::id()) {
176            publickey
177                .verify_strict(message, &signature)
178                .map_err(|_| PrecompileError::InvalidSignature)?;
179        } else {
180            publickey
181                .verify(message, &signature)
182                .map_err(|_| PrecompileError::InvalidSignature)?;
183        }
184    }
185    Ok(())
186}
187
188fn get_data_slice<'a>(
189    data: &'a [u8],
190    instruction_datas: &'a [&[u8]],
191    instruction_index: u16,
192    offset_start: u16,
193    size: usize,
194) -> Result<&'a [u8], PrecompileError> {
195    let instruction = if instruction_index == u16::MAX {
196        data
197    } else {
198        let signature_index = instruction_index as usize;
199        if signature_index >= instruction_datas.len() {
200            return Err(PrecompileError::InvalidDataOffsets);
201        }
202        instruction_datas[signature_index]
203    };
204
205    let start = offset_start as usize;
206    let end = start.saturating_add(size);
207    if end > instruction.len() {
208        return Err(PrecompileError::InvalidDataOffsets);
209    }
210
211    Ok(&instruction[start..end])
212}
213
214#[cfg(test)]
215pub mod test {
216    use ed25519_dalek1::Signer as EdSigner;
217    use hex;
218    use rand0_7::{thread_rng, Rng};
219    use rialo_s_feature_set::FeatureSet;
220    use rialo_s_keypair::Keypair;
221    use rialo_s_sdk::transaction::Transaction;
222    use rialo_s_signer::Signer;
223
224    use super::*;
225
226    pub fn new_ed25519_instruction_raw(
227        pubkey: &[u8],
228        signature: &[u8],
229        message: &[u8],
230    ) -> Instruction {
231        assert_eq!(pubkey.len(), PUBKEY_SERIALIZED_SIZE);
232        assert_eq!(signature.len(), SIGNATURE_SERIALIZED_SIZE);
233
234        let mut instruction_data = Vec::with_capacity(
235            DATA_START
236                .saturating_add(SIGNATURE_SERIALIZED_SIZE)
237                .saturating_add(PUBKEY_SERIALIZED_SIZE)
238                .saturating_add(message.len()),
239        );
240
241        let num_signatures: u8 = 1;
242        let public_key_offset = DATA_START;
243        let signature_offset = public_key_offset.saturating_add(PUBKEY_SERIALIZED_SIZE);
244        let message_data_offset = signature_offset.saturating_add(SIGNATURE_SERIALIZED_SIZE);
245
246        // add padding byte so that offset structure is aligned
247        instruction_data.extend_from_slice(bytes_of(&[num_signatures, 0]));
248
249        let offsets = Ed25519SignatureOffsets {
250            signature_offset: signature_offset as u16,
251            signature_instruction_index: u16::MAX,
252            public_key_offset: public_key_offset as u16,
253            public_key_instruction_index: u16::MAX,
254            message_data_offset: message_data_offset as u16,
255            message_data_size: message.len() as u16,
256            message_instruction_index: u16::MAX,
257        };
258
259        instruction_data.extend_from_slice(bytes_of(&offsets));
260
261        debug_assert_eq!(instruction_data.len(), public_key_offset);
262
263        instruction_data.extend_from_slice(pubkey);
264
265        debug_assert_eq!(instruction_data.len(), signature_offset);
266
267        instruction_data.extend_from_slice(signature);
268
269        debug_assert_eq!(instruction_data.len(), message_data_offset);
270
271        instruction_data.extend_from_slice(message);
272
273        Instruction {
274            program_id: rialo_s_sdk_ids::ed25519_program::id(),
275            accounts: vec![],
276            data: instruction_data,
277        }
278    }
279
280    fn test_case(
281        num_signatures: u16,
282        offsets: &Ed25519SignatureOffsets,
283    ) -> Result<(), PrecompileError> {
284        assert_eq!(
285            bytemuck::bytes_of(offsets).len(),
286            SIGNATURE_OFFSETS_SERIALIZED_SIZE
287        );
288
289        let mut instruction_data = vec![0u8; DATA_START];
290        instruction_data[0..SIGNATURE_OFFSETS_START].copy_from_slice(bytes_of(&num_signatures));
291        instruction_data[SIGNATURE_OFFSETS_START..DATA_START].copy_from_slice(bytes_of(offsets));
292
293        verify(
294            &instruction_data,
295            &[&[0u8; 100]],
296            &FeatureSet::all_enabled(),
297        )
298    }
299
300    #[test]
301    fn test_invalid_offsets() {
302        rialo_s_logger::setup();
303
304        let mut instruction_data = vec![0u8; DATA_START];
305        let offsets = Ed25519SignatureOffsets::default();
306        instruction_data[0..SIGNATURE_OFFSETS_START].copy_from_slice(bytes_of(&1u16));
307        instruction_data[SIGNATURE_OFFSETS_START..DATA_START].copy_from_slice(bytes_of(&offsets));
308        instruction_data.truncate(instruction_data.len() - 1);
309
310        assert_eq!(
311            verify(
312                &instruction_data,
313                &[&[0u8; 100]],
314                &FeatureSet::all_enabled(),
315            ),
316            Err(PrecompileError::InvalidInstructionDataSize)
317        );
318
319        let offsets = Ed25519SignatureOffsets {
320            signature_instruction_index: 1,
321            ..Ed25519SignatureOffsets::default()
322        };
323        assert_eq!(
324            test_case(1, &offsets),
325            Err(PrecompileError::InvalidDataOffsets)
326        );
327
328        let offsets = Ed25519SignatureOffsets {
329            message_instruction_index: 1,
330            ..Ed25519SignatureOffsets::default()
331        };
332        assert_eq!(
333            test_case(1, &offsets),
334            Err(PrecompileError::InvalidDataOffsets)
335        );
336
337        let offsets = Ed25519SignatureOffsets {
338            public_key_instruction_index: 1,
339            ..Ed25519SignatureOffsets::default()
340        };
341        assert_eq!(
342            test_case(1, &offsets),
343            Err(PrecompileError::InvalidDataOffsets)
344        );
345    }
346
347    #[test]
348    fn test_message_data_offsets() {
349        let offsets = Ed25519SignatureOffsets {
350            message_data_offset: 99,
351            message_data_size: 1,
352            ..Ed25519SignatureOffsets::default()
353        };
354        assert_eq!(
355            test_case(1, &offsets),
356            Err(PrecompileError::InvalidSignature)
357        );
358
359        let offsets = Ed25519SignatureOffsets {
360            message_data_offset: 100,
361            message_data_size: 1,
362            ..Ed25519SignatureOffsets::default()
363        };
364        assert_eq!(
365            test_case(1, &offsets),
366            Err(PrecompileError::InvalidDataOffsets)
367        );
368
369        let offsets = Ed25519SignatureOffsets {
370            message_data_offset: 100,
371            message_data_size: 1000,
372            ..Ed25519SignatureOffsets::default()
373        };
374        assert_eq!(
375            test_case(1, &offsets),
376            Err(PrecompileError::InvalidDataOffsets)
377        );
378
379        let offsets = Ed25519SignatureOffsets {
380            message_data_offset: u16::MAX,
381            message_data_size: u16::MAX,
382            ..Ed25519SignatureOffsets::default()
383        };
384        assert_eq!(
385            test_case(1, &offsets),
386            Err(PrecompileError::InvalidDataOffsets)
387        );
388    }
389
390    #[test]
391    fn test_pubkey_offset() {
392        let offsets = Ed25519SignatureOffsets {
393            public_key_offset: u16::MAX,
394            ..Ed25519SignatureOffsets::default()
395        };
396        assert_eq!(
397            test_case(1, &offsets),
398            Err(PrecompileError::InvalidDataOffsets)
399        );
400
401        let offsets = Ed25519SignatureOffsets {
402            public_key_offset: 100 - PUBKEY_SERIALIZED_SIZE as u16 + 1,
403            ..Ed25519SignatureOffsets::default()
404        };
405        assert_eq!(
406            test_case(1, &offsets),
407            Err(PrecompileError::InvalidDataOffsets)
408        );
409    }
410
411    #[test]
412    fn test_signature_offset() {
413        let offsets = Ed25519SignatureOffsets {
414            signature_offset: u16::MAX,
415            ..Ed25519SignatureOffsets::default()
416        };
417        assert_eq!(
418            test_case(1, &offsets),
419            Err(PrecompileError::InvalidDataOffsets)
420        );
421
422        let offsets = Ed25519SignatureOffsets {
423            signature_offset: 100 - SIGNATURE_SERIALIZED_SIZE as u16 + 1,
424            ..Ed25519SignatureOffsets::default()
425        };
426        assert_eq!(
427            test_case(1, &offsets),
428            Err(PrecompileError::InvalidDataOffsets)
429        );
430    }
431
432    #[test]
433    fn test_ed25519() {
434        rialo_s_logger::setup();
435
436        let privkey = ed25519_dalek1::Keypair::generate(&mut thread_rng());
437        let message_arr = b"hello";
438        let mut instruction = new_ed25519_instruction(&privkey, message_arr);
439        let mint_keypair = Keypair::new();
440        let feature_set = FeatureSet::all_enabled();
441        let valid_from = 0;
442
443        let tx = Transaction::new_signed_with_payer(
444            &[instruction.clone()],
445            Some(&mint_keypair.pubkey()),
446            &[&mint_keypair],
447            valid_from,
448        );
449
450        assert!(tx.verify_precompiles(&feature_set).is_ok());
451
452        let index = loop {
453            let index = thread_rng().gen_range(0, instruction.data.len());
454            // byte 1 is not used, so this would not cause the verify to fail
455            if index != 1 {
456                break index;
457            }
458        };
459
460        instruction.data[index] = instruction.data[index].wrapping_add(12);
461        let tx = Transaction::new_signed_with_payer(
462            &[instruction],
463            Some(&mint_keypair.pubkey()),
464            &[&mint_keypair],
465            valid_from,
466        );
467        assert!(tx.verify_precompiles(&feature_set).is_err());
468    }
469
470    #[test]
471    fn test_offsets_to_ed25519_instruction() {
472        rialo_s_logger::setup();
473
474        let privkey = ed25519_dalek1::Keypair::generate(&mut thread_rng());
475        let messages: [&[u8]; 3] = [b"hello", b"IBRL", b"goodbye"];
476        let data_start =
477            messages.len() * SIGNATURE_OFFSETS_SERIALIZED_SIZE + SIGNATURE_OFFSETS_START;
478        let mut data_offset = data_start + PUBKEY_SERIALIZED_SIZE;
479        let (offsets, messages): (Vec<_>, Vec<_>) = messages
480            .into_iter()
481            .map(|message| {
482                let signature_offset = data_offset;
483                let message_data_offset = signature_offset + SIGNATURE_SERIALIZED_SIZE;
484                data_offset += SIGNATURE_SERIALIZED_SIZE + message.len();
485
486                let offsets = Ed25519SignatureOffsets {
487                    signature_offset: signature_offset as u16,
488                    signature_instruction_index: u16::MAX,
489                    public_key_offset: data_start as u16,
490                    public_key_instruction_index: u16::MAX,
491                    message_data_offset: message_data_offset as u16,
492                    message_data_size: message.len() as u16,
493                    message_instruction_index: u16::MAX,
494                };
495
496                (offsets, message)
497            })
498            .unzip();
499
500        let mut instruction = offsets_to_ed25519_instruction(&offsets);
501
502        let pubkey = privkey.public.as_ref();
503        instruction.data.extend_from_slice(pubkey);
504
505        for message in messages {
506            let signature = privkey.sign(message).to_bytes();
507            instruction.data.extend_from_slice(&signature);
508            instruction.data.extend_from_slice(message);
509        }
510
511        let mint_keypair = Keypair::new();
512        let feature_set = FeatureSet::all_enabled();
513        let valid_from = 0;
514
515        let tx = Transaction::new_signed_with_payer(
516            &[instruction.clone()],
517            Some(&mint_keypair.pubkey()),
518            &[&mint_keypair],
519            valid_from,
520        );
521
522        assert!(tx.verify_precompiles(&feature_set).is_ok());
523
524        let index = loop {
525            let index = thread_rng().gen_range(0, instruction.data.len());
526            // byte 1 is not used, so this would not cause the verify to fail
527            if index != 1 {
528                break index;
529            }
530        };
531
532        instruction.data[index] = instruction.data[index].wrapping_add(12);
533        let tx = Transaction::new_signed_with_payer(
534            &[instruction],
535            Some(&mint_keypair.pubkey()),
536            &[&mint_keypair],
537            valid_from,
538        );
539        assert!(tx.verify_precompiles(&feature_set).is_err());
540    }
541
542    #[test]
543    fn test_ed25519_malleability() {
544        rialo_s_logger::setup();
545        let mint_keypair = Keypair::new();
546
547        // sig created via ed25519_dalek: both pass
548        let privkey = ed25519_dalek1::Keypair::generate(&mut thread_rng());
549        let message_arr = b"hello";
550        let valid_from = 0;
551        let instruction = new_ed25519_instruction(&privkey, message_arr);
552        let tx = Transaction::new_signed_with_payer(
553            std::slice::from_ref(&instruction),
554            Some(&mint_keypair.pubkey()),
555            &[&mint_keypair],
556            valid_from,
557        );
558
559        let feature_set = FeatureSet::default();
560        assert!(tx.verify_precompiles(&feature_set).is_ok());
561
562        let feature_set = FeatureSet::all_enabled();
563        assert!(tx.verify_precompiles(&feature_set).is_ok());
564
565        // malleable sig: verify_strict does NOT pass
566        // for example, test number 5:
567        // https://github.com/C2SP/CCTV/tree/main/ed25519
568        // R has low order (in fact R == 0)
569        let pubkey =
570            &hex::decode("10eb7c3acfb2bed3e0d6ab89bf5a3d6afddd1176ce4812e38d9fd485058fdb1f")
571                .unwrap();
572        let signature = &hex::decode("00000000000000000000000000000000000000000000000000000000000000009472a69cd9a701a50d130ed52189e2455b23767db52cacb8716fb896ffeeac09").unwrap();
573        let message = b"ed25519vectors 3";
574        let valid_from = 0;
575        let instruction = new_ed25519_instruction_raw(pubkey, signature, message);
576        let tx = Transaction::new_signed_with_payer(
577            std::slice::from_ref(&instruction),
578            Some(&mint_keypair.pubkey()),
579            &[&mint_keypair],
580            valid_from,
581        );
582
583        let feature_set = FeatureSet::default();
584        assert!(tx.verify_precompiles(&feature_set).is_ok());
585
586        let feature_set = FeatureSet::all_enabled();
587        assert!(tx.verify_precompiles(&feature_set).is_err()); // verify_strict does NOT pass
588    }
589}