use crate::{
code_cache::{
module_cache::{ModuleCache, TransactionModuleCache},
script_cache::ScriptCache,
},
data_cache::RemoteCache,
loaded_data::loaded_module::LoadedModule,
process_txn::{verify::VerifiedTransaction, ProcessTransaction},
txn_executor::TransactionExecutor,
};
use solana_libra_config::config::VMPublishingOption;
use solana_libra_crypto::HashValue;
use solana_libra_logger::prelude::*;
use solana_libra_types::{
transaction::{SignatureCheckedTransaction, TransactionPayload, MAX_TRANSACTION_SIZE_IN_BYTES},
vm_error::{StatusCode, VMStatus},
};
use solana_libra_vm::{
errors::convert_prologue_runtime_error,
gas_schedule::{self, AbstractMemorySize, GasAlgebra, GasCarrier},
transaction_metadata::TransactionMetadata,
};
use solana_libra_vm_cache_map::Arena;
pub fn is_allowed_script(publishing_option: &VMPublishingOption, program: &[u8]) -> bool {
match publishing_option {
VMPublishingOption::Open | VMPublishingOption::CustomScripts => true,
VMPublishingOption::Locked(whitelist) => {
let hash_value = HashValue::from_sha3_256(program);
whitelist.contains(hash_value.as_ref())
}
}
}
pub struct ValidatedTransaction<'alloc, 'txn, P>
where
'alloc: 'txn,
P: ModuleCache<'alloc>,
{
txn: SignatureCheckedTransaction,
txn_state: Option<ValidatedTransactionState<'alloc, 'txn, P>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ValidationMode {
Genesis,
Validating,
#[allow(dead_code)]
Executing,
}
impl<'alloc, 'txn, P> ValidatedTransaction<'alloc, 'txn, P>
where
'alloc: 'txn,
P: ModuleCache<'alloc>,
{
pub(super) fn new(
process_txn: ProcessTransaction<'alloc, 'txn, P>,
mode: ValidationMode,
publishing_option: &VMPublishingOption,
) -> Result<Self, VMStatus> {
let ProcessTransaction {
txn,
module_cache,
data_cache,
allocator,
..
} = process_txn;
let txn_state = match txn.payload() {
TransactionPayload::Program(program) => {
Some(ValidatedTransaction::validate(
&txn,
module_cache,
data_cache,
allocator,
mode,
|| {
if !is_allowed_script(&publishing_option, &program.code()) {
warn!("[VM] Custom scripts not allowed: {:?}", &program.code());
return Err(VMStatus::new(StatusCode::UNKNOWN_SCRIPT));
}
if !publishing_option.is_open() {
if !program.modules().is_empty() {
warn!("[VM] Custom modules not allowed");
return Err(VMStatus::new(StatusCode::UNKNOWN_MODULE));
}
}
Ok(())
},
)?)
}
TransactionPayload::Script(script) => {
Some(ValidatedTransaction::validate(
&txn,
module_cache,
data_cache,
allocator,
mode,
|| {
if !is_allowed_script(&publishing_option, &script.code()) {
warn!("[VM] Custom scripts not allowed: {:?}", &script.code());
return Err(VMStatus::new(StatusCode::UNKNOWN_SCRIPT));
}
Ok(())
},
)?)
}
TransactionPayload::Module(module) => {
debug!("validate module {:?}", module);
Some(ValidatedTransaction::validate(
&txn,
module_cache,
data_cache,
allocator,
mode,
|| {
if !publishing_option.is_open() {
warn!("[VM] Custom modules not allowed");
Err(VMStatus::new(StatusCode::UNKNOWN_MODULE))
} else {
Ok(())
}
},
)?)
}
TransactionPayload::WriteSet(write_set) => {
if mode != ValidationMode::Genesis {
warn!("[VM] Attempt to process genesis after initialization");
return Err(VMStatus::new(StatusCode::REJECTED_WRITE_SET));
}
for (_access_path, write_op) in write_set {
if write_op.is_deletion() {
error!("[VM] Bad genesis block");
return Err(VMStatus::new(StatusCode::INVALID_WRITE_SET));
}
}
None
}
};
Ok(Self { txn, txn_state })
}
pub fn verify(
self,
script_cache: &'txn ScriptCache<'alloc>,
) -> Result<VerifiedTransaction<'alloc, 'txn, P>, VMStatus> {
VerifiedTransaction::new(self, script_cache)
}
pub fn as_inner(&self) -> &SignatureCheckedTransaction {
&self.txn
}
#[allow(dead_code)]
pub fn into_inner(self) -> SignatureCheckedTransaction {
self.txn
}
pub(super) fn take_state(&mut self) -> Option<ValidatedTransactionState<'alloc, 'txn, P>> {
self.txn_state.take()
}
fn validate(
txn: &SignatureCheckedTransaction,
module_cache: P,
data_cache: &'txn dyn RemoteCache,
allocator: &'txn Arena<LoadedModule>,
mode: ValidationMode,
payload_check: impl Fn() -> Result<(), VMStatus>,
) -> Result<ValidatedTransactionState<'alloc, 'txn, P>, VMStatus> {
let raw_bytes_len = AbstractMemorySize::new(txn.raw_txn_bytes_len() as GasCarrier);
if txn.raw_txn_bytes_len() > MAX_TRANSACTION_SIZE_IN_BYTES {
let error_str = format!(
"max size: {}, txn size: {}",
MAX_TRANSACTION_SIZE_IN_BYTES,
raw_bytes_len.get()
);
warn!(
"[VM] Transaction size too big {} (max {})",
raw_bytes_len.get(),
MAX_TRANSACTION_SIZE_IN_BYTES
);
return Err(
VMStatus::new(StatusCode::EXCEEDED_MAX_TRANSACTION_SIZE).with_message(error_str)
);
}
assume!(raw_bytes_len.get() <= MAX_TRANSACTION_SIZE_IN_BYTES as u64);
if txn.max_gas_amount() > gas_schedule::MAXIMUM_NUMBER_OF_GAS_UNITS.get() {
let error_str = format!(
"max gas units: {}, gas units submitted: {}",
gas_schedule::MAXIMUM_NUMBER_OF_GAS_UNITS.get(),
txn.max_gas_amount()
);
warn!(
"[VM] Gas unit error; max {}, submitted {}",
gas_schedule::MAXIMUM_NUMBER_OF_GAS_UNITS.get(),
txn.max_gas_amount()
);
return Err(
VMStatus::new(StatusCode::MAX_GAS_UNITS_EXCEEDS_MAX_GAS_UNITS_BOUND)
.with_message(error_str),
);
}
let min_txn_fee = gas_schedule::calculate_intrinsic_gas(raw_bytes_len);
if txn.max_gas_amount() < min_txn_fee.get() {
let error_str = format!(
"min gas required for txn: {}, gas submitted: {}",
min_txn_fee.get(),
txn.max_gas_amount()
);
warn!(
"[VM] Gas unit error; min {}, submitted {}",
min_txn_fee.get(),
txn.max_gas_amount()
);
return Err(
VMStatus::new(StatusCode::MAX_GAS_UNITS_BELOW_MIN_TRANSACTION_GAS_UNITS)
.with_message(error_str),
);
}
#[allow(clippy::absurd_extreme_comparisons)]
let below_min_bound = txn.gas_unit_price() < gas_schedule::MIN_PRICE_PER_GAS_UNIT.get();
if below_min_bound {
let error_str = format!(
"gas unit min price: {}, submitted price: {}",
gas_schedule::MIN_PRICE_PER_GAS_UNIT.get(),
txn.gas_unit_price()
);
warn!(
"[VM] Gas unit error; min {}, submitted {}",
gas_schedule::MIN_PRICE_PER_GAS_UNIT.get(),
txn.gas_unit_price()
);
return Err(
VMStatus::new(StatusCode::GAS_UNIT_PRICE_BELOW_MIN_BOUND).with_message(error_str)
);
}
if txn.gas_unit_price() > gas_schedule::MAX_PRICE_PER_GAS_UNIT.get() {
let error_str = format!(
"gas unit max price: {}, submitted price: {}",
gas_schedule::MAX_PRICE_PER_GAS_UNIT.get(),
txn.gas_unit_price()
);
warn!(
"[VM] Gas unit error; min {}, submitted {}",
gas_schedule::MAX_PRICE_PER_GAS_UNIT.get(),
txn.gas_unit_price()
);
return Err(
VMStatus::new(StatusCode::GAS_UNIT_PRICE_ABOVE_MAX_BOUND).with_message(error_str)
);
}
payload_check()?;
let metadata = TransactionMetadata::new(&txn);
let mut txn_state =
ValidatedTransactionState::new(metadata, module_cache, data_cache, allocator);
match txn_state.txn_executor.run_prologue() {
Ok(_) => {}
Err(err) => {
let vm_status = convert_prologue_runtime_error(&err, &txn.sender());
match (mode, vm_status.major_status) {
(ValidationMode::Validating, StatusCode::SEQUENCE_NUMBER_TOO_NEW) => {
trace!("[VM] Sequence number too new error ignored");
}
(_, _) => {
warn!("[VM] Error in prologue: {:?}", err);
return Err(vm_status);
}
}
}
};
Ok(txn_state)
}
}
pub(super) struct ValidatedTransactionState<'alloc, 'txn, P>
where
'alloc: 'txn,
P: ModuleCache<'alloc>,
{
pub(super) txn_executor:
TransactionExecutor<'txn, 'txn, TransactionModuleCache<'alloc, 'txn, P>>,
}
impl<'alloc, 'txn, P> ValidatedTransactionState<'alloc, 'txn, P>
where
'alloc: 'txn,
P: ModuleCache<'alloc>,
{
fn new(
metadata: TransactionMetadata,
module_cache: P,
data_cache: &'txn dyn RemoteCache,
allocator: &'txn Arena<LoadedModule>,
) -> Self {
let txn_module_cache = TransactionModuleCache::new(module_cache, allocator);
let txn_executor = TransactionExecutor::new(txn_module_cache, data_cache, metadata);
Self { txn_executor }
}
}