@@ -27,8 +27,11 @@ use crate::{
2727} ;
2828use codec:: { Decode , Encode , EncodeLike , FullCodec , FullEncode } ;
2929use sp_core:: storage:: ChildInfo ;
30- use sp_runtime:: generic:: { Digest , DigestItem } ;
3130pub use sp_runtime:: TransactionOutcome ;
31+ use sp_runtime:: {
32+ generic:: { Digest , DigestItem } ,
33+ DispatchError , TransactionalError ,
34+ } ;
3235use sp_std:: prelude:: * ;
3336pub use types:: Key ;
3437
@@ -44,71 +47,121 @@ pub mod types;
4447pub mod unhashed;
4548pub 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) ]
14191472mod 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