use std::{collections::BTreeSet, fmt};
use allocative::Allocative;
use linera_base::{
data_types::{Amount, ArithmeticError, BlobContent, CompressedBytecode, Resources},
ensure,
identifiers::{ApplicationId, BlobType},
vm::VmRuntime,
};
use serde::{Deserialize, Serialize};
use crate::{ExecutionError, FLAG_FREE_APPLICATION_ID_PREFIX, FLAG_FREE_APPLICATION_ID_SUFFIX};
#[derive(Eq, PartialEq, Hash, Clone, Debug, Serialize, Deserialize, Allocative)]
pub struct ResourceControlPolicy {
pub wasm_fuel_unit: Amount,
pub evm_fuel_unit: Amount,
pub read_operation: Amount,
pub write_operation: Amount,
pub byte_runtime: Amount,
pub byte_read: Amount,
pub byte_written: Amount,
pub blob_read: Amount,
pub blob_published: Amount,
pub blob_byte_read: Amount,
pub blob_byte_published: Amount,
pub byte_stored: Amount,
pub operation: Amount,
pub operation_byte: Amount,
pub message: Amount,
pub message_byte: Amount,
pub service_as_oracle_query: Amount,
pub http_request: Amount,
pub maximum_wasm_fuel_per_block: u64,
pub maximum_evm_fuel_per_block: u64,
pub maximum_service_oracle_execution_ms: u64,
pub maximum_block_size: u64,
pub maximum_bytecode_size: u64,
pub maximum_blob_size: u64,
pub maximum_published_blobs: u64,
pub maximum_block_proposal_size: u64,
pub maximum_bytes_read_per_block: u64,
pub maximum_bytes_written_per_block: u64,
pub maximum_oracle_response_bytes: u64,
pub maximum_http_response_bytes: u64,
pub http_request_timeout_ms: u64,
pub http_request_allow_list: BTreeSet<String>,
}
impl fmt::Display for ResourceControlPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let ResourceControlPolicy {
wasm_fuel_unit,
evm_fuel_unit,
read_operation,
write_operation,
byte_runtime,
byte_read,
byte_written,
blob_read,
blob_published,
blob_byte_read,
blob_byte_published,
byte_stored,
operation,
operation_byte,
message,
message_byte,
service_as_oracle_query,
http_request,
maximum_wasm_fuel_per_block,
maximum_evm_fuel_per_block,
maximum_service_oracle_execution_ms,
maximum_block_size,
maximum_blob_size,
maximum_published_blobs,
maximum_bytecode_size,
maximum_block_proposal_size,
maximum_bytes_read_per_block,
maximum_bytes_written_per_block,
maximum_oracle_response_bytes,
maximum_http_response_bytes,
http_request_allow_list,
http_request_timeout_ms,
} = self;
write!(
f,
"Resource control policy:\n\
{wasm_fuel_unit:.2} cost per Wasm fuel unit\n\
{evm_fuel_unit:.2} cost per EVM fuel unit\n\
{read_operation:.2} cost per read operation\n\
{write_operation:.2} cost per write operation\n\
{byte_runtime:.2} cost per runtime byte read operation\n\
{byte_read:.2} cost per byte read\n\
{byte_written:.2} cost per byte written\n\
{blob_read:.2} base cost per read blob\n\
{blob_published:.2} base cost per published blob\n\
{blob_byte_read:.2} cost of reading blobs, per byte\n\
{blob_byte_published:.2} cost of publishing blobs, per byte\n\
{byte_stored:.2} cost per byte stored\n\
{operation:.2} per operation\n\
{operation_byte:.2} per byte in the argument of an operation\n\
{service_as_oracle_query:.2} per query to a service as an oracle\n\
{message:.2} per outgoing messages\n\
{message_byte:.2} per byte in the argument of an outgoing messages\n\
{http_request:.2} per HTTP request performed\n\
{maximum_wasm_fuel_per_block} maximum Wasm fuel per block\n\
{maximum_evm_fuel_per_block} maximum EVM fuel per block\n\
{maximum_service_oracle_execution_ms} ms maximum service-as-oracle execution time per \
block\n\
{maximum_block_size} maximum size of a block\n\
{maximum_blob_size} maximum size of a data blob, bytecode or other binary blob\n\
{maximum_published_blobs} maximum number of blobs published per block\n\
{maximum_bytecode_size} maximum size of service and contract bytecode\n\
{maximum_block_proposal_size} maximum size of a block proposal\n\
{maximum_bytes_read_per_block} maximum number of bytes read per block\n\
{maximum_bytes_written_per_block} maximum number of bytes written per block\n\
{maximum_oracle_response_bytes} maximum number of bytes of an oracle response\n\
{maximum_http_response_bytes} maximum number of bytes of an HTTP response\n\
{http_request_timeout_ms} ms timeout for HTTP requests\n\
HTTP hosts allowed for contracts and services: {http_request_allow_list:#?}\n",
)?;
Ok(())
}
}
impl Default for ResourceControlPolicy {
fn default() -> Self {
Self::no_fees()
}
}
impl ResourceControlPolicy {
pub fn no_fees() -> Self {
Self {
wasm_fuel_unit: Amount::ZERO,
evm_fuel_unit: Amount::ZERO,
read_operation: Amount::ZERO,
write_operation: Amount::ZERO,
byte_runtime: Amount::ZERO,
byte_read: Amount::ZERO,
byte_written: Amount::ZERO,
blob_read: Amount::ZERO,
blob_published: Amount::ZERO,
blob_byte_read: Amount::ZERO,
blob_byte_published: Amount::ZERO,
byte_stored: Amount::ZERO,
operation: Amount::ZERO,
operation_byte: Amount::ZERO,
message: Amount::ZERO,
message_byte: Amount::ZERO,
service_as_oracle_query: Amount::ZERO,
http_request: Amount::ZERO,
maximum_wasm_fuel_per_block: u64::MAX,
maximum_evm_fuel_per_block: u64::MAX,
maximum_service_oracle_execution_ms: u64::MAX,
maximum_block_size: u64::MAX,
maximum_blob_size: u64::MAX,
maximum_published_blobs: u64::MAX,
maximum_bytecode_size: u64::MAX,
maximum_block_proposal_size: u64::MAX,
maximum_bytes_read_per_block: u64::MAX,
maximum_bytes_written_per_block: u64::MAX,
maximum_oracle_response_bytes: u64::MAX,
maximum_http_response_bytes: u64::MAX,
http_request_timeout_ms: u64::MAX,
http_request_allow_list: BTreeSet::new(),
}
}
pub fn free_app_flag(app_id: &ApplicationId) -> String {
format!("{FLAG_FREE_APPLICATION_ID_PREFIX}{app_id}{FLAG_FREE_APPLICATION_ID_SUFFIX}")
}
pub fn is_free_app(&self, app_id: &ApplicationId) -> bool {
self.http_request_allow_list
.contains(&Self::free_app_flag(app_id))
}
pub fn maximum_fuel_per_block(&self, vm_runtime: VmRuntime) -> u64 {
match vm_runtime {
VmRuntime::Wasm => self.maximum_wasm_fuel_per_block,
VmRuntime::Evm => self.maximum_evm_fuel_per_block,
}
}
#[cfg(with_testing)]
pub fn only_fuel() -> Self {
Self {
wasm_fuel_unit: Amount::from_micros(1),
evm_fuel_unit: Amount::from_micros(1),
..Self::no_fees()
}
}
#[cfg(with_testing)]
pub fn all_categories() -> Self {
Self {
wasm_fuel_unit: Amount::from_nanos(1),
evm_fuel_unit: Amount::from_nanos(1),
byte_read: Amount::from_attos(100),
byte_written: Amount::from_attos(1_000),
blob_read: Amount::from_nanos(1),
blob_published: Amount::from_nanos(10),
blob_byte_read: Amount::from_attos(100),
blob_byte_published: Amount::from_attos(1_000),
operation: Amount::from_attos(10),
operation_byte: Amount::from_attos(1),
message: Amount::from_attos(10),
message_byte: Amount::from_attos(1),
http_request: Amount::from_micros(1),
..Self::no_fees()
}
}
pub fn testnet() -> Self {
Self {
wasm_fuel_unit: Amount::from_nanos(10),
evm_fuel_unit: Amount::from_nanos(10),
byte_runtime: Amount::from_nanos(1),
byte_read: Amount::from_nanos(10),
byte_written: Amount::from_nanos(100),
blob_read: Amount::from_nanos(100),
blob_published: Amount::from_nanos(1000),
blob_byte_read: Amount::from_nanos(10),
blob_byte_published: Amount::from_nanos(100),
read_operation: Amount::from_micros(10),
write_operation: Amount::from_micros(20),
byte_stored: Amount::from_nanos(10),
message_byte: Amount::from_nanos(100),
operation_byte: Amount::from_nanos(10),
operation: Amount::from_micros(10),
message: Amount::from_micros(10),
service_as_oracle_query: Amount::from_millis(10),
http_request: Amount::from_micros(50),
maximum_wasm_fuel_per_block: 100_000_000,
maximum_evm_fuel_per_block: 100_000_000,
maximum_service_oracle_execution_ms: 10_000,
maximum_block_size: 1_000_000,
maximum_blob_size: 1_000_000,
maximum_published_blobs: 10,
maximum_bytecode_size: 10_000_000,
maximum_block_proposal_size: 13_000_000,
maximum_bytes_read_per_block: 100_000_000,
maximum_bytes_written_per_block: 10_000_000,
maximum_oracle_response_bytes: 10_000,
maximum_http_response_bytes: 10_000,
http_request_timeout_ms: 20_000,
http_request_allow_list: BTreeSet::new(),
}
}
pub fn total_price(&self, resources: &Resources) -> Result<Amount, ArithmeticError> {
let mut amount = Amount::ZERO;
amount.try_add_assign(self.fuel_price(resources.wasm_fuel, VmRuntime::Wasm)?)?;
amount.try_add_assign(self.fuel_price(resources.evm_fuel, VmRuntime::Evm)?)?;
amount.try_add_assign(self.read_operations_price(resources.read_operations)?)?;
amount.try_add_assign(self.bytes_runtime_price(resources.bytes_runtime)?)?;
amount.try_add_assign(self.write_operations_price(resources.write_operations)?)?;
amount.try_add_assign(self.bytes_read_price(resources.bytes_to_read as u64)?)?;
amount.try_add_assign(self.bytes_written_price(resources.bytes_to_write as u64)?)?;
amount.try_add_assign(
self.blob_byte_read
.try_mul(resources.blob_bytes_to_read as u128)?
.try_add(self.blob_read.try_mul(resources.blobs_to_read as u128)?)?,
)?;
amount.try_add_assign(
self.blob_byte_published
.try_mul(resources.blob_bytes_to_publish as u128)?
.try_add(
self.blob_published
.try_mul(resources.blobs_to_publish as u128)?,
)?,
)?;
amount.try_add_assign(self.message.try_mul(resources.messages as u128)?)?;
amount.try_add_assign(self.message_bytes_price(resources.message_size as u64)?)?;
amount.try_add_assign(self.bytes_stored_price(resources.storage_size_delta as u64)?)?;
amount.try_add_assign(
self.service_as_oracle_queries_price(resources.service_as_oracle_queries)?,
)?;
amount.try_add_assign(self.http_requests_price(resources.http_requests)?)?;
Ok(amount)
}
pub(crate) fn operation_bytes_price(&self, size: u64) -> Result<Amount, ArithmeticError> {
self.operation_byte.try_mul(size as u128)
}
pub(crate) fn message_bytes_price(&self, size: u64) -> Result<Amount, ArithmeticError> {
self.message_byte.try_mul(size as u128)
}
pub(crate) fn read_operations_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
self.read_operation.try_mul(count as u128)
}
pub(crate) fn write_operations_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
self.write_operation.try_mul(count as u128)
}
pub(crate) fn bytes_runtime_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
self.byte_runtime.try_mul(count as u128)
}
pub(crate) fn bytes_read_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
self.byte_read.try_mul(count as u128)
}
pub(crate) fn bytes_written_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
self.byte_written.try_mul(count as u128)
}
pub(crate) fn blob_read_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
self.blob_byte_read
.try_mul(count as u128)?
.try_add(self.blob_read)
}
pub(crate) fn blob_published_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
self.blob_byte_published
.try_mul(count as u128)?
.try_add(self.blob_published)
}
#[allow(dead_code)]
pub(crate) fn bytes_stored_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
self.byte_stored.try_mul(count as u128)
}
pub(crate) fn service_as_oracle_queries_price(
&self,
count: u32,
) -> Result<Amount, ArithmeticError> {
self.service_as_oracle_query.try_mul(count as u128)
}
pub(crate) fn http_requests_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
self.http_request.try_mul(count as u128)
}
fn fuel_unit_price(&self, vm_runtime: VmRuntime) -> Amount {
match vm_runtime {
VmRuntime::Wasm => self.wasm_fuel_unit,
VmRuntime::Evm => self.evm_fuel_unit,
}
}
pub(crate) fn fuel_price(
&self,
fuel: u64,
vm_runtime: VmRuntime,
) -> Result<Amount, ArithmeticError> {
self.fuel_unit_price(vm_runtime).try_mul(u128::from(fuel))
}
pub(crate) fn remaining_fuel(&self, balance: Amount, vm_runtime: VmRuntime) -> u64 {
let fuel_unit = self.fuel_unit_price(vm_runtime);
u64::try_from(balance.saturating_ratio(fuel_unit)).unwrap_or(u64::MAX)
}
pub fn check_blob_size(&self, content: &BlobContent) -> Result<(), ExecutionError> {
ensure!(
u64::try_from(content.bytes().len())
.ok()
.is_some_and(|size| size <= self.maximum_blob_size),
ExecutionError::BlobTooLarge
);
match content.blob_type() {
BlobType::ContractBytecode | BlobType::ServiceBytecode | BlobType::EvmBytecode => {
ensure!(
CompressedBytecode::decompressed_size_at_most(
content.bytes(),
self.maximum_bytecode_size
)?,
ExecutionError::BytecodeTooLarge
);
}
BlobType::Data
| BlobType::ApplicationDescription
| BlobType::Committee
| BlobType::ChainDescription => {}
}
Ok(())
}
}