6

When upgrading our codebase on top of substrate's polkadot-v0.9.42, we discovered it is no longer possible to make nested request to fn execute_in_transaction(call), from within the call.

Given in polkadot-v0.9.42:

            fn execute_in_transaction<F: FnOnce(&Self) -> #crate_::TransactionOutcome<R>, R>(
                &self,
                call: F,
            ) -> R where Self: Sized {
                self.start_transaction();

                *std::cell::RefCell::borrow_mut(&self.commit_on_success) = false;
                let res = call(self);
                *std::cell::RefCell::borrow_mut(&self.commit_on_success) = true;

                self.commit_or_rollback(std::matches!(res, #crate_::TransactionOutcome::Commit(_)));

                res.into_inner()
            }


            fn start_transaction(&self) {
                if !*std::cell::RefCell::borrow(&self.commit_on_success) {
                    return
                }

                #crate_::OverlayedChanges::start_transaction(
                    &mut std::cell::RefCell::borrow_mut(&self.changes)
                );
                if let Some(recorder) = &self.recorder {
                    #crate_::ProofRecorder::<Block>::start_transaction(&recorder);
                }
            }

We get:

execute_in_transaction {
  start_transaction
  commit_on_success = false
  execute_in_transaction {
    start_transaction <- returns, no transaction created in overlay
    res = ...
    commit_on_success = true
    commit_or_rollback(res) <- closes the outer transaction
  }
  commit_on_success = true
  commit_or_rollback(res) <- fails on `NoOpenTransaction`
}

Q1: Are these changes intentional to disallow such nested behaviour? Or is this something that might be supported in the future ?

The problem: In Mangata, transaction included into block N is executed in block N+1 because of shuffling(VER prevention) mechanism. Instead of pushing extrinsics into the current block, we also build the next one, apply the extrinsics there, get the list of valid ones, and rollback the entire new block changes.

eg.

authorship.propose {
  ...
  apply_previous_block_extrinsics
  valid_txs = execute_in_transaction {
    init_new_block
    txs = []
    for tx in tx_pool {
      execute_in_transaction {
        apply_extrinsic_with_context(tx)
      } is OK ? txs.push(tx)
    }
    TransactionOutcome::Rollback(txs)
  }
  create_enqueue_txs_inherent(valid_txs)
  continue building the block as usual...
}

Without modifying the impl_runtime_apis, our initial idea was to instantiate another runtime_api() and apply changes to both of them until the next block validation step. We would use the duplicate runtime api to verify a new block with trxs and just discard the changes. Such solution would have some performance implications having to apply changes twice.

Ideally, we would imagine creating a disposable snapshot of the current runtime to validate next block execution against.

Q2: Are there currently any other substrate apis we could leverage to achieve our goal?

1 Answer 1

1

The function execute_in_transaction was actually never written with nested transaction in mind. In the early implementation we did not even supported "real" transactions. But yeah the last years it was accidentally working with nested transactions until I broke it :P

This pull request will support nested transactions officially :)

Your Answer

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

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