Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tighten security around potential P2P issues #2131

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Tighten security around potential P2P issues
([\#2131](https://github.com/anoma/namada/pull/2131))
2 changes: 1 addition & 1 deletion apps/src/lib/config/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ pub struct ImplicitAccount {
BorshDeserialize,
)]
pub struct Parameters {
// Max payload size, in bytes, for a tx batch proposal.
/// Max payload size, in bytes, for a tx batch proposal.
pub max_proposal_bytes: ProposalBytes,
/// Max block gas
pub max_block_gas: u64,
Expand Down
2 changes: 2 additions & 0 deletions apps/src/lib/config/genesis/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ impl Finalized {
fee_unshielding_descriptions_limit,
max_block_gas,
minimum_gas_price,
max_tx_bytes,
..
} = self.parameters.parameters.clone();

Expand Down Expand Up @@ -291,6 +292,7 @@ impl Finalized {
let pos_inflation_amount = 0;

namada::ledger::parameters::Parameters {
max_tx_bytes,
epoch_duration,
max_expected_time_per_block,
vp_whitelist,
Expand Down
5 changes: 5 additions & 0 deletions apps/src/lib/config/genesis/templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,9 @@ pub struct Parameters<T: TemplateValidation> {
Eq,
)]
pub struct ChainParams<T: TemplateValidation> {
/// Max payload size, in bytes, for a tx decided through
/// the consensus protocol.
pub max_tx_bytes: u32,
/// Name of the native token - this must one of the tokens from
/// `tokens.toml` file
pub native_token: Alias,
Expand Down Expand Up @@ -296,6 +299,7 @@ impl ChainParams<Unvalidated> {
tokens: &Tokens,
) -> eyre::Result<ChainParams<Validated>> {
let ChainParams {
max_tx_bytes,
native_token,
min_num_of_blocks,
max_expected_time_per_block,
Expand Down Expand Up @@ -342,6 +346,7 @@ impl ChainParams<Unvalidated> {
}

Ok(ChainParams {
max_tx_bytes,
native_token,
min_num_of_blocks,
max_expected_time_per_block,
Expand Down
55 changes: 54 additions & 1 deletion apps/src/lib/node/ledger/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ use namada::ledger::storage::{
DBIter, Sha256Hasher, Storage, StorageHasher, TempWlStorage, WlStorage, DB,
EPOCH_SWITCH_BLOCKS_DELAY,
};
use namada::ledger::storage_api::tx::validate_tx_bytes;
use namada::ledger::storage_api::{self, StorageRead};
use namada::ledger::{parameters, pos, protocol};
use namada::proof_of_stake::{self, process_slashes, read_pos_params, slash};
Expand Down Expand Up @@ -158,6 +159,7 @@ pub enum ErrorCodes {
TxGasLimit = 11,
FeeError = 12,
InvalidVoteExtension = 13,
TooLarge = 14,
}

impl ErrorCodes {
Expand All @@ -171,7 +173,8 @@ impl ErrorCodes {
Ok | WasmRuntimeError => true,
InvalidTx | InvalidSig | InvalidOrder | ExtraTxs
| Undecryptable | AllocationError | ReplayTx | InvalidChainId
| ExpiredTx | TxGasLimit | FeeError | InvalidVoteExtension => false,
| ExpiredTx | TxGasLimit | FeeError | InvalidVoteExtension
| TooLarge => false,
}
}
}
Expand Down Expand Up @@ -1069,6 +1072,18 @@ where
const VALID_MSG: &str = "Mempool validation passed";
const INVALID_MSG: &str = "Mempool validation failed";

// check tx bytes
//
// NB: always keep this as the first tx check,
// as it is a pretty cheap one
if !validate_tx_bytes(&self.wl_storage, tx_bytes.len())
.expect("Failed to get max tx bytes param from storage")
{
response.code = ErrorCodes::TooLarge.into();
response.log = format!("{INVALID_MSG}: Tx too large");
return response;
}

// Tx format check
let tx = match Tx::try_from(tx_bytes).map_err(Error::TxDecoding) {
Ok(t) => t,
Expand Down Expand Up @@ -2036,6 +2051,7 @@ mod test_utils {
.new_epoch(BlockHeight(1));
// initialize parameter storage
let params = Parameters {
max_tx_bytes: 1024 * 1024,
epoch_duration: EpochDuration {
min_num_of_blocks: 1,
min_duration: DurationSecs(3600),
Expand Down Expand Up @@ -2887,4 +2903,41 @@ mod shell_tests {
);
assert_eq!(result.code, u32::from(ErrorCodes::FeeError));
}

/// Test max tx bytes parameter in CheckTx
#[test]
fn test_max_tx_bytes_check_tx() {
let (shell, _recv, _, _) = test_utils::setup();

let max_tx_bytes: u32 = {
let key = parameters::storage::get_max_tx_bytes_key();
shell
.wl_storage
.read(&key)
.expect("Failed to read from storage")
.expect("Max tx bytes should have been written to storage")
};
let new_tx = |size: u32| {
let keypair = super::test_utils::gen_keypair();
let mut tx = Tx::new(shell.chain_id.clone(), None);
tx.add_code("wasm_code".as_bytes().to_owned())
.add_data(vec![0; size as usize])
.sign_wrapper(keypair);
tx
};

// test a small tx
let result = shell.mempool_validate(
new_tx(50).to_bytes().as_ref(),
MempoolTxType::NewTransaction,
);
assert!(result.code != u32::from(ErrorCodes::TooLarge));

// max tx bytes + 1, on the other hand, is not
let result = shell.mempool_validate(
new_tx(max_tx_bytes + 1).to_bytes().as_ref(),
MempoolTxType::NewTransaction,
);
assert_eq!(result.code, u32::from(ErrorCodes::TooLarge));
}
}
63 changes: 63 additions & 0 deletions apps/src/lib/node/ledger/shell/process_proposal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use namada::core::ledger::storage::WlStorage;
use namada::ledger::pos::PosQueries;
use namada::ledger::protocol::get_fee_unshielding_transaction;
use namada::ledger::storage::TempWlStorage;
use namada::ledger::storage_api::tx::validate_tx_bytes;
use namada::proof_of_stake::find_validator_by_raw_hash;
use namada::types::internal::TxInQueue;
use namada::types::transaction::protocol::{
Expand Down Expand Up @@ -457,6 +458,19 @@ where
where
CA: 'static + WasmCacheAccess + Sync,
{
// check tx bytes
//
// NB: always keep this as the first tx check,
// as it is a pretty cheap one
if !validate_tx_bytes(&self.wl_storage, tx_bytes.len())
.expect("Failed to get max tx bytes param from storage")
{
return TxResult {
code: ErrorCodes::TooLarge.into(),
info: "Tx too large".into(),
};
}

// try to allocate space for this tx
if let Err(e) = metadata.txs_bin.try_dump(tx_bytes) {
return TxResult {
Expand Down Expand Up @@ -2749,4 +2763,53 @@ mod test_process_proposal {
);
}
}

/// Test max tx bytes parameter in ProcessProposal
#[test]
fn test_max_tx_bytes_process_proposal() {
use namada::ledger::parameters::storage::get_max_tx_bytes_key;
let (shell, _recv, _, _) = test_utils::setup_at_height(3u64);

let max_tx_bytes: u32 = {
let key = get_max_tx_bytes_key();
shell
.wl_storage
.read(&key)
.expect("Failed to read from storage")
.expect("Max tx bytes should have been written to storage")
};
let new_tx = |size: u32| {
let keypair = super::test_utils::gen_keypair();
let mut tx = Tx::new(shell.chain_id.clone(), None);
tx.add_code("wasm_code".as_bytes().to_owned())
.add_data(vec![0; size as usize])
.sign_wrapper(keypair);
tx
};

let request = ProcessProposal {
txs: vec![new_tx(max_tx_bytes).to_bytes()],
};
match shell.process_proposal(request) {
Ok(_) => panic!("Test failed"),
Err(TestError::RejectProposal(response)) => {
assert_eq!(
response[0].result.code,
u32::from(ErrorCodes::TooLarge)
);
}
}

let request = ProcessProposal {
txs: vec![new_tx(0).to_bytes()],
};
match shell.process_proposal(request) {
Ok(_) => panic!("Test failed"),
Err(TestError::RejectProposal(response)) => {
assert!(
response[0].result.code != u32::from(ErrorCodes::TooLarge)
);
}
}
}
}
1 change: 1 addition & 0 deletions apps/src/lib/node/ledger/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ mod tests {
let mut wl_storage = WlStorage::new(WriteLog::default(), storage);
// initialize parameter storage
let params = Parameters {
max_tx_bytes: 1024 * 1024,
epoch_duration: EpochDuration {
min_num_of_blocks: 1,
min_duration: DurationSecs(3600),
Expand Down
45 changes: 34 additions & 11 deletions apps/src/lib/node/ledger/tendermint_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,22 +354,45 @@ pub fn id_from_pk(pk: &common::PublicKey) -> TendermintNodeId {

async fn update_tendermint_config(
home_dir: impl AsRef<Path>,
config: TendermintConfig,
mut config: TendermintConfig,
) -> Result<()> {
let home_dir = home_dir.as_ref();
let path = home_dir.join("config").join("config.toml");
let mut config = config.clone();

config.moniker =
Moniker::from_str(&format!("{}-{}", config.moniker, namada_version()))
.expect("Invalid moniker");

config.consensus.create_empty_blocks = true;

// We set this to true as we don't want any invalid tx be re-applied. This
// also implies that it's not possible for an invalid tx to become valid
// again in the future.
config.mempool.keep_invalid_txs_in_cache = false;
// mempool config
// https://forum.cosmos.network/t/our-understanding-of-the-cosmos-hub-mempool-issues/12040
{
// We set this to true as we don't want any invalid tx be re-applied.
// This also implies that it's not possible for an invalid tx to
// become valid again in the future.
config.mempool.keep_invalid_txs_in_cache = false;

// Drop txs from the mempool that are larger than 1 MiB
//
// The application (Namada) can assign arbitrary max tx sizes,
// which are subject to consensus. Either way, nodes are able to
// configure their local mempool however they please.
//
// 1 MiB is a reasonable value that allows governance proposal txs
// containing wasm code to be proposed by a leading validator
// during some round's start
config.mempool.max_tx_bytes = 1024 * 1024;

// Hold 50x the max amount of txs in a block
//
// 6 MiB is the default Namada max proposal size governance
// parameter -> 50 * 6 MiB
config.mempool.max_txs_bytes = 50 * 6 * 1024 * 1024;

// Hold up to 4k txs in the mempool
config.mempool.size = 4000;
}

// Bumped from the default `1_000_000`, because some WASMs can be
// quite large
Expand Down Expand Up @@ -414,11 +437,11 @@ async fn write_tm_genesis(
.try_into()
.expect("Couldn't convert DateTimeUtc to Tendermint Time");
let size = block::Size {
// maximum size of a serialized Tendermint block
// cannot go over 100 MiB
max_bytes: (100 << 20) - 1, /* unsure if we are dealing with an open
* range, so it's better to subtract one,
* here */
// maximum size of a serialized Tendermint block.
// on Namada, we have a hard-cap of 16 MiB (6 MiB max
// txs in a block + 10 MiB reserved for evidence data,
// block headers and protobuf serialization overhead)
max_bytes: 16 * 1024 * 1024,
// gas is metered app-side, so we disable it
// at the Tendermint level
max_gas: -1,
Expand Down
15 changes: 15 additions & 0 deletions core/src/ledger/parameters/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub const ADDRESS: Address = Address::Internal(InternalAddress::Parameters);
BorshSchema,
)]
pub struct Parameters {
/// Max payload size, in bytes, for a mempool tx.
pub max_tx_bytes: u32,
/// Epoch duration (read only)
pub epoch_duration: EpochDuration,
/// Maximum expected time per block (read only)
Expand Down Expand Up @@ -117,6 +119,7 @@ impl Parameters {
S: StorageRead + StorageWrite,
{
let Self {
max_tx_bytes,
epoch_duration,
max_expected_time_per_block,
max_proposal_bytes,
Expand All @@ -135,6 +138,10 @@ impl Parameters {
fee_unshielding_descriptions_limit,
} = self;

// write max tx bytes parameter
let max_tx_bytes_key = storage::get_max_tx_bytes_key();
storage.write(&max_tx_bytes_key, max_tx_bytes)?;

// write max proposal bytes parameter
let max_proposal_bytes_key = storage::get_max_proposal_bytes_key();
storage.write(&max_proposal_bytes_key, max_proposal_bytes)?;
Expand Down Expand Up @@ -542,7 +549,15 @@ where
.ok_or(ReadError::ParametersMissing)
.into_storage_result()?;

// read max tx bytes
let max_tx_bytes_key = storage::get_max_tx_bytes_key();
let value = storage.read(&max_tx_bytes_key)?;
let max_tx_bytes = value
.ok_or(ReadError::ParametersMissing)
.into_storage_result()?;

Ok(Parameters {
max_tx_bytes,
epoch_duration,
max_expected_time_per_block,
max_proposal_bytes,
Expand Down
11 changes: 11 additions & 0 deletions core/src/ledger/parameters/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ struct Keys {
tx_whitelist: &'static str,
vp_whitelist: &'static str,
max_proposal_bytes: &'static str,
max_tx_bytes: &'static str,
max_block_gas: &'static str,
minimum_gas_price: &'static str,
fee_unshielding_gas_limit: &'static str,
Expand Down Expand Up @@ -120,6 +121,11 @@ pub fn is_max_proposal_bytes_key(key: &Key) -> bool {
is_max_proposal_bytes_key_at_addr(key, &ADDRESS)
}

/// Returns if the key is the max tx bytes key.
pub fn is_max_tx_bytes_key(key: &Key) -> bool {
is_max_tx_bytes_key_at_addr(key, &ADDRESS)
}

/// Storage key used for epoch parameter.
pub fn get_epoch_duration_storage_key() -> Key {
get_epoch_duration_key_at_addr(ADDRESS)
Expand Down Expand Up @@ -185,6 +191,11 @@ pub fn get_max_proposal_bytes_key() -> Key {
get_max_proposal_bytes_key_at_addr(ADDRESS)
}

/// Storage key used for the max tx bytes.
pub fn get_max_tx_bytes_key() -> Key {
get_max_tx_bytes_key_at_addr(ADDRESS)
}

/// Storage key used for the max block gas.
pub fn get_max_block_gas_key() -> Key {
get_max_block_gas_key_at_addr(ADDRESS)
Expand Down
1 change: 1 addition & 0 deletions core/src/ledger/storage/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1315,6 +1315,7 @@ mod tests {
..Default::default()
};
let mut parameters = Parameters {
max_tx_bytes: 1024 * 1024,
max_proposal_bytes: Default::default(),
max_block_gas: 20_000_000,
epoch_duration: epoch_duration.clone(),
Expand Down
Loading
Loading