How does Motoko deal with reentrancy?

As a developer with both a bit of a background on other distributed ledger systems as well as working with Elixir/the BEAM VM which can be considered the ‘OG’ Actor Model implementation, I am wondering what approaches Motoko takes to prevent reentrancy-attacks.

Essentially, my current understanding of Motoko is:

  • Invoking a function on a different actor/canister is always asynchronous. (a ‘cast’ in BEAM parlance).
  • If we are interested in the result of this invocation (a ‘call’ in BEAM parlance), we use await.
  • Code is transformed into a continuation-passing-style under the hood.

Consider:

  1. a function a on actor A (containing both data and code) calls (and awaits) actor B’s b.
  2. b internally calls (and awaits) another function a2 of actor A that mutates A’s state.
  3. a2 finishes; b resumes.
  4. b finishes; a resumes.
  5. unless the programmer was extremely careful, A’s state might be different from what the rest of a expected, resulting in e.g. A locking up or showing unintended behaviour.

Motoko takes a lot of care to prevent/reduce other kinds of programming errors with its type system, sane defaults etc.
Are there (currently or planned) any ways of preventing (or reducing the likelyhood of) reentrancy-attacks in Motoko’s design?

On e.g. the BEAM, this is resolved by all actors having an actual mailbox which serializes the order in which a single actor handles incoming invocations. This prevents reentrancy by a (synchronious) call-sequence like shown above ‘timing out’ (since A is waiting for B, B does not get a response when attempting to invoke a2).
I’m not sure if that kind of solution (although it introduces its own peculiarities) would be possible to employ in the case of Motoko since ‘time’ is a bit of a vague and malleable concept when talking about a decentralized Internet Computer.

What are you thoughts on this?

6 Likes

Very good question! Motoko also uses mailbox, so messages are processed 1 by 1. But every async/await also translates to another message. So In your above example, while A’s a waits for B’s b to finish, there can be other messages delivered to the mailbox, and processed in between. For example, a call to A’s a2 (in your example) is valid. Similarly, another call to A’s a is also possible.

So really, the programmer should just consider await as “the incoming message/call has now finished. this canister is ready to process next message/call”. They need to deal with “re-entrancy” with care, because they do arise.

But perhaps this is also not so surprising in any async/await enabled languages.

5 Likes