Problems with Concurrency

I think the problem we are trying to solve is to guarantee atomicity when performing an atomic transactional operation involving multiple contracts.

Current Tezos implementation guarantees atomicity of an operation with a single contract. Contract entry point (P, S) -> S depends on the contract current state and produces new updated state atomically.

A contract can spawn a new transaction with involves multiple contracts by creating multiple invocations (operations) for other contracts to be executed later ( (P,S) -> (operations, S) ). Breadth First Search (BFS) guarantees that the sequence of operations will not be interleaved with other operations which can alter the state of involved contracts. List of operations returned by a contract may be considered as a transaction that modifies a global state (state of multiple contracts) from one consistent state to another consistent state.

Things become problematic if we want to create an operation which depends on the state of the more than one contract. (SA, SB …, S) -> S. We want to guarantee that the multiple contract states, which are input for such operation, are consistent. We also want to guarantee that issued operations operate on the same input.

There are two discussed approaches to obtain states of other contracts:

  1. Readonly calls (views) which return immediately (Adding Read-Only Calls)
  2. Callback style view entry point design pattern (https://gitlab.com/tzip/tzip/blob/master/proposals/tzip-4/tzip-4.md#view-entrypoints)

But both of them do not guarantee transaction consistency.

Let’s consider an example of two way transfer:

There are three contracts A, B, C representing accounts with some balances that have nat %deposit and nat %withdraw entry points. A manager contract M has a nat %transfer entry point that withdraws specified amount from account A, splits it and deposit split amounts to accounts B and C.

Current Call Next Calls
... [ M(transfer) ]
Call to M transfer [A(withdraw), B(deposit), C[deposit]
Call to A withdraw [B(deposit), C[deposit]
Call to B deposit [C(deposit)]
Call to C deposit (which calls D1) [D1]
Call to D1 []

In the beginning and in the end of execution of the sequence [A, B, C] all account are in consistent state. Some operation D1 is executed after the transaction [A, B, C] finishes.

Now, let’s assume that contract B %deposit entry point wants to create another operation that depends on the state of contracts A and C (SA, SC, SB) -> (operations, SB).

If it we use proposed readonly views ( SA = VIEW “balance” A; SC = VIEW “balance” B), it will read inconsistent state (the contract A has already changed its state, but the contract C has not).

If we use callback style view entry points, the problem seems to be solved. The state of contracts A and C is inspected after the transaction [A, B, C] finishes:

Current Call Next Calls
... [M(transfer)]
Call to A withdraw [B(deposit), C[deposit]
Call to B deposit [C(deposit), A1(get_balance), C1(get_balance)]
Call to C deposit (which calls D1) [A1(get_balance), C1(get_balance), D1]
Call to A1 balance [C1(get_balance), D1, B1(return_balance_a])
Call to C1 balance [D1, B1(return_balance_a], B2(return_balance_c)]
Call to D1 [B1(return_balance_a], B2(return_balance_c)]
Call to B1 return_balance_a [B2(return_balance_c)]
Call to B2 return_balance_c [F(with balance a, c)]
Call to F with balance a, c []

But the callback style has drawbacks mentioned above:

  1. There is no guarantee that get_balance operation would not inject other operations and/or make and adversarial call (directly or indirectly) to the contract B while it collects responses from the contracts A and C.
  2. The developer needs to write boiler plate code to maintain an intermediate state while gathering responses from view calls.

The solution proposed below tries to address weak points of both mentioned approaches.

  1. Contracts may implement readonly view points as proposed in Adding Read-Only Calls
  2. Such readonly view points cannot be invoked from another contract code directly. Instead, a contract may create a delayed transaction (special kind of operation).
  3. To create a delayed transaction, a contract provides
  • A lambda which gets parameters of other contracts view results and a contract state, and return list of operations and updated state.
  • A list of other contracts views which correspond to view parameters of the delayed transaction lambda

let delayed_tx : ( a: va, c : vc, b : B) -> (operation list) * B = (a, c, b) ->

  let new_b = …

  let op = …

  [op], new_b

let contract_b_entry(p : P, state : B) : (operation list) * B =

  let view_a = VIEW “balance” a_address in

  let view_c = VIEW “balance” b_address in

  let delayed : operation = create_delayed_transaction view_a, view_b delayed_tx in

  [delayed], state

  1. When Tezos node executes such delayed transaction, it fetches a state of the contract and queries all specified views. After that, it evaluates transaction lambda and updates contract state as usual.
  2. If a delayed transaction produces other operations they are inserted into the beginning of the operation queue. They are considered a part of the whole delayed transaction to guarantee that they operate on the same global state reflected by input views.

In modified example %deposit to an account B created delayed transaction which invoke contract F with parameters based on balances of accounts A and C:

Current Call Next Calls
... [ M(transfer) ]
Call to M transfer [A(withdraw), B(deposit), C[deposit]
Call to A withdraw [B(deposit), C[deposit]
Call to B deposit (which creates delayed tx) [C(deposit), B1(delayed(view_a, view_c))]
Call to C deposit (which calls F1) [B1(delayed(view_a, view_c))], F1]
Call to B1 which calls F [F2; F1]
Call to F2 [F1]
Call to F1 []