Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit 77d1671

Browse files
shawntabriziathei
authored andcommitted
Add Limit to Tranasctional Layers (paritytech#10808)
* introduce hard limit to transactional * add single layer transactional * remove single_transactional * Update mod.rs * add tests * maybe fix contracts cc @athei * fmt * fix contract logic * Update frame/contracts/src/exec.rs Co-authored-by: Alexander Theißen <[email protected]> * Update exec.rs * add unchecked and custom errors * Update lib.rs * Apply suggestions from code review Co-authored-by: Alexander Theißen <[email protected]> * Replace storage access by atomics Co-authored-by: Alexander Theißen <[email protected]>
1 parent b2637f3 commit 77d1671

File tree

5 files changed

+273
-109
lines changed

5 files changed

+273
-109
lines changed

frame/contracts/src/exec.rs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -774,14 +774,27 @@ where
774774

775775
// All changes performed by the contract are executed under a storage transaction.
776776
// This allows for roll back on error. Changes to the cached contract_info are
777-
// comitted or rolled back when popping the frame.
778-
let (success, output) = with_transaction(|| {
779-
let output = do_transaction();
780-
match &output {
781-
Ok(result) if !result.did_revert() => TransactionOutcome::Commit((true, output)),
782-
_ => TransactionOutcome::Rollback((false, output)),
783-
}
784-
});
777+
// committed or rolled back when popping the frame.
778+
//
779+
// `with_transactional` may return an error caused by a limit in the
780+
// transactional storage depth.
781+
let transaction_outcome =
782+
with_transaction(|| -> TransactionOutcome<Result<_, DispatchError>> {
783+
let output = do_transaction();
784+
match &output {
785+
Ok(result) if !result.did_revert() =>
786+
TransactionOutcome::Commit(Ok((true, output))),
787+
_ => TransactionOutcome::Rollback(Ok((false, output))),
788+
}
789+
});
790+
791+
let (success, output) = match transaction_outcome {
792+
// `with_transactional` executed successfully, and we have the expected output.
793+
Ok((success, output)) => (success, output),
794+
// `with_transactional` returned an error, and we propagate that error and note no state
795+
// has changed.
796+
Err(error) => (false, Err(error.into())),
797+
};
785798
self.pop_frame(success);
786799
output
787800
}

frame/support/procedural/src/transactional.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ pub fn require_transactional(_attr: TokenStream, input: TokenStream) -> Result<T
4949
let output = quote! {
5050
#(#attrs)*
5151
#vis #sig {
52-
#crate_::storage::require_transaction();
52+
if !#crate_::storage::is_transactional() {
53+
return Err(#crate_::sp_runtime::TransactionalError::NoLayer.into());
54+
}
5355
#block
5456
}
5557
};

frame/support/src/storage/mod.rs

Lines changed: 152 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,11 @@ use crate::{
2727
};
2828
use codec::{Decode, Encode, EncodeLike, FullCodec, FullEncode};
2929
use sp_core::storage::ChildInfo;
30-
use sp_runtime::generic::{Digest, DigestItem};
3130
pub use sp_runtime::TransactionOutcome;
31+
use sp_runtime::{
32+
generic::{Digest, DigestItem},
33+
DispatchError, TransactionalError,
34+
};
3235
use sp_std::prelude::*;
3336
pub use types::Key;
3437

@@ -44,71 +47,121 @@ pub mod types;
4447
pub mod unhashed;
4548
pub mod weak_bounded_vec;
4649

47-
#[cfg(all(feature = "std", any(test, debug_assertions)))]
48-
mod debug_helper {
49-
use std::cell::RefCell;
50+
mod transaction_level_tracker {
51+
use core::sync::atomic::{AtomicU32, Ordering};
52+
53+
type Layer = u32;
54+
static NUM_LEVELS: AtomicU32 = AtomicU32::new(0);
55+
const TRANSACTIONAL_LIMIT: Layer = 255;
5056

51-
thread_local! {
52-
static TRANSACTION_LEVEL: RefCell<u32> = RefCell::new(0);
57+
pub fn get_transaction_level() -> Layer {
58+
NUM_LEVELS.load(Ordering::SeqCst)
5359
}
5460

55-
pub fn require_transaction() {
56-
let level = TRANSACTION_LEVEL.with(|v| *v.borrow());
57-
if level == 0 {
58-
panic!("Require transaction not called within with_transaction");
59-
}
61+
/// Increments the transaction level. Returns an error if levels go past the limit.
62+
///
63+
/// Returns a guard that when dropped decrements the transaction level automatically.
64+
pub fn inc_transaction_level() -> Result<StorageLayerGuard, ()> {
65+
NUM_LEVELS
66+
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |existing_levels| {
67+
if existing_levels >= TRANSACTIONAL_LIMIT {
68+
return None
69+
}
70+
// Cannot overflow because of check above.
71+
Some(existing_levels + 1)
72+
})
73+
.map_err(|_| ())?;
74+
Ok(StorageLayerGuard)
6075
}
6176

62-
pub struct TransactionLevelGuard;
77+
fn dec_transaction_level() {
78+
NUM_LEVELS
79+
.fetch_update(Ordering::SeqCst, Ordering::SeqCst, |existing_levels| {
80+
if existing_levels == 0 {
81+
log::warn!(
82+
"We are underflowing with calculating transactional levels. Not great, but let's not panic...",
83+
);
84+
None
85+
} else {
86+
// Cannot underflow because of checks above.
87+
Some(existing_levels - 1)
88+
}
89+
})
90+
.ok();
91+
}
6392

64-
impl Drop for TransactionLevelGuard {
65-
fn drop(&mut self) {
66-
TRANSACTION_LEVEL.with(|v| *v.borrow_mut() -= 1);
67-
}
93+
pub fn is_transactional() -> bool {
94+
get_transaction_level() > 0
6895
}
6996

70-
/// Increments the transaction level.
71-
///
72-
/// Returns a guard that when dropped decrements the transaction level automatically.
73-
pub fn inc_transaction_level() -> TransactionLevelGuard {
74-
TRANSACTION_LEVEL.with(|v| {
75-
let mut val = v.borrow_mut();
76-
*val += 1;
77-
if *val > 10 {
78-
log::warn!(
79-
"Detected with_transaction with nest level {}. Nested usage of with_transaction is not recommended.",
80-
*val
81-
);
82-
}
83-
});
97+
pub struct StorageLayerGuard;
8498

85-
TransactionLevelGuard
99+
impl Drop for StorageLayerGuard {
100+
fn drop(&mut self) {
101+
dec_transaction_level()
102+
}
86103
}
87104
}
88105

89-
/// Assert this method is called within a storage transaction.
90-
/// This will **panic** if is not called within a storage transaction.
91-
///
92-
/// This assertion is enabled for native execution and when `debug_assertions` are enabled.
93-
pub fn require_transaction() {
94-
#[cfg(all(feature = "std", any(test, debug_assertions)))]
95-
debug_helper::require_transaction();
106+
/// Check if the current call is within a transactional layer.
107+
pub fn is_transactional() -> bool {
108+
transaction_level_tracker::is_transactional()
96109
}
97110

98111
/// Execute the supplied function in a new storage transaction.
99112
///
100113
/// All changes to storage performed by the supplied function are discarded if the returned
101114
/// outcome is `TransactionOutcome::Rollback`.
102115
///
103-
/// Transactions can be nested to any depth. Commits happen to the parent transaction.
104-
pub fn with_transaction<R>(f: impl FnOnce() -> TransactionOutcome<R>) -> R {
116+
/// Transactions can be nested up to `TRANSACTIONAL_LIMIT` times; more than that will result in an
117+
/// error.
118+
///
119+
/// Commits happen to the parent transaction.
120+
pub fn with_transaction<T, E>(f: impl FnOnce() -> TransactionOutcome<Result<T, E>>) -> Result<T, E>
121+
where
122+
E: From<DispatchError>,
123+
{
105124
use sp_io::storage::{commit_transaction, rollback_transaction, start_transaction};
106125
use TransactionOutcome::*;
107126

127+
let _guard = transaction_level_tracker::inc_transaction_level()
128+
.map_err(|()| TransactionalError::LimitReached.into())?;
129+
108130
start_transaction();
109131

110-
#[cfg(all(feature = "std", any(test, debug_assertions)))]
111-
let _guard = debug_helper::inc_transaction_level();
132+
match f() {
133+
Commit(res) => {
134+
commit_transaction();
135+
res
136+
},
137+
Rollback(res) => {
138+
rollback_transaction();
139+
res
140+
},
141+
}
142+
}
143+
144+
/// Same as [`with_transaction`] but without a limit check on nested transactional layers.
145+
///
146+
/// This is mostly for backwards compatibility before there was a transactional layer limit.
147+
/// It is recommended to only use [`with_transaction`] to avoid users from generating too many
148+
/// transactional layers.
149+
pub fn with_transaction_unchecked<R>(f: impl FnOnce() -> TransactionOutcome<R>) -> R {
150+
use sp_io::storage::{commit_transaction, rollback_transaction, start_transaction};
151+
use TransactionOutcome::*;
152+
153+
let maybe_guard = transaction_level_tracker::inc_transaction_level();
154+
155+
if maybe_guard.is_err() {
156+
log::warn!(
157+
"The transactional layer limit has been reached, and new transactional layers are being
158+
spawned with `with_transaction_unchecked`. This could be caused by someone trying to
159+
attack your chain, and you should investigate usage of `with_transaction_unchecked` and
160+
potentially migrate to `with_transaction`, which enforces a transactional limit.",
161+
);
162+
}
163+
164+
start_transaction();
112165

113166
match f() {
114167
Commit(res) => {
@@ -1418,12 +1471,13 @@ pub fn storage_prefix(pallet_name: &[u8], storage_name: &[u8]) -> [u8; 32] {
14181471
#[cfg(test)]
14191472
mod test {
14201473
use super::*;
1421-
use crate::{assert_ok, hash::Identity, Twox128};
1474+
use crate::{assert_noop, assert_ok, hash::Identity, Twox128};
14221475
use bounded_vec::BoundedVec;
14231476
use frame_support::traits::ConstU32;
14241477
use generator::StorageValue as _;
14251478
use sp_core::hashing::twox_128;
14261479
use sp_io::TestExternalities;
1480+
use sp_runtime::DispatchResult;
14271481
use weak_bounded_vec::WeakBoundedVec;
14281482

14291483
#[test]
@@ -1535,25 +1589,67 @@ mod test {
15351589
}
15361590

15371591
#[test]
1538-
#[should_panic(expected = "Require transaction not called within with_transaction")]
1539-
fn require_transaction_should_panic() {
1592+
fn is_transactional_should_return_false() {
15401593
TestExternalities::default().execute_with(|| {
1541-
require_transaction();
1594+
assert!(!is_transactional());
15421595
});
15431596
}
15441597

15451598
#[test]
1546-
fn require_transaction_should_not_panic_in_with_transaction() {
1599+
fn is_transactional_should_not_error_in_with_transaction() {
15471600
TestExternalities::default().execute_with(|| {
1548-
with_transaction(|| {
1549-
require_transaction();
1550-
TransactionOutcome::Commit(())
1551-
});
1552-
1553-
with_transaction(|| {
1554-
require_transaction();
1555-
TransactionOutcome::Rollback(())
1556-
});
1601+
assert_ok!(with_transaction(|| -> TransactionOutcome<DispatchResult> {
1602+
assert!(is_transactional());
1603+
TransactionOutcome::Commit(Ok(()))
1604+
}));
1605+
1606+
assert_noop!(
1607+
with_transaction(|| -> TransactionOutcome<DispatchResult> {
1608+
assert!(is_transactional());
1609+
TransactionOutcome::Rollback(Err("revert".into()))
1610+
}),
1611+
"revert"
1612+
);
1613+
});
1614+
}
1615+
1616+
fn recursive_transactional(num: u32) -> DispatchResult {
1617+
if num == 0 {
1618+
return Ok(())
1619+
}
1620+
1621+
with_transaction(|| -> TransactionOutcome<DispatchResult> {
1622+
let res = recursive_transactional(num - 1);
1623+
TransactionOutcome::Commit(res)
1624+
})
1625+
}
1626+
1627+
#[test]
1628+
fn transaction_limit_should_work() {
1629+
TestExternalities::default().execute_with(|| {
1630+
assert_eq!(transaction_level_tracker::get_transaction_level(), 0);
1631+
1632+
assert_ok!(with_transaction(|| -> TransactionOutcome<DispatchResult> {
1633+
assert_eq!(transaction_level_tracker::get_transaction_level(), 1);
1634+
TransactionOutcome::Commit(Ok(()))
1635+
}));
1636+
1637+
assert_ok!(with_transaction(|| -> TransactionOutcome<DispatchResult> {
1638+
assert_eq!(transaction_level_tracker::get_transaction_level(), 1);
1639+
let res = with_transaction(|| -> TransactionOutcome<DispatchResult> {
1640+
assert_eq!(transaction_level_tracker::get_transaction_level(), 2);
1641+
TransactionOutcome::Commit(Ok(()))
1642+
});
1643+
TransactionOutcome::Commit(res)
1644+
}));
1645+
1646+
assert_ok!(recursive_transactional(255));
1647+
assert_noop!(
1648+
recursive_transactional(256),
1649+
sp_runtime::TransactionalError::LimitReached
1650+
);
1651+
1652+
assert_eq!(transaction_level_tracker::get_transaction_level(), 0);
15571653
});
15581654
}
15591655

0 commit comments

Comments
 (0)