Skip to main content

zksync_vm2/precompiles/
legacy.rs

1use primitive_types::{H160, U256};
2use zk_evm_abstractions::{
3    aux::Timestamp,
4    precompiles::{
5        ecadd::ecadd_function, ecmul::ecmul_function, ecpairing::ecpairing_function,
6        ecrecover::ecrecover_function, keccak256::keccak256_rounds_function,
7        modexp::modexp_function, secp256r1_verify::secp256r1_verify_function,
8        sha256::sha256_rounds_function,
9    },
10    queries::{LogQuery, MemoryQuery},
11    vm::Memory,
12    zkevm_opcode_defs::{
13        ECADD_PRECOMPILE_ADDRESS, ECMUL_PRECOMPILE_ADDRESS, ECPAIRING_PRECOMPILE_ADDRESS,
14        MODEXP_PRECOMPILE_ADDRESS,
15    },
16};
17use zkevm_opcode_defs::{
18    PrecompileCallABI, ECRECOVER_INNER_FUNCTION_PRECOMPILE_ADDRESS,
19    KECCAK256_ROUND_FUNCTION_PRECOMPILE_ADDRESS, SECP256R1_VERIFY_PRECOMPILE_ADDRESS,
20    SHA256_ROUND_FUNCTION_PRECOMPILE_ADDRESS,
21};
22use zksync_vm2_interface::CycleStats;
23
24use super::{PrecompileMemoryReader, PrecompileOutput, Precompiles};
25
26fn create_query(input_offset: u32, input_len: u32, aux_data: u64) -> LogQuery {
27    let abi = PrecompileCallABI {
28        input_memory_offset: input_offset,
29        input_memory_length: input_len,
30        output_memory_offset: 0,
31        output_memory_length: 2, // not read by implementations
32        // Pages are fake; we assume that precompiles are implemented correctly and don't read / write anywhere but the specified pages
33        memory_page_to_read: 1,
34        memory_page_to_write: 2,
35        precompile_interpreted_data: aux_data,
36    };
37    LogQuery {
38        timestamp: Timestamp(0),
39        key: abi.to_u256(),
40        // only two first fields are read by the precompile
41        tx_number_in_block: Default::default(),
42        aux_byte: Default::default(),
43        shard_id: Default::default(),
44        address: H160::default(),
45        read_value: U256::default(),
46        written_value: U256::default(),
47        rw_flag: Default::default(),
48        rollback: Default::default(),
49        is_service: Default::default(),
50    }
51}
52
53#[derive(Debug)]
54struct LegacyIo<'a> {
55    input: PrecompileMemoryReader<'a>,
56    output: PrecompileOutput,
57}
58
59impl<'a> LegacyIo<'a> {
60    fn new(input: PrecompileMemoryReader<'a>) -> Self {
61        Self {
62            input,
63            output: PrecompileOutput::default(),
64        }
65    }
66}
67
68impl Memory for LegacyIo<'_> {
69    fn execute_partial_query(
70        &mut self,
71        _monotonic_cycle_counter: u32,
72        mut query: MemoryQuery,
73    ) -> MemoryQuery {
74        let start_word = query.location.index.0;
75        if query.rw_flag {
76            assert!(start_word < 3, "standard precompiles never write >3 words");
77            self.output.buffer[start_word as usize] = query.value;
78            self.output.len = self.output.len.max(start_word + 1);
79        } else {
80            // Access `Heap` directly for a speed-up
81            query.value = self.input.heap.read_u256(start_word * 32);
82            query.value_is_pointer = false;
83        }
84        query
85    }
86
87    fn specialized_code_query(
88        &mut self,
89        _monotonic_cycle_counter: u32,
90        _query: MemoryQuery,
91    ) -> MemoryQuery {
92        unimplemented!("should not be called")
93    }
94
95    fn read_code_query(&self, _monotonic_cycle_counter: u32, _query: MemoryQuery) -> MemoryQuery {
96        unimplemented!("should not be called")
97    }
98}
99
100/// Precompiles implementation using legacy VM code.
101#[derive(Debug)]
102pub struct LegacyPrecompiles;
103
104impl Precompiles for LegacyPrecompiles {
105    #[allow(clippy::cast_possible_truncation)]
106    fn call_precompile(
107        &self,
108        address_low: u16,
109        memory: PrecompileMemoryReader<'_>,
110        aux_input: u64,
111    ) -> PrecompileOutput {
112        let query = create_query(memory.offset, memory.len, aux_input);
113        let mut io = LegacyIo::new(memory);
114        match address_low {
115            KECCAK256_ROUND_FUNCTION_PRECOMPILE_ADDRESS => {
116                let cycles = keccak256_rounds_function::<_, false>(0, query, &mut io).0;
117                io.output
118                    .with_cycle_stats(CycleStats::Keccak256(cycles as u32))
119            }
120            SHA256_ROUND_FUNCTION_PRECOMPILE_ADDRESS => {
121                let cycles = sha256_rounds_function::<_, false>(0, query, &mut io).0;
122                io.output
123                    .with_cycle_stats(CycleStats::Sha256(cycles as u32))
124            }
125            ECRECOVER_INNER_FUNCTION_PRECOMPILE_ADDRESS => {
126                let cycles = ecrecover_function::<_, false>(0, query, &mut io).0;
127                io.output
128                    .with_cycle_stats(CycleStats::EcRecover(cycles as u32))
129            }
130            SECP256R1_VERIFY_PRECOMPILE_ADDRESS => {
131                let cycles = secp256r1_verify_function::<_, false>(0, query, &mut io).0;
132                io.output
133                    .with_cycle_stats(CycleStats::Secp256r1Verify(cycles as u32))
134            }
135            MODEXP_PRECOMPILE_ADDRESS => {
136                let cycles = modexp_function::<_, false>(0, query, &mut io).0;
137                io.output
138                    .with_cycle_stats(CycleStats::ModExp(cycles as u32))
139            }
140            ECADD_PRECOMPILE_ADDRESS => {
141                let cycles = ecadd_function::<_, false>(0, query, &mut io).0;
142                io.output.with_cycle_stats(CycleStats::EcAdd(cycles as u32))
143            }
144            ECMUL_PRECOMPILE_ADDRESS => {
145                let cycles = ecmul_function::<_, false>(0, query, &mut io).0;
146                io.output.with_cycle_stats(CycleStats::EcMul(cycles as u32))
147            }
148            ECPAIRING_PRECOMPILE_ADDRESS => {
149                let cycles = ecpairing_function::<_, false>(0, query, &mut io).0;
150                io.output
151                    .with_cycle_stats(CycleStats::EcPairing(cycles as u32))
152            }
153            _ => PrecompileOutput::default(),
154        }
155    }
156}
157
158#[allow(clippy::cast_possible_truncation)] // OK for tests
159#[cfg(test)]
160mod tests {
161    use proptest::{array, collection, num, option, prelude::*};
162    use zkevm_opcode_defs::{
163        k256::ecdsa::{SigningKey as K256SigningKey, VerifyingKey as K256VerifyingKey},
164        p256::ecdsa::SigningKey as P256SigningKey,
165        sha3::{self, Digest},
166    };
167    use zksync_vm2_interface::HeapId;
168
169    use super::*;
170    use crate::heap::Heaps;
171
172    const MAX_LEN: usize = 2_048;
173
174    fn arbitrary_aligned_bytes(alignment: usize) -> impl Strategy<Value = Vec<u8>> {
175        (0..=(MAX_LEN / alignment)).prop_flat_map(move |len_in_words| {
176            collection::vec(num::u8::ANY, len_in_words * alignment)
177        })
178    }
179
180    fn key_to_address(key: &K256VerifyingKey) -> U256 {
181        let encoded_key = key.to_encoded_point(false);
182        let encoded_key = &encoded_key.as_bytes()[1..];
183        debug_assert_eq!(encoded_key.len(), 64);
184        let address_digest = sha3::Keccak256::digest(encoded_key);
185        let address_u256 = U256::from_big_endian(&address_digest);
186        // Mask out upper bytes of the hash.
187        address_u256 & (U256::MAX >> (256 - 160))
188    }
189
190    fn test_keccak_precompile(input: &[u8], initial_offset: u32) -> Result<(), TestCaseError> {
191        let input_len = input.len() as u32;
192        assert_eq!(input_len % 32, 0);
193
194        let mut heaps = Heaps::new(&[]);
195        for (i, u256_chunk) in input.chunks(32).enumerate() {
196            let offset = i as u32 * 32 + initial_offset;
197            heaps.write_u256(HeapId::FIRST, offset, U256::from_big_endian(u256_chunk));
198        }
199
200        let memory = PrecompileMemoryReader::new(&heaps[HeapId::FIRST], initial_offset, input_len);
201        let output = LegacyPrecompiles.call_precompile(
202            KECCAK256_ROUND_FUNCTION_PRECOMPILE_ADDRESS,
203            memory,
204            0,
205        );
206
207        prop_assert_eq!(output.len, 1);
208        let expected_hash = sha3::Keccak256::digest(input);
209        let expected_hash = U256::from_big_endian(&expected_hash);
210        prop_assert_eq!(output.buffer[0], expected_hash);
211        prop_assert!(matches!(output.cycle_stats, Some(CycleStats::Keccak256(_))));
212        Ok(())
213    }
214
215    fn test_sha256_precompile(
216        input: &[u8],
217        initial_offset_in_words: u32,
218    ) -> Result<(), TestCaseError> {
219        assert_eq!(input.len() % 64, 0);
220        let mut heaps = Heaps::new(&[]);
221
222        for (i, u256_chunk) in input.chunks(32).enumerate() {
223            let offset = i as u32 * 32 + initial_offset_in_words * 32;
224            heaps.write_u256(HeapId::FIRST, offset, U256::from_big_endian(u256_chunk));
225        }
226
227        let max_round_count = input.len() as u32 / 64;
228        for round_count in 0..=max_round_count {
229            let memory = PrecompileMemoryReader::new(
230                &heaps[HeapId::FIRST],
231                initial_offset_in_words,
232                round_count * 2,
233            );
234            let output = LegacyPrecompiles.call_precompile(
235                SHA256_ROUND_FUNCTION_PRECOMPILE_ADDRESS,
236                memory,
237                round_count.into(),
238            );
239
240            if round_count == 0 {
241                prop_assert_eq!(output.len, 0);
242            } else {
243                prop_assert_eq!(output.len, 1);
244                prop_assert_ne!(output.buffer[0], U256::zero());
245            }
246            prop_assert!(matches!(output.cycle_stats, Some(CycleStats::Sha256(_))));
247        }
248        Ok(())
249    }
250
251    #[derive(Debug, Clone, Copy)]
252    enum EcRecoverMutation {
253        RecoveryId,
254        Digest(usize),
255        R(usize),
256        S(usize),
257    }
258
259    impl EcRecoverMutation {
260        fn gen() -> impl Strategy<Value = Self> {
261            (0..4).prop_flat_map(|raw| match raw {
262                0 => Just(Self::RecoveryId).boxed(),
263                1 => (0_usize..32).prop_map(Self::Digest).boxed(),
264                2 => (0_usize..32).prop_map(Self::R).boxed(),
265                3 => (0_usize..32).prop_map(Self::S).boxed(),
266                _ => unreachable!(),
267            })
268        }
269    }
270
271    fn test_ecrecover_precompile(
272        signing_key: &K256SigningKey,
273        mutation: Option<EcRecoverMutation>,
274        initial_offset_in_words: u32,
275    ) -> Result<(), TestCaseError> {
276        let mut heaps = Heaps::new(&[]);
277        let initial_offset = initial_offset_in_words * 32;
278
279        let message = "test message!";
280        let mut message_digest = sha3::Keccak256::digest(message);
281
282        let (signature, recovery_id) = signing_key
283            .sign_prehash_recoverable(&message_digest)
284            .unwrap();
285        if recovery_id.is_x_reduced() {
286            return Ok(());
287        }
288        let mut recovery_id = recovery_id.to_byte();
289
290        println!(
291            "testing key {:?} with mutation {mutation:?}",
292            signing_key.verifying_key().to_encoded_point(true)
293        );
294        let mut signature_bytes = signature.to_bytes();
295
296        match mutation {
297            Some(EcRecoverMutation::Digest(byte)) => {
298                message_digest[byte] ^= 1;
299            }
300            Some(EcRecoverMutation::RecoveryId) => {
301                recovery_id = 1 - recovery_id;
302            }
303            Some(EcRecoverMutation::R(byte)) => {
304                signature_bytes[byte] ^= 1;
305            }
306            Some(EcRecoverMutation::S(byte)) => {
307                signature_bytes[byte + 32] ^= 1;
308            }
309            None => { /* Do nothing */ }
310        }
311
312        heaps.write_u256(
313            HeapId::FIRST,
314            initial_offset,
315            U256::from_big_endian(&message_digest),
316        );
317        heaps.write_u256(HeapId::FIRST, initial_offset + 32, recovery_id.into());
318        heaps.write_u256(
319            HeapId::FIRST,
320            initial_offset + 64,
321            U256::from_big_endian(&signature_bytes[..32]),
322        );
323        heaps.write_u256(
324            HeapId::FIRST,
325            initial_offset + 96,
326            U256::from_big_endian(&signature_bytes[32..]),
327        );
328
329        let memory = PrecompileMemoryReader::new(&heaps[HeapId::FIRST], initial_offset_in_words, 4);
330        let output = LegacyPrecompiles.call_precompile(
331            ECRECOVER_INNER_FUNCTION_PRECOMPILE_ADDRESS,
332            memory,
333            0,
334        );
335
336        prop_assert_eq!(output.len, 2);
337        let expected_address = key_to_address(signing_key.verifying_key());
338        let [is_success, address, _] = output.buffer;
339        if mutation.is_some() {
340            prop_assert_ne!(address, expected_address);
341        } else {
342            prop_assert_eq!(is_success, U256::one());
343            prop_assert_eq!(address, expected_address);
344        }
345        prop_assert!(matches!(output.cycle_stats, Some(CycleStats::EcRecover(1))));
346        Ok(())
347    }
348
349    #[derive(Debug, Clone, Copy)]
350    enum P256Mutation {
351        Digest(usize),
352        R(usize),
353        S(usize),
354        Key(usize),
355    }
356
357    impl P256Mutation {
358        fn gen() -> impl Strategy<Value = Self> {
359            (0..4).prop_flat_map(|raw| match raw {
360                0 => (0_usize..32).prop_map(Self::Digest).boxed(),
361                1 => (0_usize..32).prop_map(Self::R).boxed(),
362                2 => (0_usize..32).prop_map(Self::S).boxed(),
363                3 => (0_usize..64).prop_map(Self::Key).boxed(),
364                _ => unreachable!(),
365            })
366        }
367    }
368
369    fn test_secp256r1_precompile(
370        signing_key: &P256SigningKey,
371        mutation: Option<P256Mutation>,
372        initial_offset_in_words: u32,
373    ) -> Result<(), TestCaseError> {
374        use zkevm_opcode_defs::p256::ecdsa::{signature::hazmat::PrehashSigner, Signature};
375
376        let mut heaps = Heaps::new(&[]);
377        let initial_offset = initial_offset_in_words * 32;
378
379        let message = "test message!";
380        let mut message_digest = sha3::Keccak256::digest(message);
381
382        let signature: Signature = signing_key.sign_prehash(&message_digest).unwrap();
383
384        println!(
385            "testing key {:?} with mutation {mutation:?}",
386            signing_key.verifying_key().to_encoded_point(true)
387        );
388        let mut signature_bytes = signature.to_bytes();
389        let mut key_bytes = signing_key
390            .verifying_key()
391            .to_encoded_point(false)
392            .as_bytes()[1..]
393            .to_vec();
394        assert_eq!(key_bytes.len(), 64);
395
396        match mutation {
397            Some(P256Mutation::Digest(byte)) => {
398                message_digest[byte] ^= 1;
399            }
400            Some(P256Mutation::R(byte)) => {
401                signature_bytes[byte] ^= 1;
402            }
403            Some(P256Mutation::S(byte)) => {
404                signature_bytes[byte + 32] ^= 1;
405            }
406            Some(P256Mutation::Key(byte)) => {
407                key_bytes[byte] ^= 1;
408            }
409            None => { /* Do nothing */ }
410        }
411
412        heaps.write_u256(
413            HeapId::FIRST,
414            initial_offset,
415            U256::from_big_endian(&message_digest),
416        );
417        heaps.write_u256(
418            HeapId::FIRST,
419            initial_offset + 32,
420            U256::from_big_endian(&signature_bytes[..32]),
421        );
422        heaps.write_u256(
423            HeapId::FIRST,
424            initial_offset + 64,
425            U256::from_big_endian(&signature_bytes[32..]),
426        );
427        heaps.write_u256(
428            HeapId::FIRST,
429            initial_offset + 96,
430            U256::from_big_endian(&key_bytes[..32]),
431        );
432        heaps.write_u256(
433            HeapId::FIRST,
434            initial_offset + 128,
435            U256::from_big_endian(&key_bytes[32..]),
436        );
437
438        let memory = PrecompileMemoryReader::new(&heaps[HeapId::FIRST], initial_offset_in_words, 5);
439        let output =
440            LegacyPrecompiles.call_precompile(SECP256R1_VERIFY_PRECOMPILE_ADDRESS, memory, 0);
441
442        prop_assert_eq!(output.len, 2);
443        let [is_ok, is_verified, _] = output.buffer;
444        if mutation.is_none() {
445            prop_assert_eq!(is_ok, U256::one());
446            prop_assert_eq!(is_verified, U256::one());
447        } else {
448            prop_assert!(is_ok.is_zero() || is_verified.is_zero());
449        }
450        prop_assert!(matches!(
451            output.cycle_stats,
452            Some(CycleStats::Secp256r1Verify(1))
453        ));
454        Ok(())
455    }
456
457    proptest! {
458        #[test]
459        fn keccak_precompile_works(
460            bytes in arbitrary_aligned_bytes(32),
461            initial_offset in 0..u32::MAX / 2,
462        ) {
463            test_keccak_precompile(&bytes, initial_offset)?;
464        }
465
466        #[test]
467        fn sha256_precompile_works(
468            bytes in arbitrary_aligned_bytes(64),
469            initial_offset_in_words in 0..u32::MAX / 64,
470        ) {
471            test_sha256_precompile(&bytes, initial_offset_in_words)?;
472        }
473
474        #[test]
475        fn ecrecover_precompile_works(
476            signing_key in array::uniform32(num::u8::ANY)
477                .prop_filter_map("not a key", |bytes| K256SigningKey::from_bytes(&bytes.into()).ok()),
478            mutation in option::of(EcRecoverMutation::gen()),
479            initial_offset_in_words in 0..u32::MAX / 64,
480        ) {
481            test_ecrecover_precompile(&signing_key, mutation, initial_offset_in_words)?;
482        }
483
484        #[test]
485        fn secp256r1_precompile_works(
486            signing_key in array::uniform32(num::u8::ANY)
487                .prop_filter_map("not a key", |bytes| P256SigningKey::from_bytes(&bytes.into()).ok()),
488            mutation in option::of(P256Mutation::gen()),
489            initial_offset_in_words in 0..u32::MAX / 64,
490        ) {
491            test_secp256r1_precompile(&signing_key, mutation, initial_offset_in_words)?;
492        }
493    }
494}