1#![expect(incomplete_features, reason = "explicit_tail_calls")]
31#![feature(
32 const_block_items,
33 const_cmp,
34 const_convert,
35 const_default,
36 const_index,
37 const_trait_impl,
38 const_try,
39 const_try_residual,
40 core_io,
41 core_io_borrowed_buf,
42 derive_const,
43 explicit_tail_calls,
44 fn_align,
45 maybe_uninit_fill,
46 signed_bigint_helpers,
47 trusted_len,
48 try_blocks
49)]
50#![no_std]
51
52pub mod instruction;
53
54use crate::instruction::ContractInstruction;
55use ab_contracts_common::metadata::decode::{
56 MetadataDecoder, MetadataDecodingError, MetadataItem, MethodMetadataItem,
57 MethodsMetadataDecoder,
58};
59use ab_io_type::trivial_type::TrivialType;
60use ab_io_type::unaligned::Unaligned;
61use ab_riscv_primitives::prelude::*;
62use core::io::BorrowedCursor;
63use core::iter;
64use core::iter::TrustedLen;
65use replace_with::replace_with_or_abort;
66use tracing::{debug, trace};
67
68pub const CONTRACT_FILE_MAGIC: [u8; 4] = *b"ABC0";
70
71const {
73 assert!(size_of::<ContractInstruction>() == 8);
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, TrivialType)]
78#[repr(C)]
79pub struct ContractFileHeader {
80 pub magic: [u8; 4],
82 pub read_only_section_file_size: u32,
84 pub read_only_section_memory_size: u32,
88 pub metadata_offset: u32,
90 pub metadata_size: u16,
92 pub num_methods: u16,
94 pub host_call_fn_offset: u32,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, TrivialType)]
102#[repr(C)]
103pub struct ContractFileMethodMetadata {
104 pub offset: u32,
106 pub size: u32,
108}
109
110#[derive(Debug, Copy, Clone)]
111pub struct ContractFileMethod<'a> {
112 pub address: u32,
114 pub size: u32,
116 pub method_metadata_item: MethodMetadataItem<'a>,
118 pub method_metadata_bytes: &'a [u8],
124}
125
126#[derive(Debug, thiserror::Error)]
128pub enum ContractFileParseError {
129 #[error("The file is too large, must fit into `u32`: {file_size} bytes")]
131 FileTooLarge {
132 file_size: usize,
134 },
135 #[error("The file does not have a header (not enough bytes)")]
137 NoHeader,
138 #[error("The magic bytes in the header are incorrect")]
140 WrongMagicBytes,
141 #[error(
143 "The metadata section is out of bounds of the file: offset {offset}, size {size}, file \
144 size {file_size}"
145 )]
146 MetadataOutOfRange {
147 offset: u32,
149 size: u16,
151 file_size: u32,
153 },
154 #[error("Failed to decode metadata item")]
156 MetadataDecoding,
157 #[error(
159 "The file is too small: num_methods {num_methods}, read_only_section_size \
160 {read_only_section_size}, file_size {file_size}"
161 )]
162 FileTooSmall {
163 num_methods: u16,
165 read_only_section_size: u32,
167 file_size: u32,
169 },
170 #[error("Method is unaligned: file offset {file_offset}, memory address {memory_address}")]
172 MethodUnaligned {
173 file_offset: u32,
175 memory_address: u32,
177 },
178 #[error(
180 "Method offset is out of bounds of the file: offset {offset}, code section \
181 offset {code_section_offset} file_size {file_size}"
182 )]
183 MethodOutOfRange {
184 offset: u32,
186 code_section_offset: u32,
188 file_size: u32,
190 },
191 #[error(
193 "Host call function is unaligned: file offset {file_offset}, memory address \
194 {memory_address}"
195 )]
196 HostCallFnUnaligned {
197 file_offset: u32,
199 memory_address: u32,
201 },
202 #[error(
204 "The host call function offset is out of bounds of the file: offset {offset}, code section \
205 offset {code_section_offset} file_size {file_size}"
206 )]
207 HostCallFnOutOfRange {
208 offset: u32,
210 code_section_offset: u32,
212 file_size: u32,
214 },
215 #[error("The host call function doesn't have jal tailcall instruction: {instruction}")]
217 InvalidHostCallFnPattern {
218 instruction: ContractInstruction,
220 },
221 #[error(
223 "The read-only section file size is larger than the memory size: file_size {file_size}, \
224 memory_size {memory_size}"
225 )]
226 InvalidReadOnlySizes {
227 file_size: u32,
229 memory_size: u32,
231 },
232 #[error(
234 "Contract memory size doesn't fit into `u32`: read_only_section_memory_size \
235 {read_only_section_memory_size}, code_size {code_size}"
236 )]
237 ContractMemoryTooLarge {
238 read_only_section_memory_size: u32,
240 code_size: u32,
242 },
243 #[error(
246 "There are not enough methods in the header to match the number of methods in the actual \
247 metadata: header_num_methods {header_num_methods}, metadata_method_index \
248 {metadata_method_index}"
249 )]
250 InsufficientHeaderMethods {
251 header_num_methods: u16,
253 metadata_method_index: u16,
255 },
256 #[error(
259 "The number of methods in the header {header_num_methods} does not match the number of \
260 methods in the actual metadata {metadata_num_methods}"
261 )]
262 MetadataNumMethodsMismatch {
263 header_num_methods: u16,
265 metadata_num_methods: u16,
267 },
268 #[error("Invalid instruction encountered while parsing the code section: {instruction:#x}")]
270 InvalidInstruction {
271 instruction: u32,
273 },
274 #[error("The code section is empty")]
276 CodeEmpty,
277 #[error(
279 "Unexpected trailing code bytes encountered while parsing the code section: {num_bytes} \
280 trailing bytes"
281 )]
282 UnexpectedTrailingCodeBytes {
283 num_bytes: usize,
285 },
286}
287
288impl From<MetadataDecodingError<'_>> for ContractFileParseError {
289 fn from(error: MetadataDecodingError<'_>) -> Self {
290 debug!(?error, "Failed to decode metadata item");
291 Self::MetadataDecoding
292 }
293}
294
295#[derive(Debug)]
297pub struct ContractFile<'a> {
298 read_only_section_file_size: u32,
299 read_only_section_memory_size: u32,
300 num_methods: u16,
301 bytes: &'a [u8],
302}
303
304impl<'a> ContractFile<'a> {
305 pub fn parse<CM>(
315 file_bytes: &'a [u8],
316 mut contract_method: CM,
317 ) -> Result<Self, ContractFileParseError>
318 where
319 CM: FnMut(ContractFileMethod<'a>) -> Result<(), ContractFileParseError>,
320 {
321 let file_size = u32::try_from(file_bytes.len()).map_err(|_error| {
322 ContractFileParseError::FileTooLarge {
323 file_size: file_bytes.len(),
324 }
325 })?;
326 let (header, after_header_bytes) = Unaligned::<ContractFileHeader>::from_bytes(file_bytes)
327 .ok_or(ContractFileParseError::NoHeader)?;
328 let header = header.as_inner();
329
330 if header.magic != CONTRACT_FILE_MAGIC {
331 return Err(ContractFileParseError::WrongMagicBytes);
332 }
333
334 if header.read_only_section_file_size > header.read_only_section_memory_size {
335 return Err(ContractFileParseError::InvalidReadOnlySizes {
336 file_size: header.read_only_section_file_size,
337 memory_size: header.read_only_section_memory_size,
338 });
339 }
340
341 let metadata_bytes = file_bytes
342 .get(header.metadata_offset as usize..)
343 .ok_or(ContractFileParseError::MetadataOutOfRange {
344 offset: header.metadata_offset,
345 size: header.metadata_size,
346 file_size,
347 })?
348 .get(..header.metadata_size as usize)
349 .ok_or(ContractFileParseError::MetadataOutOfRange {
350 offset: header.metadata_offset,
351 size: header.metadata_size,
352 file_size,
353 })?;
354
355 let read_only_padding_size =
356 header.read_only_section_memory_size - header.read_only_section_file_size;
357 let read_only_section_offset = ContractFileHeader::SIZE
358 + u32::from(header.num_methods) * ContractFileMethodMetadata::SIZE;
359 let code_section_offset =
360 read_only_section_offset.saturating_add(header.read_only_section_file_size);
361
362 let Some(code_size) = file_size
363 .checked_sub(code_section_offset)
364 .filter(|&code_size| code_size > 0)
365 else {
366 return Err(ContractFileParseError::FileTooSmall {
367 num_methods: header.num_methods,
368 read_only_section_size: header.read_only_section_file_size,
369 file_size,
370 });
371 };
372
373 if header
375 .read_only_section_memory_size
376 .checked_add(code_size)
377 .is_none()
378 {
379 return Err(ContractFileParseError::ContractMemoryTooLarge {
380 read_only_section_memory_size: header.read_only_section_memory_size,
381 code_size,
382 });
383 }
384
385 {
386 let (contract_file_methods_metadata, _remainder) =
387 Unaligned::<ContractFileMethodMetadata>::slice_from_bytes(
388 after_header_bytes,
389 usize::from(header.num_methods),
390 )
391 .expect(
392 "`code_size` is not zero and `code_section_offset` is at least \
393 `read_only_section_offset`, the size of the header and metadata of all \
394 methods, so the file is larger than that; qed",
395 );
396 let mut contract_file_methods_metadata_iter = contract_file_methods_metadata
397 .iter()
398 .map(|contract_file_method_metadata| {
399 let contract_file_method_metadata = contract_file_method_metadata.as_inner();
400
401 if contract_file_method_metadata
402 .offset
403 .checked_add(contract_file_method_metadata.size)
404 .is_none_or(|method_end| method_end > file_size)
405 {
406 return Err(ContractFileParseError::FileTooSmall {
407 num_methods: header.num_methods,
408 read_only_section_size: header.read_only_section_file_size,
409 file_size,
410 });
411 }
412
413 if contract_file_method_metadata.offset < code_section_offset
416 || contract_file_method_metadata.offset >= file_size
417 {
418 return Err(ContractFileParseError::MethodOutOfRange {
419 offset: contract_file_method_metadata.offset,
420 code_section_offset,
421 file_size,
422 });
423 }
424
425 Ok(contract_file_method_metadata)
426 });
427
428 let mut metadata_num_methods = 0;
429 let mut remaining_metadata_bytes = metadata_bytes;
430 let mut metadata_decoder = MetadataDecoder::new(metadata_bytes);
431
432 while let Some(maybe_metadata_item) = metadata_decoder.decode_next() {
433 let metadata_item = maybe_metadata_item?;
434 trace!(?metadata_item, "Decoded metadata item");
435
436 let mut methods_metadata_decoder = metadata_item.into_decoder();
437 loop {
438 let Some(method_metadata_decoder) = methods_metadata_decoder.decode_next()
442 else {
443 break;
444 };
445
446 let before_remaining_bytes = method_metadata_decoder.remaining_metadata_bytes();
447 let (_, method_metadata_item) = method_metadata_decoder.decode_next()?;
448
449 trace!(?method_metadata_item, "Decoded method metadata item");
450 metadata_num_methods += 1;
451
452 let method_metadata_bytes = remaining_metadata_bytes
453 .split_off(
454 ..before_remaining_bytes
455 - methods_metadata_decoder.remaining_metadata_bytes(),
456 )
457 .ok_or(MetadataDecodingError::NotEnoughMetadata)?;
458
459 let contract_file_method_metadata = contract_file_methods_metadata_iter
460 .next()
461 .ok_or(ContractFileParseError::InsufficientHeaderMethods {
462 header_num_methods: header.num_methods,
463 metadata_method_index: metadata_num_methods - 1,
464 })??;
465 let address = contract_file_method_metadata.offset - read_only_section_offset
466 + read_only_padding_size;
467
468 if !address.is_multiple_of(u32::from(<ContractInstruction>::ALIGNMENT)) {
469 return Err(ContractFileParseError::MethodUnaligned {
470 file_offset: contract_file_method_metadata.offset,
471 memory_address: address,
472 });
473 }
474
475 contract_method(ContractFileMethod {
476 address,
477 size: contract_file_method_metadata.size,
478 method_metadata_item,
479 method_metadata_bytes,
480 })?;
481 }
482 }
483
484 if metadata_num_methods != header.num_methods {
485 return Err(ContractFileParseError::MetadataNumMethodsMismatch {
486 header_num_methods: header.num_methods,
487 metadata_num_methods,
488 });
489 }
490 }
491
492 if header.host_call_fn_offset != 0 {
493 if header.host_call_fn_offset >= file_size
494 || header.host_call_fn_offset < code_section_offset
495 {
496 return Err(ContractFileParseError::HostCallFnOutOfRange {
497 offset: header.host_call_fn_offset,
498 code_section_offset,
499 file_size,
500 });
501 }
502
503 let instruction_bytes = file_bytes
504 .get(header.host_call_fn_offset as usize..)
505 .ok_or(ContractFileParseError::HostCallFnOutOfRange {
506 offset: header.host_call_fn_offset,
507 code_section_offset,
508 file_size,
509 })?;
510 let instruction = Unaligned::<u32>::from_bytes(instruction_bytes)
511 .map(|(instruction, _remainder)| instruction.as_inner())
512 .or_else(|| {
513 Unaligned::<u16>::from_bytes(instruction_bytes)
514 .map(|(instruction, _remainder)| u32::from(instruction.as_inner()))
515 })
516 .ok_or(ContractFileParseError::HostCallFnOutOfRange {
517 offset: header.host_call_fn_offset,
518 code_section_offset,
519 file_size,
520 })?;
521
522 let instruction = ContractInstruction::try_decode(instruction)
523 .ok_or(ContractFileParseError::InvalidInstruction { instruction })?;
524
525 if usize::from(instruction.size()) > instruction_bytes.len() {
528 return Err(ContractFileParseError::HostCallFnOutOfRange {
529 offset: header.host_call_fn_offset,
530 code_section_offset,
531 file_size,
532 });
533 }
534
535 #[expect(
538 clippy::rest_pattern_accessible_field,
539 reason = "Do not need other fields"
540 )]
541 let matches_expected_pattern = match instruction {
542 ContractInstruction::Jal { rd, .. } => rd == Register::ZERO,
543 ContractInstruction::CJ { .. } => true,
544 _ => false,
545 };
546
547 if !matches_expected_pattern {
548 return Err(ContractFileParseError::InvalidHostCallFnPattern { instruction });
549 }
550
551 let address =
552 header.host_call_fn_offset - read_only_section_offset + read_only_padding_size;
553
554 if !address.is_multiple_of(u32::from(<ContractInstruction>::ALIGNMENT)) {
555 return Err(ContractFileParseError::HostCallFnUnaligned {
556 file_offset: header.host_call_fn_offset,
557 memory_address: address,
558 });
559 }
560 }
561
562 {
564 let mut offset = code_section_offset as usize;
565
566 while offset < file_bytes.len() {
567 let remaining = &file_bytes[offset..];
568
569 let instruction_word = if remaining.len() >= size_of::<u32>() {
570 u32::from_le_bytes([remaining[0], remaining[1], remaining[2], remaining[3]])
571 } else if remaining.len() >= size_of::<u16>() {
572 u32::from_le_bytes([remaining[0], remaining[1], 0, 0])
573 } else {
574 return Err(ContractFileParseError::UnexpectedTrailingCodeBytes {
576 num_bytes: remaining.len(),
577 });
578 };
579
580 let instruction = <ContractInstruction>::try_decode(instruction_word).ok_or(
581 ContractFileParseError::InvalidInstruction {
582 instruction: instruction_word,
583 },
584 )?;
585
586 if usize::from(instruction.size()) > remaining.len() {
589 return Err(ContractFileParseError::UnexpectedTrailingCodeBytes {
590 num_bytes: remaining.len(),
591 });
592 }
593
594 offset += usize::from(instruction.size());
595 }
596 }
597
598 Ok(Self {
599 read_only_section_file_size: header.read_only_section_file_size,
600 read_only_section_memory_size: header.read_only_section_memory_size,
601 num_methods: header.num_methods,
602 bytes: file_bytes,
603 })
604 }
605
606 pub unsafe fn parse_unchecked(file_bytes: &'a [u8]) -> Self {
614 let (header, _remainder) =
617 unsafe { Unaligned::<ContractFileHeader>::from_bytes(file_bytes).unwrap_unchecked() };
618 let header = header.as_inner();
619
620 Self {
621 read_only_section_file_size: header.read_only_section_file_size,
622 read_only_section_memory_size: header.read_only_section_memory_size,
623 num_methods: header.num_methods,
624 bytes: file_bytes,
625 }
626 }
627
628 #[inline(always)]
630 pub fn header(&self) -> ContractFileHeader {
631 let (header, _remainder) =
634 unsafe { Unaligned::<ContractFileHeader>::from_bytes(self.bytes).unwrap_unchecked() };
635 header.as_inner()
636 }
637
638 #[inline]
640 pub fn metadata_bytes(&self) -> &[u8] {
641 let header = self.header();
642 unsafe {
644 self.bytes
645 .get_unchecked(header.metadata_offset as usize..)
646 .get_unchecked(..header.metadata_size as usize)
647 }
648 }
649
650 #[inline]
652 pub fn contract_memory_size(&self) -> u32 {
653 let read_only_section_offset = ContractFileHeader::SIZE
654 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
655 let read_only_padding_size =
656 self.read_only_section_memory_size - self.read_only_section_file_size;
657 self.bytes.len() as u32 - read_only_section_offset + read_only_padding_size
658 }
659
660 #[must_use = "Must check that contract memory was large enough"]
665 pub fn initialize_contract_memory(&self, mut contract_memory: BorrowedCursor<'_, u8>) -> bool {
666 let contract_memory_input_size = contract_memory.capacity();
667 let read_only_section_offset = ContractFileHeader::SIZE
668 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
669 let read_only_padding_size =
670 self.read_only_section_memory_size - self.read_only_section_file_size;
671
672 let source_bytes = unsafe {
674 self.bytes
675 .get_unchecked(read_only_section_offset as usize..)
676 };
677
678 let mut contract_memory_bytes = unsafe { contract_memory.as_mut() };
680
681 if contract_memory_bytes.len() == source_bytes.len() {
683 contract_memory_bytes.write_copy_of_slice(source_bytes);
684 unsafe {
686 contract_memory.advance(contract_memory_input_size);
687 }
688 return true;
689 }
690
691 let Some(read_only_file_target_bytes) =
692 contract_memory_bytes.split_off_mut(..self.read_only_section_file_size as usize)
693 else {
694 trace!(
695 %contract_memory_input_size,
696 contract_memory_size = %self.contract_memory_size(),
697 read_only_section_file_size = self.read_only_section_file_size,
698 "Not enough bytes to write read-only section from the file"
699 );
700
701 return false;
702 };
703
704 let (read_only_file_source_bytes, code_source_bytes) =
706 unsafe { source_bytes.split_at_unchecked(self.read_only_section_file_size as usize) };
707 read_only_file_target_bytes.write_copy_of_slice(read_only_file_source_bytes);
709
710 let Some(read_only_padding_bytes) =
711 contract_memory_bytes.split_off_mut(..read_only_padding_size as usize)
712 else {
713 trace!(
714 %contract_memory_input_size,
715 contract_memory_size = %self.contract_memory_size(),
716 read_only_section_file_size = self.read_only_section_file_size,
717 read_only_section_memory_size = self.read_only_section_memory_size,
718 %read_only_padding_size,
719 "Not enough bytes to write read-only padding section"
720 );
721
722 return false;
723 };
724
725 read_only_padding_bytes.write_filled(0);
727
728 if code_source_bytes.len() != contract_memory_bytes.len() {
729 trace!(
730 %contract_memory_input_size,
731 contract_memory_size = %self.contract_memory_size(),
732 read_only_section_file_size = self.read_only_section_file_size,
733 read_only_section_memory_size = self.read_only_section_memory_size,
734 %read_only_padding_size,
735 code_size = %code_source_bytes.len(),
736 "Not enough bytes to write code section from the file"
737 );
738
739 return false;
740 }
741
742 contract_memory_bytes.write_copy_of_slice(code_source_bytes);
743 unsafe {
745 contract_memory.advance(contract_memory_input_size);
746 }
747
748 true
749 }
750
751 pub fn get_code(&self) -> &[u8] {
753 let read_only_section_offset = ContractFileHeader::SIZE
754 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
755
756 let source_bytes = unsafe {
758 self.bytes
759 .get_unchecked(read_only_section_offset as usize..)
760 };
761
762 let (_read_only_file_source_bytes, code_source_bytes) =
764 unsafe { source_bytes.split_at_unchecked(self.read_only_section_file_size as usize) };
765
766 code_source_bytes
767 }
768
769 pub fn iterate_methods(
771 &self,
772 ) -> impl ExactSizeIterator<Item = ContractFileMethod<'_>> + TrustedLen {
773 let metadata_bytes = self.metadata_bytes();
774
775 #[ouroboros::self_referencing]
776 struct MethodsMetadataIterState<'metadata> {
777 metadata_decoder: MetadataDecoder<'metadata>,
778 #[borrows(mut metadata_decoder)]
779 #[covariant]
780 methods_metadata_decoder: Option<MethodsMetadataDecoder<'this, 'metadata>>,
781 }
782
783 let metadata_decoder = MetadataDecoder::new(metadata_bytes);
784
785 let mut methods_metadata_state =
786 MethodsMetadataIterState::new(metadata_decoder, |metadata_decoder| {
787 metadata_decoder
788 .decode_next()
789 .and_then(Result::ok)
790 .map(MetadataItem::into_decoder)
791 });
792
793 let mut metadata_methods_iter = iter::from_fn(move || {
794 loop {
795 let maybe_next_item = methods_metadata_state.with_methods_metadata_decoder_mut(
796 |maybe_methods_metadata_decoder| {
797 let methods_metadata_decoder = maybe_methods_metadata_decoder.as_mut()?;
798 let method_metadata_decoder = methods_metadata_decoder.decode_next()?;
799
800 let before_remaining_bytes =
801 method_metadata_decoder.remaining_metadata_bytes();
802
803 let (_, method_metadata_item) = method_metadata_decoder
804 .decode_next()
805 .expect("Input is valid according to function contract; qed");
806
807 let method_metadata_bytes = unsafe {
809 metadata_bytes
810 .get_unchecked(metadata_bytes.len() - before_remaining_bytes..)
811 .get_unchecked(
812 ..before_remaining_bytes
813 - methods_metadata_decoder.remaining_metadata_bytes(),
814 )
815 };
816
817 Some((method_metadata_item, method_metadata_bytes))
818 },
819 );
820
821 if let Some(next_item) = maybe_next_item {
822 return Some(next_item);
823 }
824
825 replace_with_or_abort(&mut methods_metadata_state, |methods_metadata_state| {
827 let metadata_decoder = methods_metadata_state.into_heads().metadata_decoder;
828 MethodsMetadataIterState::new(metadata_decoder, |metadata_decoder| {
829 metadata_decoder
830 .decode_next()
831 .and_then(Result::ok)
832 .map(MetadataItem::into_decoder)
833 })
834 });
835
836 if methods_metadata_state
837 .borrow_methods_metadata_decoder()
838 .is_none()
839 {
840 return None;
841 }
842 }
843 });
844
845 let read_only_section_offset = ContractFileHeader::SIZE
846 + u32::from(self.num_methods) * ContractFileMethodMetadata::SIZE;
847 let read_only_padding_size =
848 self.read_only_section_memory_size - self.read_only_section_file_size;
849 let contract_file_methods_metadata_bytes =
851 unsafe { self.bytes.get_unchecked(size_of::<ContractFileHeader>()..) };
852 let (contract_file_methods_metadata, _remainder) =
853 Unaligned::<ContractFileMethodMetadata>::slice_from_bytes(
854 contract_file_methods_metadata_bytes,
855 usize::from(self.num_methods),
856 )
857 .expect("Protected internal invariant checked in constructor; qed");
858
859 contract_file_methods_metadata
860 .iter()
861 .map(move |contract_file_method_metadata| {
862 let contract_file_method_metadata = contract_file_method_metadata.as_inner();
863
864 let (method_metadata_item, method_metadata_bytes) = metadata_methods_iter
865 .next()
866 .expect("Protected internal invariant checked in constructor; qed");
867
868 ContractFileMethod {
869 address: contract_file_method_metadata.offset - read_only_section_offset
870 + read_only_padding_size,
871 size: contract_file_method_metadata.size,
872 method_metadata_item,
873 method_metadata_bytes,
874 }
875 })
876 }
877}