Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions cc-book/src/concepts/mls/transactions.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,68 @@ val bytes = ctx.getData()

These were implemented for the purpose of checkpointing during initial sync / batch decryption, but are not limited to
that use.

## Transaction Cancellation (Swift)

The foreign-language wrappers can associate a `CoreCryptoCancellationToken` with a transaction, i.e., this is invisible
to users of the wrappers. However, currently **only the Swift wrapper** supports cancellable transactions. The
`CoreCryptoCancellationToken` is created through FFI but lives on the Rust side. Once the transaction owns the
transaction semaphore, Rust publishes the token in the `CoreCrypto` cancellation slot and, when a PKI environment
exists, in its separate slot. The returned guards keep the slots filled for exactly the lifetime of the transaction.

The following shows a transaction with a cancellable foreign callback, such as `MlsTransport.sendCommitBundle()` or a
`PkiEnvironment` hook. A transaction that does not invoke such a callback skips the callback portion of the sequence.

```mermaid
sequenceDiagram
autonumber
participant Foreign as Foreign language Wrapper
participant Tx as Rust transaction
participant Slots as Rust cancellation slots
participant Callback as Foreign callback

Foreign->>Foreign: create cancellation token
Foreign->>Tx: start cancellable transaction
Tx->>Tx: race cancellation against semaphore acquisition

alt Token is cancelled before acquisition
Foreign->>Tx: cancel token
Tx-->>Foreign: TransactionCanceled (slots remain empty)
else Transaction semaphore is acquired
Tx->>Slots: enter token in CoreCrypto slot
Slots-->>Tx: cancellation guard
opt PKI environment exists
Tx->>Slots: enter token in PKI slot
Slots-->>Tx: cancellation guard
end
Tx->>Foreign: execute command with context
Foreign->>Tx: context operation
Tx->>Slots: read active token
Slots-->>Tx: shared token
Tx->>Callback: invoke callback and race its result against cancellation

alt Command finishes first
Callback-->>Tx: callback result
Tx-->>Foreign: context operation result
Foreign-->>Tx: command result
alt Command succeeded
Tx->>Tx: commit
else Command failed
Tx->>Tx: abort
end
else Token is cancelled while callback is running
Foreign->>Tx: cancel token
Tx-xCallback: drop callback future, causing UniFFI to cancel the foreign task
Tx->>Tx: drop command future and abort
end

Tx->>Slots: drop guards and clear both slots
Tx->>Tx: drop context and release semaphore
Tx-->>Foreign: success, command error, or TransactionCanceled
end
```

The guards are deliberately created after the transaction context. Rust drops local values in reverse declaration order,
so both slots are cleared before dropping the context that releases the semaphore. Consequently, the next transaction
cannot observe the previous transaction's token. If cancellation wins a callback race, dropping the Rust future also
causes UniFFI's generated foreign-language binding to cancel the task executing that callback.
32 changes: 32 additions & 0 deletions crypto-ffi/src/cancellation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
//! # CoreCryptoCancellationToken Implementation Details
//!
//! For a more high-level description and a sequence diagram, see the `Transaction` chapter of the CoreCrypto book.
//!
//! The [CoreCryptoCancellationToken] is supposed, but in theory not limited, to be used in
//! [crate::CoreCryptoFfi::transaction_ffi_cancellable]. This describes the implementation and intended usage which
//! matches the current usage in that function.
//!
//! ## How Cancellation Works
//! To make a future cancellable, we race the future against [CoreCryptoCancellationToken::cancelled] using
//! [futures_util::select_biased], with a bias for [CoreCryptoCancellationToken::cancelled] - in case both futures
//! complete at the same time, cancellation is preferred. If cancellation wins, we return an appropriate error.
//!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This already helps quite a bit, but I think we can go one step further and really make things clear by outlining the sequence of steps that occur, both on the Rust side and the foreign language side.
IOW, something like a sequence diagram would be helpful to show when tokens are created, when the slots are filled, when the guards are dropped etc.

//! ## Sharing the Cancellation Token With Foreign Callbacks
//! Cancellation needs to cause rust futures currently waiting for foreign callbacks to stop waiting, i.e., be dropped.
//! This is because when they are dropped, the uniffi-generated swift code will cancel the task that is running the
//! foreign callback. To share an existing token with callbacks, [crate::CoreCryptoFfi] has a clonable
//! [CancellationSlot], with space for a single token.
//! To fill a slot, call [CancellationSlot::enter]. This will return a [slot::CancellationGuard] which empties the slot
//! when dropped. [CancellationSlot::enter] will panic if called while the slot is still filled.
//! [crate::CoreCryptoFfi::transaction_ffi_cancellable] ensures that this constraint isn't violated by only filling the
//! slot after the transaction semaphore was acquired. *This constraint makes the [CancellationSlot] currently only
//! usable in transactions*.
//! The slot instance is cloned to instances of this crate's structs implementing foreign traits, currently
//! [crate::MlsTransport] and [crate::PkiEnvironmentHooks]. Trait method implementations can then use the token from the
//! slot to race against its cancellation as described [above](#how-cancellation-works).
//! The [crate::MlsTransport] struct receives its [CancellationSlot] clone directly from [crate::CoreCryptoContext]
//! during [crate::CoreCryptoContext::mls_init].
//! In case of [crate::PkiEnvironmentHooks] it is simpler to provide it with its own cancellation slot that is
//! explicitly filled with the same token during the transaction. That is because of the undetermined initialization
//! order (it may be initialized before or after [crate::CoreCryptoFfi]).

mod slot;
mod token;

Expand Down
2 changes: 2 additions & 0 deletions crypto-ffi/src/core_crypto/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ impl CoreCryptoFfi {
///
/// Cancelling the token aborts the transaction and stops waiting for any
/// in-flight `MlsTransport` callback or `PkiEnvironment` hook associated with it.
///
/// For implementation details, see the [`cancellation` docs][crate::cancellation].
pub async fn transaction_ffi_cancellable(
&self,
command: Command,
Expand Down
Loading