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