linera_execution/
policy.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! This module contains types related to fees and pricing.
5
6use std::{collections::BTreeSet, fmt};
7
8use async_graphql::InputObject;
9use linera_base::{
10    data_types::{Amount, ArithmeticError, BlobContent, CompressedBytecode, Resources},
11    ensure,
12    identifiers::BlobType,
13};
14use serde::{Deserialize, Serialize};
15
16use crate::ExecutionError;
17
18/// A collection of prices and limits associated with block execution.
19#[derive(Eq, PartialEq, Hash, Clone, Debug, Serialize, Deserialize, InputObject)]
20pub struct ResourceControlPolicy {
21    /// The base price for creating a new block.
22    pub block: Amount,
23    /// The price per unit of fuel (aka gas) for VM execution.
24    pub fuel_unit: Amount,
25    /// The price of one read operation.
26    pub read_operation: Amount,
27    /// The price of one write operation.
28    pub write_operation: Amount,
29    /// The price of reading a byte.
30    pub byte_read: Amount,
31    /// The price of writing a byte
32    pub byte_written: Amount,
33    /// The base price to read a blob.
34    pub blob_read: Amount,
35    /// The base price to publish a blob.
36    pub blob_published: Amount,
37    /// The price to read a blob, per byte.
38    pub blob_byte_read: Amount,
39    /// The price to publish a blob, per byte.
40    pub blob_byte_published: Amount,
41    /// The price of increasing storage by a byte.
42    // TODO(#1536): This is not fully supported.
43    pub byte_stored: Amount,
44    /// The base price of adding an operation to a block.
45    pub operation: Amount,
46    /// The additional price for each byte in the argument of a user operation.
47    pub operation_byte: Amount,
48    /// The base price of sending a message from a block.
49    pub message: Amount,
50    /// The additional price for each byte in the argument of a user message.
51    pub message_byte: Amount,
52    /// The price per query to a service as an oracle.
53    pub service_as_oracle_query: Amount,
54    /// The price for a performing an HTTP request.
55    pub http_request: Amount,
56
57    // TODO(#1538): Cap the number of transactions per block and the total size of their
58    // arguments.
59    /// The maximum amount of fuel a block can consume.
60    pub maximum_fuel_per_block: u64,
61    /// The maximum time in milliseconds that a block can spend executing services as oracles.
62    pub maximum_service_oracle_execution_ms: u64,
63    /// The maximum size of a block. This includes the block proposal itself as well as
64    /// the execution outcome.
65    pub maximum_block_size: u64,
66    /// The maximum size of decompressed contract or service bytecode, in bytes.
67    pub maximum_bytecode_size: u64,
68    /// The maximum size of a blob.
69    pub maximum_blob_size: u64,
70    /// The maximum number of published blobs per block.
71    pub maximum_published_blobs: u64,
72    /// The maximum size of a block proposal.
73    pub maximum_block_proposal_size: u64,
74    /// The maximum data to read per block
75    pub maximum_bytes_read_per_block: u64,
76    /// The maximum data to write per block
77    pub maximum_bytes_written_per_block: u64,
78    /// The maximum size in bytes of an oracle response.
79    pub maximum_oracle_response_bytes: u64,
80    /// The maximum size in bytes of a received HTTP response.
81    pub maximum_http_response_bytes: u64,
82    /// The maximum amount of time allowed to wait for an HTTP response.
83    pub http_request_timeout_ms: u64,
84    /// The list of hosts that contracts and services can send HTTP requests to.
85    pub http_request_allow_list: BTreeSet<String>,
86}
87
88impl fmt::Display for ResourceControlPolicy {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        let ResourceControlPolicy {
91            block,
92            fuel_unit,
93            read_operation,
94            write_operation,
95            byte_read,
96            byte_written,
97            blob_read,
98            blob_published,
99            blob_byte_read,
100            blob_byte_published,
101            byte_stored,
102            operation,
103            operation_byte,
104            message,
105            message_byte,
106            service_as_oracle_query,
107            http_request,
108            maximum_fuel_per_block,
109            maximum_service_oracle_execution_ms,
110            maximum_block_size,
111            maximum_blob_size,
112            maximum_published_blobs,
113            maximum_bytecode_size,
114            maximum_block_proposal_size,
115            maximum_bytes_read_per_block,
116            maximum_bytes_written_per_block,
117            maximum_oracle_response_bytes,
118            maximum_http_response_bytes,
119            http_request_allow_list,
120            http_request_timeout_ms,
121        } = self;
122        write!(
123            f,
124            "Resource control policy:\n\
125            {block:.2} base cost per block\n\
126            {fuel_unit:.2} cost per fuel unit\n\
127            {read_operation:.2} cost per read operation\n\
128            {write_operation:.2} cost per write operation\n\
129            {byte_read:.2} cost per byte read\n\
130            {byte_written:.2} cost per byte written\n\
131            {blob_read:.2} base cost per read blob\n\
132            {blob_published:.2} base cost per published blob\n\
133            {blob_byte_read:.2} cost of reading blobs, per byte\n\
134            {blob_byte_published:.2} cost of publishing blobs, per byte\n\
135            {byte_stored:.2} cost per byte stored\n\
136            {operation:.2} per operation\n\
137            {operation_byte:.2} per byte in the argument of an operation\n\
138            {service_as_oracle_query:.2} per query to a service as an oracle\n\
139            {message:.2} per outgoing messages\n\
140            {message_byte:.2} per byte in the argument of an outgoing messages\n\
141            {http_request:.2} per HTTP request performed\n\
142            {maximum_fuel_per_block} maximum fuel per block\n\
143            {maximum_service_oracle_execution_ms} ms maximum service-as-oracle execution time per \
144                block\n\
145            {maximum_block_size} maximum size of a block\n\
146            {maximum_blob_size} maximum size of a data blob, bytecode or other binary blob\n\
147            {maximum_published_blobs} maximum number of blobs published per block\n\
148            {maximum_bytecode_size} maximum size of service and contract bytecode\n\
149            {maximum_block_proposal_size} maximum size of a block proposal\n\
150            {maximum_bytes_read_per_block} maximum number of bytes read per block\n\
151            {maximum_bytes_written_per_block} maximum number of bytes written per block\n\
152            {maximum_oracle_response_bytes} maximum number of bytes of an oracle response\n\
153            {maximum_http_response_bytes} maximum number of bytes of an HTTP response\n\
154            {http_request_timeout_ms} ms timeout for HTTP requests\n\
155            HTTP hosts allowed for contracts and services: {http_request_allow_list:#?}\n",
156        )?;
157        Ok(())
158    }
159}
160
161impl Default for ResourceControlPolicy {
162    fn default() -> Self {
163        Self::no_fees()
164    }
165}
166
167impl ResourceControlPolicy {
168    /// Creates a policy with no cost for anything.
169    ///
170    /// This can be used in tests or benchmarks.
171    pub fn no_fees() -> Self {
172        Self {
173            block: Amount::ZERO,
174            fuel_unit: Amount::ZERO,
175            read_operation: Amount::ZERO,
176            write_operation: Amount::ZERO,
177            byte_read: Amount::ZERO,
178            byte_written: Amount::ZERO,
179            blob_read: Amount::ZERO,
180            blob_published: Amount::ZERO,
181            blob_byte_read: Amount::ZERO,
182            blob_byte_published: Amount::ZERO,
183            byte_stored: Amount::ZERO,
184            operation: Amount::ZERO,
185            operation_byte: Amount::ZERO,
186            message: Amount::ZERO,
187            message_byte: Amount::ZERO,
188            service_as_oracle_query: Amount::ZERO,
189            http_request: Amount::ZERO,
190            maximum_fuel_per_block: u64::MAX,
191            maximum_service_oracle_execution_ms: u64::MAX,
192            maximum_block_size: u64::MAX,
193            maximum_blob_size: u64::MAX,
194            maximum_published_blobs: u64::MAX,
195            maximum_bytecode_size: u64::MAX,
196            maximum_block_proposal_size: u64::MAX,
197            maximum_bytes_read_per_block: u64::MAX,
198            maximum_bytes_written_per_block: u64::MAX,
199            maximum_oracle_response_bytes: u64::MAX,
200            maximum_http_response_bytes: u64::MAX,
201            http_request_timeout_ms: u64::MAX,
202            http_request_allow_list: BTreeSet::new(),
203        }
204    }
205
206    /// Creates a policy with no cost for anything except fuel.
207    ///
208    /// This can be used in tests that need whole numbers in their chain balance.
209    #[cfg(with_testing)]
210    pub fn only_fuel() -> Self {
211        Self {
212            fuel_unit: Amount::from_micros(1),
213            ..Self::no_fees()
214        }
215    }
216
217    /// Creates a policy with no cost for anything except fuel, and 0.001 per block.
218    ///
219    /// This can be used in tests, and that keep track of how many blocks were created.
220    #[cfg(with_testing)]
221    pub fn fuel_and_block() -> Self {
222        Self {
223            block: Amount::from_millis(1),
224            fuel_unit: Amount::from_micros(1),
225            ..Self::no_fees()
226        }
227    }
228
229    /// Creates a policy where all categories have a small non-zero cost.
230    #[cfg(with_testing)]
231    pub fn all_categories() -> Self {
232        Self {
233            block: Amount::from_millis(1),
234            fuel_unit: Amount::from_nanos(1),
235            byte_read: Amount::from_attos(100),
236            byte_written: Amount::from_attos(1_000),
237            blob_read: Amount::from_nanos(1),
238            blob_published: Amount::from_nanos(10),
239            blob_byte_read: Amount::from_attos(100),
240            blob_byte_published: Amount::from_attos(1_000),
241            operation: Amount::from_attos(10),
242            operation_byte: Amount::from_attos(1),
243            message: Amount::from_attos(10),
244            message_byte: Amount::from_attos(1),
245            http_request: Amount::from_micros(1),
246            ..Self::no_fees()
247        }
248    }
249
250    /// Creates a policy that matches the Testnet.
251    pub fn testnet() -> Self {
252        Self {
253            block: Amount::from_millis(1),
254            fuel_unit: Amount::from_nanos(10),
255            byte_read: Amount::from_nanos(10),
256            byte_written: Amount::from_nanos(100),
257            blob_read: Amount::from_nanos(100),
258            blob_published: Amount::from_nanos(1000),
259            blob_byte_read: Amount::from_nanos(10),
260            blob_byte_published: Amount::from_nanos(100),
261            read_operation: Amount::from_micros(10),
262            write_operation: Amount::from_micros(20),
263            byte_stored: Amount::from_nanos(10),
264            message_byte: Amount::from_nanos(100),
265            operation_byte: Amount::from_nanos(10),
266            operation: Amount::from_micros(10),
267            message: Amount::from_micros(10),
268            service_as_oracle_query: Amount::from_millis(10),
269            http_request: Amount::from_micros(50),
270            maximum_fuel_per_block: 100_000_000,
271            maximum_service_oracle_execution_ms: 10_000,
272            maximum_block_size: 1_000_000,
273            maximum_blob_size: 1_000_000,
274            maximum_published_blobs: 10,
275            maximum_bytecode_size: 10_000_000,
276            maximum_block_proposal_size: 13_000_000,
277            maximum_bytes_read_per_block: 100_000_000,
278            maximum_bytes_written_per_block: 10_000_000,
279            maximum_oracle_response_bytes: 10_000,
280            maximum_http_response_bytes: 10_000,
281            http_request_timeout_ms: 20_000,
282            http_request_allow_list: BTreeSet::new(),
283        }
284    }
285
286    pub fn block_price(&self) -> Amount {
287        self.block
288    }
289
290    pub fn total_price(&self, resources: &Resources) -> Result<Amount, ArithmeticError> {
291        let mut amount = Amount::ZERO;
292        amount.try_add_assign(self.fuel_price(resources.fuel)?)?;
293        amount.try_add_assign(self.read_operations_price(resources.read_operations)?)?;
294        amount.try_add_assign(self.write_operations_price(resources.write_operations)?)?;
295        amount.try_add_assign(self.bytes_read_price(resources.bytes_to_read as u64)?)?;
296        amount.try_add_assign(self.bytes_written_price(resources.bytes_to_write as u64)?)?;
297        amount.try_add_assign(
298            self.blob_byte_read
299                .try_mul(resources.blob_bytes_to_read as u128)?
300                .try_add(self.blob_read.try_mul(resources.blobs_to_read as u128)?)?,
301        )?;
302        amount.try_add_assign(
303            self.blob_byte_published
304                .try_mul(resources.blob_bytes_to_publish as u128)?
305                .try_add(
306                    self.blob_published
307                        .try_mul(resources.blobs_to_publish as u128)?,
308                )?,
309        )?;
310        amount.try_add_assign(self.message.try_mul(resources.messages as u128)?)?;
311        amount.try_add_assign(self.message_bytes_price(resources.message_size as u64)?)?;
312        amount.try_add_assign(self.bytes_stored_price(resources.storage_size_delta as u64)?)?;
313        amount.try_add_assign(
314            self.service_as_oracle_queries_price(resources.service_as_oracle_queries)?,
315        )?;
316        amount.try_add_assign(self.http_requests_price(resources.http_requests)?)?;
317        Ok(amount)
318    }
319
320    pub(crate) fn operation_bytes_price(&self, size: u64) -> Result<Amount, ArithmeticError> {
321        self.operation_byte.try_mul(size as u128)
322    }
323
324    pub(crate) fn message_bytes_price(&self, size: u64) -> Result<Amount, ArithmeticError> {
325        self.message_byte.try_mul(size as u128)
326    }
327
328    pub(crate) fn read_operations_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
329        self.read_operation.try_mul(count as u128)
330    }
331
332    pub(crate) fn write_operations_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
333        self.write_operation.try_mul(count as u128)
334    }
335
336    pub(crate) fn bytes_read_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
337        self.byte_read.try_mul(count as u128)
338    }
339
340    pub(crate) fn bytes_written_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
341        self.byte_written.try_mul(count as u128)
342    }
343
344    pub(crate) fn blob_read_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
345        self.blob_byte_read
346            .try_mul(count as u128)?
347            .try_add(self.blob_read)
348    }
349
350    pub(crate) fn blob_published_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
351        self.blob_byte_published
352            .try_mul(count as u128)?
353            .try_add(self.blob_published)
354    }
355
356    // TODO(#1536): This is not fully implemented.
357    #[allow(dead_code)]
358    pub(crate) fn bytes_stored_price(&self, count: u64) -> Result<Amount, ArithmeticError> {
359        self.byte_stored.try_mul(count as u128)
360    }
361
362    /// Returns how much it would cost to perform `count` queries to services running as oracles.
363    pub(crate) fn service_as_oracle_queries_price(
364        &self,
365        count: u32,
366    ) -> Result<Amount, ArithmeticError> {
367        self.service_as_oracle_query.try_mul(count as u128)
368    }
369
370    pub(crate) fn http_requests_price(&self, count: u32) -> Result<Amount, ArithmeticError> {
371        self.http_request.try_mul(count as u128)
372    }
373
374    pub(crate) fn fuel_price(&self, fuel: u64) -> Result<Amount, ArithmeticError> {
375        self.fuel_unit.try_mul(u128::from(fuel))
376    }
377
378    /// Returns how much fuel can be paid with the given balance.
379    pub(crate) fn remaining_fuel(&self, balance: Amount) -> u64 {
380        u64::try_from(balance.saturating_div(self.fuel_unit)).unwrap_or(u64::MAX)
381    }
382
383    pub fn check_blob_size(&self, content: &BlobContent) -> Result<(), ExecutionError> {
384        ensure!(
385            u64::try_from(content.bytes().len())
386                .ok()
387                .is_some_and(|size| size <= self.maximum_blob_size),
388            ExecutionError::BlobTooLarge
389        );
390        match content.blob_type() {
391            BlobType::ContractBytecode | BlobType::ServiceBytecode | BlobType::EvmBytecode => {
392                ensure!(
393                    CompressedBytecode::decompressed_size_at_most(
394                        content.bytes(),
395                        self.maximum_bytecode_size
396                    )?,
397                    ExecutionError::BytecodeTooLarge
398                );
399            }
400            BlobType::Data | BlobType::ApplicationDescription | BlobType::Committee => {}
401        }
402        Ok(())
403    }
404}