Skip to content

Commit

Permalink
Adds replay protection checks in prepare_proposal
Browse files Browse the repository at this point in the history
  • Loading branch information
grarco committed May 19, 2023
1 parent d425dd5 commit ae79f1b
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 62 deletions.
58 changes: 55 additions & 3 deletions apps/src/lib/node/ledger/shell/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use std::path::{Path, PathBuf};
use std::rc::Rc;

use borsh::{BorshDeserialize, BorshSerialize};
use namada::core::types::hash::Hash;
use namada::ledger::events::log::EventLog;
use namada::ledger::events::Event;
use namada::ledger::gas::BlockGasMeter;
Expand All @@ -30,9 +31,9 @@ use namada::ledger::pos::namada_proof_of_stake::types::{
};
use namada::ledger::storage::write_log::WriteLog;
use namada::ledger::storage::{
DBIter, Sha256Hasher, Storage, StorageHasher, WlStorage, DB,
DBIter, Sha256Hasher, Storage, StorageHasher, TempWlStorage, WlStorage, DB,
};
use namada::ledger::storage_api::{self, StorageRead};
use namada::ledger::storage_api::{self, StorageRead, StorageWrite};
use namada::ledger::{ibc, pos, protocol, replay_protection};
use namada::proof_of_stake::{self, read_pos_params, slash};
use namada::proto::{self, Tx};
Expand All @@ -47,7 +48,7 @@ use namada::types::token::{self};
use namada::types::transaction::MIN_FEE;
use namada::types::transaction::{
hash_tx, process_tx, verify_decrypted_correctly, AffineCurve, DecryptedTx,
EllipticCurve, PairingEngine, TxType,
EllipticCurve, PairingEngine, TxType, WrapperTx,
};
use namada::types::{address, hash};
use namada::vm::wasm::{TxCache, VpCache};
Expand Down Expand Up @@ -112,6 +113,8 @@ pub enum Error {
LoadingWasm(String),
#[error("Error reading from or writing to storage: {0}")]
StorageApi(#[from] storage_api::Error),
#[error("Transaction replay attempt: {0}")]
ReplayAttempt(String),
}

impl From<Error> for TxResult {
Expand Down Expand Up @@ -648,6 +651,55 @@ where
response
}

/// Checks that neither the wrapper nor the inner transaction have already
/// been applied. Requires a [`TempWlStorage`] to perform the check during
/// block construction and validation
pub fn replay_protection_checks(
&self,
wrapper: &WrapperTx,
tx_bytes: &[u8],
temp_wl_storage: &mut TempWlStorage<D, H>,
) -> Result<()> {
let inner_hash_key =
replay_protection::get_tx_hash_key(&wrapper.tx_hash);
if temp_wl_storage
.has_key(&inner_hash_key)
.expect("Error while checking inner tx hash key in storage")
{
return Err(Error::ReplayAttempt(format!(
"Inner transaction hash {} already in storage",
&wrapper.tx_hash
)));
}

// Write inner hash to WAL
temp_wl_storage
.write(&inner_hash_key, ())
.expect("Couldn't write inner transaction hash to write log");

let tx =
Tx::try_from(tx_bytes).expect("Deserialization shouldn't fail");
let wrapper_hash = Hash(tx.unsigned_hash());
let wrapper_hash_key =
replay_protection::get_tx_hash_key(&wrapper_hash);
if temp_wl_storage
.has_key(&wrapper_hash_key)
.expect("Error while checking wrapper tx hash key in storage")
{
return Err(Error::ReplayAttempt(format!(
"Wrapper transaction hash {} already in storage",
wrapper_hash
)));
}

// Write wrapper hash to WAL
temp_wl_storage
.write(&wrapper_hash_key, ())
.expect("Couldn't write wrapper tx hash to write log");

Ok(())
}

/// Validate a transaction request. On success, the transaction will
/// included in the mempool and propagated to peers, otherwise it will be
/// rejected.
Expand Down
239 changes: 233 additions & 6 deletions apps/src/lib/node/ledger/shell/prepare_proposal.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Implementation of the [`RequestPrepareProposal`] ABCI++ method for the Shell
use namada::core::hints;
use namada::ledger::storage::{DBIter, StorageHasher, DB};
use namada::ledger::storage::{DBIter, StorageHasher, TempWlStorage, DB};
use namada::proof_of_stake::pos_queries::PosQueries;
use namada::proto::Tx;
use namada::types::internal::WrapperTxInQueue;
Expand Down Expand Up @@ -47,8 +47,12 @@ where
let alloc = self.get_encrypted_txs_allocator();

// add encrypted txs
let (encrypted_txs, alloc) =
self.build_encrypted_txs(alloc, &req.txs, &req.time);
let (encrypted_txs, alloc) = self.build_encrypted_txs(
alloc,
TempWlStorage::new(&self.wl_storage.storage),
&req.txs,
&req.time,
);
let mut txs = encrypted_txs;

// decrypt the wrapper txs included in the previous block
Expand Down Expand Up @@ -119,6 +123,7 @@ where
fn build_encrypted_txs(
&self,
mut alloc: EncryptedTxBatchAllocator,
mut temp_wl_storage: TempWlStorage<D, H>,
txs: &[TxBytes],
block_time: &Option<Timestamp>,
) -> (Vec<TxBytes>, BlockSpaceAllocator<BuildingDecryptedTxBatch>) {
Expand All @@ -138,8 +143,10 @@ where
if let (Some(block_time), Some(exp)) = (block_time.as_ref(), &tx.expiration) {
if block_time > exp { return None }
}
if let Ok(TxType::Wrapper(_)) = process_tx(tx) {
return Some(tx_bytes.clone());
if let Ok(TxType::Wrapper(ref wrapper)) = process_tx(tx) {
if self.replay_protection_checks(wrapper, tx_bytes.as_slice(), &mut temp_wl_storage).is_ok() {
return Some(tx_bytes.clone())
}
}
}
None
Expand Down Expand Up @@ -271,7 +278,9 @@ where
mod test_prepare_proposal {

use borsh::BorshSerialize;
use namada::ledger::replay_protection;
use namada::proof_of_stake::Epoch;
use namada::types::hash::Hash;
use namada::types::transaction::{Fee, WrapperTx};

use super::*;
Expand Down Expand Up @@ -412,6 +421,224 @@ mod test_prepare_proposal {
assert_eq!(received, expected_txs);
}

/// Test that if the unsigned wrapper tx hash is known (replay attack), the
/// transaction is not included in the block
#[test]
fn test_wrapper_tx_hash() {
let (mut shell, _) = test_utils::setup(1);

let keypair = crate::wallet::defaults::daewon_keypair();

let tx = Tx::new(
"wasm_code".as_bytes().to_owned(),
Some("transaction data".as_bytes().to_owned()),
shell.chain_id.clone(),
None,
);
let wrapper = WrapperTx::new(
Fee {
amount: 0.into(),
token: shell.wl_storage.storage.native_token.clone(),
},
&keypair,
Epoch(0),
0.into(),
tx,
Default::default(),
#[cfg(not(feature = "mainnet"))]
None,
);
let signed = wrapper
.sign(&keypair, shell.chain_id.clone(), None)
.expect("Test failed");

// Write wrapper hash to storage
let wrapper_unsigned_hash = Hash(signed.unsigned_hash());
let hash_key =
replay_protection::get_tx_hash_key(&wrapper_unsigned_hash);
shell
.wl_storage
.storage
.write(&hash_key, vec![])
.expect("Test failed");

let req = RequestPrepareProposal {
txs: vec![signed.to_bytes()],
..Default::default()
};

let received =
shell.prepare_proposal(req).txs.into_iter().map(|tx_bytes| {
Tx::try_from(tx_bytes.as_slice())
.expect("Test failed")
.data
.expect("Test failed")
});
assert_eq!(received.len(), 0);
}

/// Test that if two identical wrapper txs are proposed for this block, only
/// one gets accepted
#[test]
fn test_wrapper_tx_hash_same_block() {
let (shell, _) = test_utils::setup(1);

let keypair = crate::wallet::defaults::daewon_keypair();

let tx = Tx::new(
"wasm_code".as_bytes().to_owned(),
Some("transaction data".as_bytes().to_owned()),
shell.chain_id.clone(),
None,
);
let wrapper = WrapperTx::new(
Fee {
amount: 0.into(),
token: shell.wl_storage.storage.native_token.clone(),
},
&keypair,
Epoch(0),
0.into(),
tx,
Default::default(),
#[cfg(not(feature = "mainnet"))]
None,
);
let signed = wrapper
.sign(&keypair, shell.chain_id.clone(), None)
.expect("Test failed");
let req = RequestPrepareProposal {
txs: vec![signed.to_bytes(); 2],
..Default::default()
};
let received =
shell.prepare_proposal(req).txs.into_iter().map(|tx_bytes| {
Tx::try_from(tx_bytes.as_slice())
.expect("Test failed")
.data
.expect("Test failed")
});
assert_eq!(received.len(), 1);
}

/// Test that if the unsigned inner tx hash is known (replay attack), the
/// transaction is not included in the block
#[test]
fn test_inner_tx_hash() {
let (mut shell, _) = test_utils::setup(1);

let keypair = crate::wallet::defaults::daewon_keypair();

let tx = Tx::new(
"wasm_code".as_bytes().to_owned(),
Some("transaction data".as_bytes().to_owned()),
shell.chain_id.clone(),
None,
);
let wrapper = WrapperTx::new(
Fee {
amount: 0.into(),
token: shell.wl_storage.storage.native_token.clone(),
},
&keypair,
Epoch(0),
0.into(),
tx,
Default::default(),
#[cfg(not(feature = "mainnet"))]
None,
);
let inner_unsigned_hash = wrapper.tx_hash.clone();
let signed = wrapper
.sign(&keypair, shell.chain_id.clone(), None)
.expect("Test failed");

// Write inner hash to storage
let hash_key = replay_protection::get_tx_hash_key(&inner_unsigned_hash);
shell
.wl_storage
.storage
.write(&hash_key, vec![])
.expect("Test failed");

let req = RequestPrepareProposal {
txs: vec![signed.to_bytes()],
..Default::default()
};

let received =
shell.prepare_proposal(req).txs.into_iter().map(|tx_bytes| {
Tx::try_from(tx_bytes.as_slice())
.expect("Test failed")
.data
.expect("Test failed")
});
assert_eq!(received.len(), 0);
}

/// Test that if two identical decrypted txs are proposed for this block,
/// only one gets accepted
#[test]
fn test_inner_tx_hash_same_block() {
let (shell, _) = test_utils::setup(1);

let keypair = crate::wallet::defaults::daewon_keypair();
let keypair_2 = crate::wallet::defaults::daewon_keypair();

let tx = Tx::new(
"wasm_code".as_bytes().to_owned(),
Some("transaction data".as_bytes().to_owned()),
shell.chain_id.clone(),
None,
);
let wrapper = WrapperTx::new(
Fee {
amount: 0.into(),
token: shell.wl_storage.storage.native_token.clone(),
},
&keypair,
Epoch(0),
0.into(),
tx.clone(),
Default::default(),
#[cfg(not(feature = "mainnet"))]
None,
);
let signed = wrapper
.sign(&keypair, shell.chain_id.clone(), None)
.expect("Test failed");

let new_wrapper = WrapperTx::new(
Fee {
amount: 0.into(),
token: shell.wl_storage.storage.native_token.clone(),
},
&keypair_2,
Epoch(0),
0.into(),
tx,
Default::default(),
#[cfg(not(feature = "mainnet"))]
None,
);
let new_signed = new_wrapper
.sign(&keypair, shell.chain_id.clone(), None)
.expect("Test failed");

let req = RequestPrepareProposal {
txs: vec![signed.to_bytes(), new_signed.to_bytes()],
..Default::default()
};
let received =
shell.prepare_proposal(req).txs.into_iter().map(|tx_bytes| {
Tx::try_from(tx_bytes.as_slice())
.expect("Test failed")
.data
.expect("Test failed")
});
assert_eq!(received.len(), 1);
}

/// Test that expired wrapper transactions are not included in the block
#[test]
fn test_expired_wrapper_tx() {
Expand Down Expand Up @@ -455,6 +682,6 @@ mod test_prepare_proposal {
};
let result = shell.prepare_proposal(req);
eprintln!("Proposal: {:?}", result.txs);
assert!(result.txs.is_empty());
assert_eq!(result.txs.len(), 0);
}
}
Loading

0 comments on commit ae79f1b

Please sign in to comment.