8

Substrate has a macro, #[transactional], to ensure that any modifications to storage are not persisted in case of an error during execution.

I imagine there's some sort of caching done before committing any changes to the underlying database, and I was curious to know what kind of overhead was associated with annotating a function as #[transactional].

Maybe as a simple metric, if we consider two functions - one tagged with transactional and another not, how many more times could we execute the non-transactional function vs. the transactional function in the same amount of time?

2 Answers 2

5

Benchmark results for the #[transactional] overhead can be found here:

https://github.com/paritytech/substrate/pull/10809

The key takeaways:

  • Spawning a transactional layer is quite cheap; just 50 nanoseconds per layer.
  • Writing 1,000 storage item between one layer is 146,163 nanoseconds, so ~146 nanoseconds per storage item.
    • When compared to the weight of a write in RocksDB (100,000 nanoseconds), the overhead is only .15%, which is extremely nominal.

This is why we are interested to make #[tranasctional] the default behavior across all Substrate extrinsics.

A specification can be found here:

https://github.com/paritytech/substrate/issues/10806

The main concern to have with using #[transactional] today is to ensure you prevent a potential stack overflow by a malicious user spawning too many of them in a transaction.

This is why in my proposal, I set an upper limit to the number of layers that can be spawned, and return an error when trying to spawn more layers.

Additionally, we will make spawning a new layer an explicit action, so that most users will not run into this limit when writing code.

2
  • Is each 'layer' a nested declaration of #[transactional]? How could a malicious user spawn too many layers?
    – Angelo
    Mar 24, 2022 at 22:56
  • 1
    Yes. You can imagine making an extrinsic transactional that can dispatch another extrinsic. For example, batch_all is transactional. Without any blockers, you could do batch_all(batch_all(batch_all(... which would nest a bunch of layers. We actually wrote code in the pallet to explicitly prevent this right now.
    – Shawn Tabrizi
    Mar 25, 2022 at 3:03
4

One thing to keep in mind is that the the size of the storage items do not matter for the overhead. They are kept as Vec in the overlay and are moved around without copying.

You pay O(#dirty_keys) on each commit or discard. Most notably the read and write performance for keys are unaffected by the current layering depth. More information can be found in the original PR:

https://github.com/paritytech/substrate/pull/6269

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.