Bug fix, increase tx response soft limit (#6301)

This commit is contained in:
Emilia Hane
2024-01-30 23:17:31 +01:00
committed by GitHub
parent ff49b95d63
commit 606640285e
6 changed files with 58 additions and 46 deletions

View File

@ -16,7 +16,7 @@ use std::{
use tokio::sync::{mpsc::error::TrySendError, oneshot, oneshot::error::RecvError};
use tracing::{debug, trace};
use super::{Peer, PooledTransactions, FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT};
use super::{Peer, PooledTransactions, POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE};
/// Maximum concurrent [`GetPooledTxRequest`]s to allow per peer.
pub(super) const MAX_CONCURRENT_TX_REQUESTS_PER_PEER: u8 = 1;
@ -175,7 +175,7 @@ impl TransactionFetcher {
if let Some(size) = self.eth68_meta.peek(&hash) {
let next_acc_size = *acc_size_response + size;
if next_acc_size <= FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT {
if next_acc_size <= POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE {
// only update accumulated size of tx response if tx will fit in without exceeding
// soft limit
*acc_size_response = next_acc_size;
@ -205,7 +205,7 @@ impl TransactionFetcher {
) -> Vec<TxHash> {
if let Some(hash) = hashes.first() {
if let Some(size) = self.eth68_meta.get(hash) {
if *size >= FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT {
if *size >= POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE {
return hashes.split_off(1)
}
}
@ -222,7 +222,8 @@ impl TransactionFetcher {
hash=%hash,
size=self.eth68_meta.peek(&hash).expect("should find size in `eth68-meta`"),
acc_size_response=acc_size_response,
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT=FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE=
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE,
"no space for hash in `GetPooledTransactions` request to peer"
);
@ -233,7 +234,7 @@ impl TransactionFetcher {
// all hashes included in request and there is still space
// todo: compare free space with min tx size
if acc_size_response < FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT {
if acc_size_response < POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE {
self.fill_eth68_request_for_peer(hashes, peer_id, &mut acc_size_response);
}
@ -498,7 +499,7 @@ impl TransactionFetcher {
peer_id: PeerId,
acc_size_response: &mut usize,
) {
if *acc_size_response >= FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT {
if *acc_size_response >= POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE {
return
}
@ -522,11 +523,12 @@ impl TransactionFetcher {
let mut next_acc_size = *acc_size_response;
// 1. Check acc size against limit, if so stop looping.
if next_acc_size >= FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT {
if next_acc_size >= POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE {
trace!(target: "net::tx",
peer_id=format!("{peer_id:#}"),
acc_size_eth68_response=acc_size_response, // no change acc size
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT=FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE=
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE,
"request to peer full"
);
@ -568,7 +570,8 @@ impl TransactionFetcher {
peer_id=format!("{peer_id:#}"),
hash=%hash,
acc_size_eth68_response=acc_size_response,
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT=FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE=
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE,
"found buffered hash for request to peer"
);
}
@ -629,7 +632,8 @@ impl TransactionFetcher {
trace!(target: "net::tx",
peer_id=format!("{peer_id:#}"),
hash=%hash,
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT=FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE=
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE,
"found buffered hash for request to peer"
);
}
@ -818,12 +822,14 @@ mod test {
B256::from_slice(&[6; 32]),
];
let eth68_hashes_sizes = [
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT - 4,
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT, // this one will not fit
2, // this one will fit
3, // but now this one won't
2, /* this one will, no more txns will fit
* after this */
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE - 4,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE, // this one will not fit
2, // this one will fit
3, // but now this one won't
2, /* this one will, no more txns
* will
* fit
* after this */
1,
];

View File

@ -74,12 +74,11 @@ const PEER_TRANSACTION_CACHE_LIMIT: usize = 1024 * 10;
/// Soft limit for NewPooledTransactions
const NEW_POOLED_TRANSACTION_HASHES_SOFT_LIMIT: usize = 4096;
/// Soft limit for the message of full transactions in bytes.
const FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT: usize = 100 * 1024;
/// Softlimit for the response size of a GetPooledTransactions message (2MB)
const GET_POOLED_TRANSACTION_SOFT_LIMIT_SIZE: GetPooledTransactionLimit =
GetPooledTransactionLimit::SizeSoftLimit(2 * 1024 * 1024);
/// Soft limit for the response size of a GetPooledTransactions message (2MB) in bytes. Standard
/// maximum response size. See specs
///
/// <https://github.com/ethereum/devp2p/blob/master/caps/eth.md#protocol-messages>.
const POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE: usize = 2 * 1024 * 1024;
/// The future for inserting a function into the pool
pub type PoolImportFuture = Pin<Box<dyn Future<Output = PoolResult<TxHash>> + Send + 'static>>;
@ -300,9 +299,12 @@ where
let _ = response.send(Ok(PooledTransactions::default()));
return
}
let transactions = self
.pool
.get_pooled_transaction_elements(request.0, GET_POOLED_TRANSACTION_SOFT_LIMIT_SIZE);
let transactions = self.pool.get_pooled_transaction_elements(
request.0,
GetPooledTransactionLimit::ResponseSizeSoftLimit(
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE,
),
);
// we sent a response at which point we assume that the peer is aware of the
// transactions
@ -1112,7 +1114,7 @@ impl PropagateTransaction {
}
/// Helper type for constructing the full transaction message that enforces the
/// `FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT`
/// [`POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE`].
#[derive(Default)]
struct FullTransactionsBuilder {
total_size: usize,
@ -1125,7 +1127,7 @@ impl FullTransactionsBuilder {
/// Append a transaction to the list if it doesn't exceed the maximum target size.
fn push(&mut self, transaction: &PropagateTransaction) {
let new_size = self.total_size + transaction.size;
if new_size > FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT {
if new_size > POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE {
return
}
@ -1790,8 +1792,8 @@ mod tests {
let eth_version = EthVersion::Eth68;
let unseen_eth68_hashes = [B256::from_slice(&[1; 32]), B256::from_slice(&[2; 32])];
let unseen_eth68_hashes_sizes = [
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT / 2,
FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT / 2 - 4,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE / 2,
POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE / 2 - 4,
];
// hashes and sizes to buffer in reverse order so that seen_eth68_hashes[0] and
// seen_eth68_hashes_sizes[0] are lru
@ -1828,7 +1830,9 @@ mod tests {
let mut backups = default_cache();
backups.insert(peer_id_other);
tx_fetcher.unknown_hashes.insert(hash_other, (0, backups));
tx_fetcher.eth68_meta.insert(hash_other, FULL_TRANSACTIONS_PACKET_SIZE_SOFT_LIMIT - 2); // a big tx
tx_fetcher
.eth68_meta
.insert(hash_other, POOLED_TRANSACTIONS_RESPONSE_SOFT_LIMIT_BYTE_SIZE - 2); // a big tx
tx_fetcher.buffered_hashes.insert(hash_other);
let (peer, mut to_mock_session_rx) = new_mock_session(peer_id, eth_version);

View File

@ -1134,7 +1134,7 @@ pub enum GetPooledTransactionLimit {
/// No limit, return all transactions.
None,
/// Enforce a size limit on the returned transactions, for example 2MB
SizeSoftLimit(usize),
ResponseSizeSoftLimit(usize),
}
impl GetPooledTransactionLimit {
@ -1143,7 +1143,7 @@ impl GetPooledTransactionLimit {
pub fn exceeds(&self, size: usize) -> bool {
match self {
GetPooledTransactionLimit::None => false,
GetPooledTransactionLimit::SizeSoftLimit(limit) => size > *limit,
GetPooledTransactionLimit::ResponseSizeSoftLimit(limit) => size > *limit,
}
}
}

View File

@ -1,17 +1,17 @@
/// TX_SLOT_SIZE is used to calculate how many data slots a single transaction
/// takes up based on its size. The slots are used as DoS protection, ensuring
/// [`TX_SLOT_BYTE_SIZE`] is used to calculate how many data slots a single transaction
/// takes up based on its byte size. The slots are used as DoS protection, ensuring
/// that validating a new transaction remains a constant operation (in reality
/// O(maxslots), where max slots are 4 currently).
pub const TX_SLOT_SIZE: usize = 32 * 1024;
pub const TX_SLOT_BYTE_SIZE: usize = 32 * 1024;
/// TX_MAX_SIZE is the maximum size a single transaction can have. This field has
/// [`MAX_TX_INPUT_BYTES`] is the maximum size a single transaction can have. This field has
/// non-trivial consequences: larger transactions are significantly harder and
/// more expensive to propagate; larger transactions also take more resources
/// to validate whether they fit into the pool or not.
pub const TX_MAX_SIZE: usize = 4 * TX_SLOT_SIZE; // 128KB
pub const MAX_TX_INPUT_BYTES: usize = 4 * TX_SLOT_BYTE_SIZE; // 128KB
/// Maximum bytecode to permit for a contract
pub const MAX_CODE_SIZE: usize = 24576;
/// Maximum bytecode to permit for a contract.
pub const MAX_CODE_BYTE_SIZE: usize = 24576;
/// Maximum initcode to permit in a creation transaction and create instructions
pub const MAX_INIT_CODE_SIZE: usize = 2 * MAX_CODE_SIZE;
/// Maximum initcode to permit in a creation transaction and create instructions.
pub const MAX_INIT_CODE_BYTE_SIZE: usize = 2 * MAX_CODE_BYTE_SIZE;

View File

@ -4,7 +4,7 @@ use crate::{
blobstore::BlobStore,
error::{Eip4844PoolTransactionError, InvalidPoolTransactionError},
traits::TransactionOrigin,
validate::{ValidTransaction, ValidationTask, MAX_INIT_CODE_SIZE, TX_MAX_SIZE},
validate::{ValidTransaction, ValidationTask, MAX_INIT_CODE_BYTE_SIZE, MAX_TX_INPUT_BYTES},
EthBlobTransactionSidecar, EthPoolTransaction, LocalTransactionConfig, PoolTransaction,
TransactionValidationOutcome, TransactionValidationTaskExecutor, TransactionValidator,
};
@ -201,17 +201,17 @@ where
};
// Reject transactions over defined size to prevent DOS attacks
if transaction.size() > TX_MAX_SIZE {
if transaction.size() > MAX_TX_INPUT_BYTES {
let size = transaction.size();
return TransactionValidationOutcome::Invalid(
transaction,
InvalidPoolTransactionError::OversizedData(size, TX_MAX_SIZE),
InvalidPoolTransactionError::OversizedData(size, MAX_TX_INPUT_BYTES),
)
}
// Check whether the init code size has been exceeded.
if self.fork_tracker.is_shanghai_activated() {
if let Err(err) = ensure_max_init_code_size(&transaction, MAX_INIT_CODE_SIZE) {
if let Err(err) = ensure_max_init_code_size(&transaction, MAX_INIT_CODE_BYTE_SIZE) {
return TransactionValidationOutcome::Invalid(transaction, err)
}
}

View File

@ -22,7 +22,9 @@ pub use eth::*;
pub use task::{TransactionValidationTaskExecutor, ValidationTask};
/// Validation constants.
pub use constants::{MAX_CODE_SIZE, MAX_INIT_CODE_SIZE, TX_MAX_SIZE, TX_SLOT_SIZE};
pub use constants::{
MAX_CODE_BYTE_SIZE, MAX_INIT_CODE_BYTE_SIZE, MAX_TX_INPUT_BYTES, TX_SLOT_BYTE_SIZE,
};
/// A Result type returned after checking a transaction's validity.
#[derive(Debug)]