1#![feature(
4 bigint_helper_methods,
5 const_option_ops,
6 const_trait_impl,
7 maybe_uninit_as_bytes,
8 maybe_uninit_fill,
9 trusted_len,
10 const_try,
11 const_try_residual,
12 try_blocks
13)]
14#![expect(incomplete_features, reason = "generic_const_exprs")]
15#![feature(generic_const_exprs)]
18#![no_std]
19
20pub mod contract_instruction_prototype;
21
22use crate::contract_instruction_prototype::{
23 ContractInstructionPrototype, NotPopularInstruction, PopularInstruction,
24};
25use ab_contracts_common::metadata::decode::{
26 MetadataDecoder, MetadataDecodingError, MetadataItem, MethodMetadataItem,
27 MethodsMetadataDecoder,
28};
29use ab_io_type::trivial_type::TrivialType;
30use ab_riscv_primitives::instruction::Instruction;
31use ab_riscv_primitives::registers::EReg;
32use core::iter;
33use core::iter::TrustedLen;
34use core::mem::MaybeUninit;
35use replace_with::replace_with_or_abort;
36use tracing::{debug, trace};
37
38pub const CONTRACT_FILE_MAGIC: [u8; 4] = *b"ABC0";
40pub type ContractRegister = EReg<u64>;
42pub type ContractInstruction = ContractInstructionPrototype<ContractRegister>;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, TrivialType)]
47#[repr(C)]
48pub struct ContractFileHeader {
49 pub magic: [u8; 4],
51 pub read_only_section_file_size: u32,
53 pub read_only_section_memory_size: u32,
57 pub metadata_offset: u32,
59 pub metadata_size: u16,
61 pub num_methods: u16,
63 pub host_call_fn_offset: u32,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, TrivialType)]
71#[repr(C)]
72pub struct ContractFileMethodMetadata {
73 pub offset: u32,
75 pub size: u32,
77}
78
79#[derive(Debug, Copy, Clone)]
80pub struct ContractFileMethod<'a> {
81 pub address: u32,
83 pub method_metadata_item: MethodMetadataItem<'a>,
85 pub method_metadata_bytes: &'a [u8],
91}
92
93#[derive(Debug, thiserror::Error)]
95pub enum ContractFileParseError {
96 #[error("The file is too large, must fit into `u32`: {file_size} bytes")]
98 FileTooLarge {
99 file_size: usize,
101 },
102 #[error("The file does not have a header (not enough bytes)")]
104 NoHeader,
105 #[error("The magic bytes in the header are incorrect")]
107 WrongMagicBytes,
108 #[error(
110 "The metadata section is out of bounds of the file: offset {offset}, size {size}, file \
111 size {file_size}"
112 )]
113 MetadataOutOfRange {
114 offset: u32,
116 size: u16,
118 file_size: u32,
120 },
121 #[error("Failed to decode metadata item")]
123 MetadataDecoding,
124 #[error(
126 "The file is too small: num_methods {num_methods}, read_only_section_size \
127 {read_only_section_size}, file_size {file_size}"
128 )]
129 FileTooSmall {
130 num_methods: u16,
132 read_only_section_size: u32,
134 file_size: u32,
136 },
137 #[error("Method is unaligned: file offset {file_offset}, memory address {memory_address}")]
139 MethodUnaligned {
140 file_offset: u32,
142 memory_address: u32,
144 },
145 #[error(
147 "Method offset is out of bounds of the file: offset {offset}, code section \
148 offset {code_section_offset} file_size {file_size}"
149 )]
150 MethodOutOfRange {
151 offset: u32,
153 code_section_offset: u32,
155 file_size: u32,
157 },
158 #[error(
160 "Host call function is unaligned: file offset {file_offset}, memory address \
161 {memory_address}"
162 )]
163 HostCallFnUnaligned {
164 file_offset: u32,
166 memory_address: u32,
168 },
169 #[error(
171 "The host call function offset is out of bounds of the file: offset {offset}, code section \
172 offset {code_section_offset} file_size {file_size}"
173 )]
174 HostCallFnOutOfRange {
175 offset: u32,
177 code_section_offset: u32,
179 file_size: u32,
181 },
182 #[error("The host call function doesn't have auipc + jalr tailcall pattern: {first} {second}")]
184 InvalidHostCallFnPattern {
185 first: ContractInstruction,
187 second: ContractInstruction,
189 },
190 #[error(
192 "The read-only section file size is larger than the memory size: file_size {file_size}, \
193 memory_size {memory_size}"
194 )]
195 InvalidReadOnlySizes {
196 file_size: u32,
198 memory_size: u32,
200 },
201 #[error(
204 "There are not enough methods in the header to match the number of methods in the actual \
205 metadata: header_num_methods {header_num_methods}, metadata_method_index \
206 {metadata_method_index}"
207 )]
208 InsufficientHeaderMethods {
209 header_num_methods: u16,
211 metadata_method_index: u16,
213 },
214 #[error(
217 "The number of methods in the header {header_num_methods} does not match the number of \
218 methods in the actual metadata {metadata_num_methods}"
219 )]
220 MetadataNumMethodsMismatch {
221 header_num_methods: u16,
223 metadata_num_methods: u16,
225 },
226 #[error("Invalid instruction encountered while parsing the code section: {instruction}")]
228 InvalidInstruction {
229 instruction: u32,
231 },
232 #[error("The code section is empty")]
234 CodeEmpty,
235 #[error(
237 "Unexpected trailing code bytes encountered while parsing the code section: {num_bytes} \
238 trailing bytes"
239 )]
240 UnexpectedTrailingCodeBytes {
241 num_bytes: usize,
243 },
244 #[error("The last instruction in the code section must be a jump instruction: {instruction}")]
246 LastInstructionMustBeJump {
247 instruction: ContractInstruction,
249 },
250}
251
252impl From<MetadataDecodingError<'_>> for ContractFileParseError {
253 fn from(error: MetadataDecodingError<'_>) -> Self {
254 debug!(?error, "Failed to decode metadata item");
255 Self::MetadataDecoding
256 }
257}
258
259#[derive(Debug)]
262pub struct ContractFile<'a> {
263 read_only_section_file_size: u32,
264 read_only_section_memory_size: u32,
265 num_methods: u16,
266 bytes: &'a [u8],
267}
268
269impl<'a> ContractFile<'a> {
270 pub fn parse<CM>(
279 file_bytes: &'a [u8],
280 mut contract_method: CM,
281 ) -> Result<Self, ContractFileParseError>
282 where
283 CM: FnMut(ContractFileMethod<'a>) -> Result<(), ContractFileParseError>,
284 {
285 let file_size = u32::try_from(file_bytes.len()).map_err(|_error| {
286 ContractFileParseError::FileTooLarge {
287 file_size: file_bytes.len(),
288 }
289 })?;
290 let (header_bytes, after_header_bytes) = file_bytes
291 .split_at_checked(size_of::<ContractFileHeader>())
292 .ok_or(ContractFileParseError::NoHeader)?;
293 let header = unsafe { ContractFileHeader::read_unaligned_unchecked(header_bytes) };
295
296 if header.magic != CONTRACT_FILE_MAGIC {
297 return Err(ContractFileParseError::WrongMagicBytes);
298 }
299
300 if header.read_only_section_file_size > header.read_only_section_memory_size {
301 return Err(ContractFileParseError::InvalidReadOnlySizes {
302 file_size: header.read_only_section_file_size,
303 memory_size: header.read_only_section_memory_size,
304 });
305 }
306
307 let metadata_bytes = file_bytes
308 .get(header.metadata_offset as usize..)
309 .ok_or(ContractFileParseError::MetadataOutOfRange {
310 offset: header.metadata_offset,
311 size: header.metadata_size,
312 file_size,
313 })?
314 .get(..header.metadata_size as usize)
315 .ok_or(ContractFileParseError::MetadataOutOfRange {
316 offset: header.metadata_offset,
317 size: header.metadata_size,
318 file_size,
319 })?;
320
321 let read_only_padding_size =
322 header.read_only_section_memory_size - header.read_only_section_file_size;
323 let read_only_section_offset = ContractFileHeader::SIZE
324 + u32::from(header.num_methods) * ContractFileMethodMetadata::SIZE;
325 let code_section_offset =
326 read_only_section_offset.saturating_add(header.read_only_section_file_size);
327
328 {
329 let mut contract_file_methods_metadata_iter = {
330 let mut file_contract_metadata_bytes = after_header_bytes;
331
332 (0..header.num_methods).map(move |_| {
333 let contract_file_method_metadata_bytes = file_contract_metadata_bytes
334 .split_off(..size_of::<ContractFileMethodMetadata>())
335 .ok_or(ContractFileParseError::FileTooSmall {
336 num_methods: header.num_methods,
337 read_only_section_size: header.read_only_section_file_size,
338 file_size,
339 })?;
340 let contract_file_method_metadata = unsafe {
342 ContractFileMethodMetadata::read_unaligned_unchecked(
343 contract_file_method_metadata_bytes,
344 )
345 };
346
347 if (contract_file_method_metadata.offset + contract_file_method_metadata.size)
348 > file_size
349 {
350 return Err(ContractFileParseError::FileTooSmall {
351 num_methods: header.num_methods,
352 read_only_section_size: header.read_only_section_file_size,
353 file_size,
354 });
355 }
356
357 if contract_file_method_metadata.offset < code_section_offset {
358 return Err(ContractFileParseError::MethodOutOfRange {
359 offset: contract_file_method_metadata.offset,
360 code_section_offset,
361 file_size,
362 });
363 }
364
365 Ok(contract_file_method_metadata)
366 })
367 };
368
369 let mut metadata_num_methods = 0;
370 let mut remaining_metadata_bytes = metadata_bytes;
371 let mut metadata_decoder = MetadataDecoder::new(metadata_bytes);
372
373 while let Some(maybe_metadata_item) = metadata_decoder.decode_next() {
374 let metadata_item = maybe_metadata_item?;
375 trace!(?metadata_item, "Decoded metadata item");
376
377 let mut methods_metadata_decoder = metadata_item.into_decoder();
378 loop {
379 let Some(method_metadata_decoder) = methods_metadata_decoder.decode_next()
383 else {
384 break;
385 };
386
387 let before_remaining_bytes = method_metadata_decoder.remaining_metadata_bytes();
388 let (_, method_metadata_item) = method_metadata_decoder.decode_next()?;
389
390 trace!(?method_metadata_item, "Decoded method metadata item");
391 metadata_num_methods += 1;
392
393 let method_metadata_bytes = remaining_metadata_bytes
394 .split_off(
395 ..before_remaining_bytes
396 - methods_metadata_decoder.remaining_metadata_bytes(),
397 )
398 .ok_or(MetadataDecodingError::NotEnoughMetadata)?;
399
400 let contract_file_method_metadata = contract_file_methods_metadata_iter
401 .next()
402 .ok_or(ContractFileParseError::InsufficientHeaderMethods {
403 header_num_methods: header.num_methods,
404 metadata_method_index: metadata_num_methods - 1,
405 })??;
406 let address = contract_file_method_metadata.offset - read_only_section_offset
407 + read_only_padding_size;
408
409 if !address.is_multiple_of(size_of::<u32>() as u32) {
410 return Err(ContractFileParseError::MethodUnaligned {
411 file_offset: contract_file_method_metadata.offset,
412 memory_address: address,
413 });
414 }
415
416 contract_method(ContractFileMethod {
417 address,
418 method_metadata_item,
419 method_metadata_bytes,
420 })?;
421 }
422 }
423
424 if metadata_num_methods != header.num_methods {
425 return Err(ContractFileParseError::MetadataNumMethodsMismatch {
426 header_num_methods: header.num_methods,
427 metadata_num_methods,
428 });
429 }
430 }
431
432 if code_section_offset >= file_size {
433 return Err(ContractFileParseError::FileTooSmall {
434 num_methods: header.num_methods,
435 read_only_section_size: header.read_only_section_file_size,
436 file_size,
437 });
438 }
439
440 if header.host_call_fn_offset != 0 {
441 if header.host_call_fn_offset >= file_size
442 || header.host_call_fn_offset < code_section_offset
443 {
444 return Err(ContractFileParseError::HostCallFnOutOfRange {
445 offset: header.host_call_fn_offset,
446 code_section_offset,
447 file_size,
448 });
449 }
450
451 let instructions_bytes = file_bytes
452 .get(header.host_call_fn_offset as usize..)
453 .ok_or(ContractFileParseError::HostCallFnOutOfRange {
454 offset: header.host_call_fn_offset,
455 code_section_offset,
456 file_size,
457 })?
458 .get(..size_of::<[u32; 2]>())
459 .ok_or(ContractFileParseError::HostCallFnOutOfRange {
460 offset: header.host_call_fn_offset,
461 code_section_offset,
462 file_size,
463 })?;
464
465 let first_instruction = u32::from_le_bytes([
466 instructions_bytes[0],
467 instructions_bytes[1],
468 instructions_bytes[2],
469 instructions_bytes[3],
470 ]);
471 let second_instruction = u32::from_le_bytes([
472 instructions_bytes[4],
473 instructions_bytes[5],
474 instructions_bytes[6],
475 instructions_bytes[7],
476 ]);
477
478 let first = ContractInstruction::try_decode(first_instruction).ok_or(
479 ContractFileParseError::InvalidInstruction {
480 instruction: first_instruction,
481 },
482 )?;
483 let second = ContractInstruction::try_decode(second_instruction).ok_or(
484 ContractFileParseError::InvalidInstruction {
485 instruction: second_instruction,
486 },
487 )?;
488
489 let matches_expected_pattern = if let (
495 ContractInstruction::Popular(PopularInstruction::Auipc {
496 rd: auipc_rd,
497 imm: _,
498 }),
499 ContractInstruction::Popular(PopularInstruction::Jalr {
500 rd: jalr_rd,
501 rs1: jalr_rs1,
502 imm: _,
503 }),
504 ) = (first, second)
505 {
506 auipc_rd == jalr_rs1 && jalr_rd == ContractRegister::Zero
507 } else {
508 false
509 };
510
511 if !matches_expected_pattern {
512 return Err(ContractFileParseError::InvalidHostCallFnPattern { first, second });
513 }
514
515 let address =
516 header.host_call_fn_offset - read_only_section_offset + read_only_padding_size;
517
518 if !address.is_multiple_of(size_of::<u32>() as u32) {
519 return Err(ContractFileParseError::HostCallFnUnaligned {
520 file_offset: header.host_call_fn_offset,
521 memory_address: address,
522 });
523 }
524 }
525
526 {
528 let mut remaining_code_file_bytes = &file_bytes[code_section_offset as usize..];
529
530 let mut instruction = ContractInstruction::NotPopular(NotPopularInstruction::Unimp);
531 while let Some(instruction_bytes) =
532 remaining_code_file_bytes.split_off(..size_of::<u32>())
533 {
534 let instruction_word = u32::from_le_bytes([
535 instruction_bytes[0],
536 instruction_bytes[1],
537 instruction_bytes[2],
538 instruction_bytes[3],
539 ]);
540 instruction = ContractInstruction::try_decode(instruction_word).ok_or(
541 ContractFileParseError::InvalidInstruction {
542 instruction: instruction_word,
543 },
544 )?;
545 }
546
547 let is_jump_instruction = matches!(
548 instruction,
549 ContractInstruction::Popular(PopularInstruction::Jalr { .. })
550 | ContractInstruction::NotPopular(
551 NotPopularInstruction::Beq { .. }
552 | NotPopularInstruction::Bne { .. }
553 | NotPopularInstruction::Blt { .. }
554 | NotPopularInstruction::Bge { .. }
555 | NotPopularInstruction::Bltu { .. }
556 | NotPopularInstruction::Bgeu { .. }
557 | NotPopularInstruction::Jal { .. }
558 )
559 );
560 if !is_jump_instruction {
561 return Err(ContractFileParseError::LastInstructionMustBeJump { instruction });
562 }
563
564 if !remaining_code_file_bytes.is_empty() {
565 return Err(ContractFileParseError::UnexpectedTrailingCodeBytes {
566 num_bytes: remaining_code_file_bytes.len(),
567 });
568 }
569 }
570
571 Ok(Self {
572 read_only_section_file_size: header.read_only_section_file_size,
573 read_only_section_memory_size: header.read_only_section_memory_size,
574 num_methods: header.num_methods,
575 bytes: file_bytes,
576 })
577 }
578
579 pub unsafe fn parse_unchecked(file_bytes: &'a [u8]) -> Self {
587 let header = unsafe { ContractFileHeader::read_unaligned_unchecked(file_bytes) };
589
590 Self {
591 read_only_section_file_size: header.read_only_section_file_size,
592 read_only_section_memory_size: header.read_only_section_memory_size,
593 num_methods: header.num_methods,
594 bytes: file_bytes,
595 }
596 }
597
598 #[inline(always)]
600 pub fn header(&self) -> ContractFileHeader {
601 unsafe { ContractFileHeader::read_unaligned_unchecked(self.bytes) }
603 }
604
605 #[inline]
607 pub fn metadata_bytes(&self) -> &[u8] {
608 let header = self.header();
609 unsafe {
611 self.bytes
612 .get_unchecked(header.metadata_offset as usize..)
613 .get_unchecked(..header.metadata_size as usize)
614 }
615 }
616
617 #[inline]
619 pub fn contract_memory_size(&self) -> u32 {
620 let read_only_section_offset = ContractFileHeader::SIZE
621 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
622 let read_only_padding_size =
623 self.read_only_section_memory_size - self.read_only_section_file_size;
624 self.bytes.len() as u32 - read_only_section_offset + read_only_padding_size
625 }
626
627 #[must_use = "Must check that contract memory was large enough"]
631 pub fn initialize_contract_memory(&self, mut contract_memory: &mut [MaybeUninit<u8>]) -> bool {
632 let contract_memory_input_size = contract_memory.len();
633 let read_only_section_offset = ContractFileHeader::SIZE
634 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
635 let read_only_padding_size =
636 self.read_only_section_memory_size - self.read_only_section_file_size;
637
638 let source_bytes = unsafe {
640 self.bytes
641 .get_unchecked(read_only_section_offset as usize..)
642 };
643
644 if contract_memory.len() == source_bytes.len() {
646 contract_memory.write_copy_of_slice(source_bytes);
647 return true;
648 }
649
650 let Some(read_only_file_target_bytes) =
651 contract_memory.split_off_mut(..self.read_only_section_file_size as usize)
652 else {
653 trace!(
654 %contract_memory_input_size,
655 contract_memory_size = %self.contract_memory_size(),
656 read_only_section_file_size = self.read_only_section_file_size,
657 "Not enough bytes to write read-only section from the file"
658 );
659
660 return false;
661 };
662
663 let (read_only_file_source_bytes, code_source_bytes) =
665 unsafe { source_bytes.split_at_unchecked(self.read_only_section_file_size as usize) };
666 read_only_file_target_bytes.write_copy_of_slice(read_only_file_source_bytes);
668
669 let Some(read_only_padding_bytes) =
670 contract_memory.split_off_mut(..read_only_padding_size as usize)
671 else {
672 trace!(
673 %contract_memory_input_size,
674 contract_memory_size = %self.contract_memory_size(),
675 read_only_section_file_size = self.read_only_section_file_size,
676 read_only_section_memory_size = self.read_only_section_memory_size,
677 %read_only_padding_size,
678 "Not enough bytes to write read-only padding section"
679 );
680
681 return false;
682 };
683
684 read_only_padding_bytes.write_filled(0);
686
687 if code_source_bytes.len() != contract_memory.len() {
688 trace!(
689 %contract_memory_input_size,
690 contract_memory_size = %self.contract_memory_size(),
691 read_only_section_file_size = self.read_only_section_file_size,
692 read_only_section_memory_size = self.read_only_section_memory_size,
693 %read_only_padding_size,
694 code_size = %code_source_bytes.len(),
695 "Not enough bytes to write code section from the file"
696 );
697
698 return false;
699 }
700
701 contract_memory.write_copy_of_slice(code_source_bytes);
702
703 true
704 }
705
706 pub fn get_code(&self) -> &[u8] {
708 let read_only_section_offset = ContractFileHeader::SIZE
709 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
710
711 let source_bytes = unsafe {
713 self.bytes
714 .get_unchecked(read_only_section_offset as usize..)
715 };
716
717 let (_read_only_file_source_bytes, code_source_bytes) =
719 unsafe { source_bytes.split_at_unchecked(self.read_only_section_file_size as usize) };
720
721 code_source_bytes
722 }
723
724 pub fn iterate_methods(
726 &self,
727 ) -> impl ExactSizeIterator<Item = ContractFileMethod<'_>> + TrustedLen {
728 let metadata_bytes = self.metadata_bytes();
729
730 #[ouroboros::self_referencing]
731 struct MethodsMetadataIterState<'metadata> {
732 metadata_decoder: MetadataDecoder<'metadata>,
733 #[borrows(mut metadata_decoder)]
734 #[covariant]
735 methods_metadata_decoder: Option<MethodsMetadataDecoder<'this, 'metadata>>,
736 }
737
738 let metadata_decoder = MetadataDecoder::new(metadata_bytes);
739
740 let mut methods_metadata_state =
741 MethodsMetadataIterState::new(metadata_decoder, |metadata_decoder| {
742 metadata_decoder
743 .decode_next()
744 .and_then(Result::ok)
745 .map(MetadataItem::into_decoder)
746 });
747
748 let mut metadata_methods_iter = iter::from_fn(move || {
749 loop {
750 let maybe_next_item = methods_metadata_state.with_methods_metadata_decoder_mut(
751 |maybe_methods_metadata_decoder| {
752 let methods_metadata_decoder = maybe_methods_metadata_decoder.as_mut()?;
753 let method_metadata_decoder = methods_metadata_decoder.decode_next()?;
754
755 let before_remaining_bytes =
756 method_metadata_decoder.remaining_metadata_bytes();
757
758 let (_, method_metadata_item) = method_metadata_decoder
759 .decode_next()
760 .expect("Input is valid according to function contract; qed");
761
762 let method_metadata_bytes = unsafe {
764 metadata_bytes
765 .get_unchecked(metadata_bytes.len() - before_remaining_bytes..)
766 .get_unchecked(
767 ..before_remaining_bytes
768 - methods_metadata_decoder.remaining_metadata_bytes(),
769 )
770 };
771
772 Some((method_metadata_item, method_metadata_bytes))
773 },
774 );
775
776 if let Some(next_item) = maybe_next_item {
777 return Some(next_item);
778 }
779
780 replace_with_or_abort(&mut methods_metadata_state, |methods_metadata_state| {
782 let metadata_decoder = methods_metadata_state.into_heads().metadata_decoder;
783 MethodsMetadataIterState::new(metadata_decoder, |metadata_decoder| {
784 metadata_decoder
785 .decode_next()
786 .and_then(Result::ok)
787 .map(MetadataItem::into_decoder)
788 })
789 });
790
791 if methods_metadata_state
792 .borrow_methods_metadata_decoder()
793 .is_none()
794 {
795 return None;
796 }
797 }
798 });
799
800 let read_only_padding_size =
801 self.read_only_section_memory_size - self.read_only_section_file_size;
802 let contract_file_methods_metadata_bytes =
804 unsafe { self.bytes.get_unchecked(size_of::<ContractFileHeader>()..) };
805
806 (0..self.num_methods).map(move |method_index| {
807 let contract_file_method_metadata_bytes = unsafe {
809 contract_file_methods_metadata_bytes
810 .get_unchecked(
811 method_index as usize * size_of::<ContractFileMethodMetadata>()..,
812 )
813 .get_unchecked(..size_of::<ContractFileMethodMetadata>())
814 };
815 let contract_file_method_metadata = unsafe {
817 ContractFileMethodMetadata::read_unaligned_unchecked(
818 contract_file_method_metadata_bytes,
819 )
820 };
821
822 let (method_metadata_item, method_metadata_bytes) = metadata_methods_iter
823 .next()
824 .expect("Protected internal invariant checked in constructor; qed");
825
826 ContractFileMethod {
827 address: contract_file_method_metadata.offset + read_only_padding_size,
828 method_metadata_item,
829 method_metadata_bytes,
830 }
831 })
832 }
833}