When is it advantageous to use transient contracts?

Transient contracts are those that are created and consumed within the same transaction.

They never appear in the active contracts and they are also filtered out of the “flat” transaction stream, but their creation and archival are part of the overall transaction and they do appear in the transaction tree stream.

Since they appear in the transaction tree stream, transient contracts must be persisted, which could lead to performance implications in very complex workflows.

As such, I would argue it could be advantageous to limit their usage to cases in which it’s impossible to express an equivalent workflow with a less powerful construct.

The obvious difference between using and not using a transient contract is the effect itself of having the contract persisted as part of the transaction. However, it’s unclear to me whether a transient contract can add any relevant information that cannot be derived from the model and the structure of the transaction.

Long shot question: are usages of transient contracts generally translatable to some less powerful language construct without losing expressiveness?

  • If they are: should (and can?) the compiler emit a warning when a contract is consumed within the same transaction in which it’s created?
  • If they are not: what are there guidelines that can be used to understand when and when not to resort to transient contracts? Could those guidelines be translated into a static check?
3 Likes

There are a few things transient or sometimes ephemeral contracts are useful for, but let me focus on the important one: Compositionality

Let’s say you have a transfer flow with a TransferProposal step. A corresponding Trade usually creates and archives the TransferProposal in the Settle choice.

template Asset
  with
    issuer : Party
    owner : Party
    symbol : Text
    quantity : Decimal
  where
    signatory issuer, owner
    ensure quantity > 0.0

    controller owner can

      ProposeTransfer
        : ContractId TransferProposal
        with
          newOwner : Party
        do
          create TransferProposal with
            asset = this
            newOwner

template TransferProposal
  with
    asset : Asset
    newOwner : Party
  where
    signatory (signatory asset)

    controller newOwner can
      TransferProposal_Accept
        : ContractId Asset
        do
          create asset with
            owner = newOwner


template Trade
  with
    baseAssetCid : ContractId Asset
    baseAsset : Asset
    quoteAsset : Asset
  where
    signatory baseAsset.owner

    controller quoteAsset.owner can

      Trade_Settle
        : (ContractId Asset, ContractId Asset)
        with
          quoteAssetCid : ContractId Asset
        do
          fetchedBaseAsset <- fetch baseAssetCid
          assertMsg
            "Base asset mismatch"
            (baseAsset == fetchedBaseAsset with
              observers = baseAsset.observers)

          fetchedQuoteAsset <- fetch quoteAssetCid
          assertMsg
            "Quote asset mismatch"
            (quoteAsset == fetchedQuoteAsset with
              observers = quoteAsset.observers)

          tpBase <- exercise baseAssetCid ProposeTransfer with
            newOwner = quoteAsset.owner
          newBaseCid <- exercise tpBase TransferProposal_Accept

          tpQuote <- exercise quoteAssetCid ProposeTransfer with
            newOwner = quoteAsset.owner
          newQuoteCid <- exercise tpQuote TransferProposal_Accept

          return (newBaseCid, newQuoteCid)

Usually the TransferProposal is used as part of the propose-accept pattern to collect the agreements of owner and newOwner. In the Trade_Settle choice we already have the authorities of owner and newOwner so the TradeProposal workflow is unnecessary, but can’t make use of that without adding a new choice to Asset:

      choice BilateralTransfer
        : ContractId TransferProposal
        with
          newOwner : Party
        controller [owner, newOwner]
        do
          create this with
            owner = newOwner

The issuer would need to agree to that model change, and if Trade is an add-on to the basic Asset, it may not be practical even then.

So - to replace this type to transient contract, we’d need an entirely new mechanism to transfer authority and compose existing contracts. But that new feature would probably introduce new nodes into the Ledger Model, which you may have to persist.

Which leads me to a different question: How do you decide what to persist in the first place? The essential state of a participant’s projection are

  1. The root nodes of each received transaction
  2. A map ContractId -> ContractArgs containing all divulged, but otherwise unknown contracts.

Everything else can be reinterpreted from that. In other words, transient contracts like the above are not essential state for any of the parties. The exercise ProposeTransfer nodes are. Persisting or not persisting the TransferProposal contract is a question of optimization, just like it is for persisting Fetch and NoSuchKey nodes.

I do think that we should have a way get ones hands on Ledger as per the Ledger Model, including Fetch, NoSuchKey and “transient” contracts. So maybe the questions we should be asking are

  1. What data should the Ledger API be able to serve in a performant manner
  2. How do we enable external ledger validation and exploration without bogging down the Ledger API Server in persistence tasks

Maybe the answer to 1. is “some version of the flat transaction stream” and the answer to 2. is “Ledger API can serve essential state, which has to be expanded using an external process”?

3 Likes

Thanks for the answer.

Your questions probably deserve a topic of their own to keep the discussion focused. :slightly_smiling_face: