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