Skip to content

Commit

Permalink
Add a block size limit (#2454)
Browse files Browse the repository at this point in the history
* Add a block size limit

* Track exact size instead of checking again at the end.

* Also print maximum block size, and don't make an operation if everything is None.

* Return early if delta == 0.
  • Loading branch information
afck authored Sep 6, 2024
1 parent 9a3a20e commit 43d7bff
Show file tree
Hide file tree
Showing 14 changed files with 231 additions and 18 deletions.
2 changes: 2 additions & 0 deletions CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ View or update the resource control policy
* `--operation-byte <OPERATION_BYTE>` — Set the additional price for each byte in the argument of a user operation
* `--message <MESSAGE>` — Set the base price of sending a message from a block..
* `--message-byte <MESSAGE_BYTE>` — Set the additional price for each byte in the argument of a user message
* `--maximum-executed-block-size <MAXIMUM_EXECUTED_BLOCK_SIZE>` — Set the maximum size of an executed block
* `--maximum-bytes-read-per-block <MAXIMUM_BYTES_READ_PER_BLOCK>` — Set the maximum read data per block
* `--maximum-bytes-written-per-block <MAXIMUM_BYTES_WRITTEN_PER_BLOCK>` — Set the maximum write data per block

Expand Down Expand Up @@ -458,6 +459,7 @@ Create genesis configuration for a Linera deployment. Create initial user chains
* `--message-byte-price <MESSAGE_BYTE_PRICE>` — Set the additional price for each byte in the argument of a user message

Default value: `0`
* `--maximum-executed-block-size <MAXIMUM_EXECUTED_BLOCK_SIZE>` — Set the maximum size of an executed block
* `--maximum-bytes-read-per-block <MAXIMUM_BYTES_READ_PER_BLOCK>` — Set the maximum read data per block
* `--maximum-bytes-written-per-block <MAXIMUM_BYTES_WRITTEN_PER_BLOCK>` — Set the maximum write data per block
* `--testing-prng-seed <TESTING_PRNG_SEED>` — Force this wallet to generate keys using a PRNG and a given seed. USE FOR TESTING ONLY
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions linera-chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ tracing.workspace = true

[dev-dependencies]
assert_matches.workspace = true
bcs.workspace = true
linera-chain = { path = ".", features = ["test"] }

[build-dependencies]
Expand Down
47 changes: 45 additions & 2 deletions linera-chain/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ static STATE_HASH_COMPUTATION_LATENCY: LazyLock<HistogramVec> = LazyLock::new(||
.expect("Histogram can be created")
});

/// The BCS-serialized size of an empty `ExecutedBlock`.
const EMPTY_EXECUTED_BLOCK_SIZE: usize = 91;

/// An origin, cursor and timestamp of a unskippable bundle in our inbox.
#[derive(Debug, Clone, Serialize, Deserialize, async_graphql::SimpleObject)]
pub struct TimestampedBundleInInbox {
Expand Down Expand Up @@ -732,6 +735,17 @@ where
tracker: ResourceTracker::default(),
account: block.authenticated_signer,
};
resource_controller
.track_executed_block_size(EMPTY_EXECUTED_BLOCK_SIZE)
.and_then(|()| {
resource_controller
.track_executed_block_size_sequence_extension(0, block.incoming_bundles.len())
})
.and_then(|()| {
resource_controller
.track_executed_block_size_sequence_extension(0, block.operations.len())
})
.map_err(|err| ChainError::ExecutionError(err, ChainExecutionContext::Block))?;

if self.is_closed() {
ensure!(
Expand Down Expand Up @@ -789,6 +803,9 @@ where
let mut txn_tracker = TransactionTracker::new(next_message_index, maybe_responses);
match transaction {
Transaction::ReceiveMessages(incoming_bundle) => {
resource_controller
.track_executed_block_size_of(&incoming_bundle)
.map_err(with_context)?;
for (message_id, posted_message) in incoming_bundle.messages_and_ids() {
self.execute_message_in_block(
message_id,
Expand All @@ -804,6 +821,9 @@ where
}
}
Transaction::ExecuteOperation(operation) => {
resource_controller
.track_executed_block_size_of(&operation)
.map_err(with_context)?;
#[cfg(with_metrics)]
let _operation_latency = OPERATION_EXECUTION_LATENCY.measure_latency();
let context = OperationContext {
Expand Down Expand Up @@ -857,6 +877,18 @@ where
.map_err(with_context)?;
}
}
resource_controller
.track_executed_block_size_of(&(&txn_oracle_responses, &txn_messages, &txn_events))
.map_err(with_context)?;
resource_controller
.track_executed_block_size_sequence_extension(oracle_responses.len(), 1)
.map_err(with_context)?;
resource_controller
.track_executed_block_size_sequence_extension(messages.len(), 1)
.map_err(with_context)?;
resource_controller
.track_executed_block_size_sequence_extension(events.len(), 1)
.map_err(with_context)?;
oracle_responses.push(txn_oracle_responses);
messages.push(txn_messages);
events.push(txn_events);
Expand Down Expand Up @@ -910,12 +942,13 @@ where
messages.len(),
block.incoming_bundles.len() + block.operations.len()
);
Ok(BlockExecutionOutcome {
let outcome = BlockExecutionOutcome {
messages,
state_hash,
oracle_responses,
events,
})
};
Ok(outcome)
}

/// Executes a message as part of an incoming bundle in a block.
Expand Down Expand Up @@ -1240,3 +1273,13 @@ where
Ok(())
}
}

#[test]
fn empty_executed_block_size() {
let executed_block = crate::data_types::ExecutedBlock {
block: crate::test::make_first_block(ChainId::root(0)),
outcome: crate::data_types::BlockExecutionOutcome::default(),
};
let size = bcs::serialized_size(&executed_block).unwrap();
assert_eq!(size, EMPTY_EXECUTED_BLOCK_SIZE);
}
89 changes: 83 additions & 6 deletions linera-chain/src/unit_tests/chain_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

#![allow(clippy::large_futures)]

use std::{iter, sync::Arc};
use std::{collections::BTreeMap, iter, sync::Arc};

use assert_matches::assert_matches;
use linera_base::{
Expand All @@ -15,11 +15,11 @@ use linera_base::{
ownership::ChainOwnership,
};
use linera_execution::{
committee::{Committee, Epoch},
system::OpenChainConfig,
committee::{Committee, Epoch, ValidatorName, ValidatorState},
system::{OpenChainConfig, Recipient, UserData},
test_utils::{ExpectedCall, MockApplication},
ExecutionRuntimeConfig, ExecutionRuntimeContext, Message, MessageKind, Operation,
SystemMessage, TestExecutionRuntimeContext,
ExecutionError, ExecutionRuntimeConfig, ExecutionRuntimeContext, Message, MessageKind,
Operation, ResourceControlPolicy, SystemMessage, SystemOperation, TestExecutionRuntimeContext,
};
use linera_views::{
context::{Context as _, MemoryContext},
Expand All @@ -31,7 +31,7 @@ use linera_views::{
use crate::{
data_types::{HashedCertificateValue, IncomingBundle, MessageAction, MessageBundle, Origin},
test::{make_child_block, make_first_block, BlockTestExt, MessageTestExt},
ChainError, ChainStateView,
ChainError, ChainExecutionContext, ChainStateView,
};

impl ChainStateView<MemoryContext<TestExecutionRuntimeContext>>
Expand Down Expand Up @@ -94,6 +94,83 @@ fn make_open_chain_config() -> OpenChainConfig {
}
}

#[tokio::test]
async fn test_block_size_limit() {
let time = Timestamp::from(0);
let message_id = make_admin_message_id(BlockHeight(3));
let chain_id = ChainId::child(message_id);
let mut chain = ChainStateView::new(chain_id).await;

// The size of the executed valid block below.
let maximum_executed_block_size = 667;

// Initialize the chain.
let mut config = make_open_chain_config();
config.committees.insert(
Epoch(0),
Committee::new(
BTreeMap::from([(
ValidatorName(PublicKey::test_key(1)),
ValidatorState {
network_address: PublicKey::test_key(1).to_string(),
votes: 1,
},
)]),
ResourceControlPolicy {
maximum_executed_block_size,
..ResourceControlPolicy::default()
},
),
);

chain
.execute_init_message(message_id, &config, time, time)
.await
.unwrap();
let open_chain_bundle = IncomingBundle {
origin: Origin::chain(admin_id()),
bundle: MessageBundle {
certificate_hash: CryptoHash::test_hash("certificate"),
height: BlockHeight(1),
transaction_index: 0,
timestamp: time,
messages: vec![Message::System(SystemMessage::OpenChain(config))
.to_posted(0, MessageKind::Protected)],
},
action: MessageAction::Accept,
};

let valid_block = make_first_block(chain_id).with_incoming_bundle(open_chain_bundle.clone());

// Any block larger than the valid block is rejected.
let invalid_block = valid_block
.clone()
.with_operation(SystemOperation::Transfer {
owner: None,
recipient: Recipient::root(0),
amount: Amount::ONE,
user_data: UserData::default(),
});
let result = chain.execute_block(&invalid_block, time, None).await;
assert_matches!(
result,
Err(ChainError::ExecutionError(
ExecutionError::ExecutedBlockTooLarge,
ChainExecutionContext::Operation(1),
))
);

// The valid block is accepted...
let outcome = chain.execute_block(&valid_block, time, None).await.unwrap();
let executed_block = outcome.with(valid_block);

// ...because its size is exactly at the allowed limit.
assert_eq!(
bcs::serialized_size(&executed_block).unwrap(),
maximum_executed_block_size as usize
);
}

#[tokio::test]
async fn test_application_permissions() {
let time = Timestamp::from(0);
Expand Down
8 changes: 8 additions & 0 deletions linera-client/src/client_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ pub enum ClientCommand {
#[arg(long)]
message_byte: Option<Amount>,

/// Set the maximum size of an executed block.
#[arg(long)]
maximum_executed_block_size: Option<u64>,

/// Set the maximum read data per block.
#[arg(long)]
maximum_bytes_read_per_block: Option<u64>,
Expand Down Expand Up @@ -617,6 +621,10 @@ pub enum ClientCommand {
#[arg(long, default_value = "0")]
message_byte_price: Amount,

/// Set the maximum size of an executed block.
#[arg(long)]
maximum_executed_block_size: Option<u64>,

/// Set the maximum read data per block.
#[arg(long)]
maximum_bytes_read_per_block: Option<u64>,
Expand Down
4 changes: 4 additions & 0 deletions linera-execution/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ pub enum ExecutionError {
ExcessiveRead,
#[error("Excessive number of bytes written to storage")]
ExcessiveWrite,
#[error("Serialized size of the executed block exceeds limit")]
ExecutedBlockTooLarge,
#[error("Runtime failed to respond to application")]
MissingRuntimeResponse,
#[error("Bytecode ID {0:?} is invalid")]
Expand All @@ -164,6 +166,8 @@ pub enum ExecutionError {
UnexpectedOracleResponse,
#[error("Invalid JSON: {}", .0)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Bcs(#[from] bcs::Error),
#[error("Recorded response for oracle query has the wrong type")]
OracleResponseMismatch,
#[error("Assertion failed: local time {local_time} is not earlier than {timestamp}")]
Expand Down
5 changes: 5 additions & 0 deletions linera-execution/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ pub struct ResourceControlPolicy {

// TODO(#1538): Cap the number of transactions per block and the total size of their
// arguments.
/// The maximum size of an executed block. This includes the block proposal itself as well as
/// the execution outcome.
pub maximum_executed_block_size: u64,
/// The maximum data to read per block
pub maximum_bytes_read_per_block: u64,
/// The maximum data to write per block
Expand All @@ -56,6 +59,7 @@ impl Default for ResourceControlPolicy {
operation_byte: Amount::default(),
message: Amount::default(),
message_byte: Amount::default(),
maximum_executed_block_size: u64::MAX,
maximum_bytes_read_per_block: u64::MAX,
maximum_bytes_written_per_block: u64::MAX,
}
Expand Down Expand Up @@ -173,6 +177,7 @@ impl ResourceControlPolicy {
operation_byte: Amount::from_nanos(10),
operation: Amount::from_micros(10),
message: Amount::from_micros(10),
maximum_executed_block_size: 1_000_000,
maximum_bytes_read_per_block: 100_000_000,
maximum_bytes_written_per_block: 10_000_000,
}
Expand Down
52 changes: 52 additions & 0 deletions linera-execution/src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ use std::sync::Arc;
use custom_debug_derive::Debug;
use linera_base::{
data_types::{Amount, ArithmeticError},
ensure,
identifiers::Owner,
};
use linera_views::{context::Context, views::ViewError};
use serde::Serialize;

use crate::{
system::SystemExecutionError, ExecutionError, ExecutionStateView, Message, Operation,
Expand All @@ -32,6 +34,8 @@ pub struct ResourceController<Account = Amount, Tracker = ResourceTracker> {
pub struct ResourceTracker {
/// The number of blocks created.
pub blocks: u32,
/// The total size of the executed block so far.
pub executed_block_size: u64,
/// The fuel used so far.
pub fuel: u64,
/// The number of read operations.
Expand Down Expand Up @@ -253,6 +257,54 @@ where
}
}

impl<Account, Tracker> ResourceController<Account, Tracker>
where
Tracker: AsMut<ResourceTracker>,
{
/// Tracks the extension of a sequence in an executed block.
///
/// The sequence length is ULEB128-encoded, so extending a sequence can add an additional byte.
pub fn track_executed_block_size_sequence_extension(
&mut self,
old_len: usize,
delta: usize,
) -> Result<(), ExecutionError> {
if delta == 0 {
return Ok(());
}
let new_len = old_len + delta;
// ULEB128 uses one byte per 7 bits of the number. It always uses at least one byte.
let old_size = ((usize::BITS - old_len.leading_zeros()) / 7).max(1);
let new_size = ((usize::BITS - new_len.leading_zeros()) / 7).max(1);
if new_size > old_size {
self.track_executed_block_size((new_size - old_size) as usize)?;
}
Ok(())
}

/// Tracks the serialized size of an executed block, or parts of it.
pub fn track_executed_block_size_of(
&mut self,
data: &impl Serialize,
) -> Result<(), ExecutionError> {
self.track_executed_block_size(bcs::serialized_size(data)?)
}

/// Tracks the serialized size of an executed block, or parts of it.
pub fn track_executed_block_size(&mut self, size: usize) -> Result<(), ExecutionError> {
let tracker = self.tracker.as_mut();
tracker.executed_block_size = u64::try_from(size)
.ok()
.and_then(|size| tracker.executed_block_size.checked_add(size))
.ok_or(ExecutionError::ExecutedBlockTooLarge)?;
ensure!(
tracker.executed_block_size <= self.policy.maximum_executed_block_size,
ExecutionError::ExecutedBlockTooLarge
);
Ok(())
}
}

// The simplest `BalanceHolder` is an `Amount`.
impl BalanceHolder for Amount {
fn balance(&self) -> Result<Amount, ArithmeticError> {
Expand Down
Loading

0 comments on commit 43d7bff

Please sign in to comment.