pub const MEMORY_REQUIRED: u32 = memory_required();
pub const CALL_STACK_DEPTH: u32 = 25;
pub const NUM_EVENT_TOPICS: u32 = 4;
pub const MAX_TRANSACTION_PAYLOAD_SIZE: u32 = code::BLOB_BYTES + CALLDATA_BYTES;
pub const STORAGE_BYTES: u32 = 416;
pub const EVENT_BYTES: u32 = 64 * 1024;
pub const EXTRA_EVENT_CHARGE_PER_BYTE: u64 = 256 * 1024;
pub const CALLDATA_BYTES: u32 = 128 * 1024;
pub const TRANSIENT_STORAGE_BYTES: u32 = 4 * 1024;
pub const STORAGE_KEY_BYTES: u32 = 128;
pub const PAGE_SIZE: u32 = 4 * 1024;
pub const IMMUTABLE_BYTES: u32 = 4 * 1024;
pub const EVM_MEMORY_BYTES: u32 = 1024 * 1024;
pub const EVM_STACK_LIMIT: u32 = 1024;
pub mod code {
use super::PAGE_SIZE;
use crate::{Config, Error, LOG_TARGET};
use alloc::vec::Vec;
use sp_runtime::DispatchError;
pub const BLOB_BYTES: u32 = 1024 * 1024;
pub const INTERPRETER_CACHE_BYTES: u32 = 1024 * 1024;
pub const BASIC_BLOCK_SIZE: u32 = 1000;
pub const PURGABLE_MEMORY_LIMIT: u32 = INTERPRETER_CACHE_BYTES + 2 * 1024 * 1024;
pub const BASELINE_MEMORY_LIMIT: u32 = BLOB_BYTES + 512 * 1024;
pub fn enforce<T: Config>(
pvm_blob: Vec<u8>,
available_syscalls: &[&[u8]],
) -> Result<Vec<u8>, DispatchError> {
use polkavm_common::program::{
EstimateInterpreterMemoryUsageArgs, ISA_ReviveV1, InstructionSetKind,
};
let len: u64 = pvm_blob.len() as u64;
if len > crate::limits::code::BLOB_BYTES.into() {
log::debug!(target: LOG_TARGET, "contract blob too large: {len} limit: {BLOB_BYTES}");
return Err(<Error<T>>::BlobTooLarge.into())
}
#[cfg(feature = "std")]
if std::env::var_os("REVIVE_SKIP_VALIDATION").is_some() {
log::warn!(target: LOG_TARGET, "Skipping validation because env var REVIVE_SKIP_VALIDATION is set");
return Ok(pvm_blob)
}
let program = polkavm::ProgramBlob::parse(pvm_blob.as_slice().into()).map_err(|err| {
log::debug!(target: LOG_TARGET, "failed to parse polkavm blob: {err:?}");
Error::<T>::CodeRejected
})?;
if !program.is_64_bit() {
log::debug!(target: LOG_TARGET, "32bit programs are not supported.");
Err(Error::<T>::CodeRejected)?;
}
if program.isa() != InstructionSetKind::ReviveV1 {
log::debug!(target: LOG_TARGET, "Program instruction set '{}' is not '{}'", program.isa().name(), InstructionSetKind::ReviveV1.name());
Err(Error::<T>::CodeRejected)?;
}
for (idx, import) in program.imports().iter().enumerate() {
if idx == available_syscalls.len() {
log::debug!(target: LOG_TARGET, "Program contains too many imports.");
Err(Error::<T>::CodeRejected)?;
}
let Some(import) = import else {
log::debug!(target: LOG_TARGET, "Program contains malformed import.");
return Err(Error::<T>::CodeRejected.into());
};
if !available_syscalls.contains(&import.as_bytes()) {
log::debug!(target: LOG_TARGET, "Program references unknown syscall: {}", import);
Err(Error::<T>::CodeRejected)?;
}
}
let mut max_block_size: u32 = 0;
let mut block_size: u32 = 0;
let mut basic_block_count: u32 = 0;
let mut instruction_count: u32 = 0;
for inst in program.instructions_with_isa(ISA_ReviveV1) {
use polkavm::program::Instruction;
block_size += 1;
instruction_count += 1;
if inst.kind.opcode().starts_new_basic_block() {
max_block_size = max_block_size.max(block_size);
block_size = 0;
basic_block_count += 1;
}
match inst.kind {
Instruction::invalid => {
log::debug!(target: LOG_TARGET, "invalid instruction at offset {}", inst.offset);
return Err(<Error<T>>::InvalidInstruction.into())
},
Instruction::sbrk(_, _) => {
log::debug!(target: LOG_TARGET, "sbrk instruction is not allowed. offset {}", inst.offset);
return Err(<Error<T>>::InvalidInstruction.into())
},
#[cfg(not(feature = "runtime-benchmarks"))]
Instruction::ecalli(idx) if idx == crate::SENTINEL => {
log::debug!(target: LOG_TARGET, "reserved syscall idx {idx}. offset {}", inst.offset);
return Err(<Error<T>>::InvalidInstruction.into())
},
_ => (),
}
}
max_block_size = max_block_size.max(block_size);
if max_block_size > BASIC_BLOCK_SIZE {
log::debug!(target: LOG_TARGET, "basic block too large: {max_block_size} limit: {BASIC_BLOCK_SIZE}");
return Err(Error::<T>::BasicBlockTooLarge.into())
}
let usage_args = EstimateInterpreterMemoryUsageArgs::BoundedCache {
max_cache_size_bytes: INTERPRETER_CACHE_BYTES,
instruction_count,
max_block_size,
basic_block_count,
page_size: PAGE_SIZE,
};
let program_info =
program.estimate_interpreter_memory_usage(usage_args).map_err(|err| {
log::debug!(target: LOG_TARGET, "failed to estimate memory usage of program: {err:?}");
Error::<T>::CodeRejected
})?;
log::trace!(
target: LOG_TARGET, "Contract memory usage: purgable={}/{} KB baseline={}/{}",
program_info.purgeable_ram_consumption, PURGABLE_MEMORY_LIMIT,
program_info.baseline_ram_consumption, BASELINE_MEMORY_LIMIT,
);
if program_info.purgeable_ram_consumption > PURGABLE_MEMORY_LIMIT {
log::debug!(target: LOG_TARGET, "contract uses too much purgeable memory: {} limit: {}",
program_info.purgeable_ram_consumption,
PURGABLE_MEMORY_LIMIT,
);
return Err(Error::<T>::StaticMemoryTooLarge.into())
}
if program_info.baseline_ram_consumption > BASELINE_MEMORY_LIMIT {
log::debug!(target: LOG_TARGET, "contract uses too much baseline memory: {} limit: {}",
program_info.baseline_ram_consumption,
BASELINE_MEMORY_LIMIT,
);
return Err(Error::<T>::StaticMemoryTooLarge.into())
}
Ok(pvm_blob)
}
}
const fn memory_required() -> u32 {
let max_call_depth = CALL_STACK_DEPTH + 1;
let per_stack_memory = code::PURGABLE_MEMORY_LIMIT + TRANSIENT_STORAGE_BYTES * 2;
let evm_max_initcode_size = revm::primitives::eip3860::MAX_INITCODE_SIZE as u32;
let evm_overhead = EVM_MEMORY_BYTES + evm_max_initcode_size + EVM_STACK_LIMIT * 32;
let per_frame_memory = if code::BASELINE_MEMORY_LIMIT > evm_overhead {
code::BASELINE_MEMORY_LIMIT
} else {
evm_overhead
} + CALLDATA_BYTES * 2;
per_stack_memory + max_call_depth * per_frame_memory
}