Skip to content
This repository has been archived by the owner on Feb 6, 2023. It is now read-only.

Latest commit

 

History

History
174 lines (117 loc) · 11.3 KB

plutus-comparison.md

File metadata and controls

174 lines (117 loc) · 11.3 KB

Comparing CTL and Plutus

This document outlines the core differences between CTL and Plutus (particularly in the context of the Plutus Application Backend [PAB]). CTL is of course directly inspired by Plutus and PAB and we have attempted to preserve a high degree of similarity between the two APIs. In many cases, it should be possible to copy-paste existing code written for PAB deployments and adjust it for CTL fairly easily (accounting of course for existing differences between Haskell and Purescript). Nevertheless, CTL and Plutus differ in several important ways, as outlined below.

Note that differences between Haskell and Purescript, while also relevant to such a comparison, is beyond the scope of this document unless such differences have a direct bearing on divergences between the two CTL and Plutus.

Table of Contents

Core conceptual differences

Library vs. process

Unlike contracts written for PAB, which are compiled to a single process, CTL is a library. CTL itself can be imported as a Purescript library and contracts written in CTL compile to Javascript that can be run in the browser or NodeJS. Accordingly, there is no need to activate endpoints in CTL -- contracts are executed by calling effectful functions written using the library. This distinction has influenced our adaption of Plutus' Contract type, as outlined below.

Note, however, that CTL still requires a number of runtime dependencies. In some respects, this is similar to PAB, which also needs to communicate with plutus-chain-index and a running node. Please see the documentation for more details on CTL's runtime.

The Contract type

Both CTL and Plutus define Contract monads for constructing, balancing, and submitting transactions. There are considerable differences between the two, however:

CTL:

newtype Contract (r :: Row Type) (a :: Type) = Contract (QueryMExtended r a)

where

  • QueryMExtended is an internal monad transformer stack based on ReaderT over Aff (Purescript's monad for asynchronous effects, with no Haskell analogue).
  • r extends the record serving as the reader environment

Plutus:

newtype Contract (w :: Type) (s :: Row Type) (e :: Type) (a :: Type) = Contract { ... }

where

  • (w :: Type) is usually some Monoid used for Writer-like effects
  • (s :: Row Type) (note that Row comes from the row-types package) represents the contract schema
  • (e :: Type) represents the errors that the contract can throw

As this direct comparison illustrates, CTL's Contract is significantly simpler than Plutus'. Importantly, CTL's Contract allows for arbitrary effects. This makes Writer capabilities redundant in CTL, for instance, as all communication between Contracts can be done in a more direct manner (e.g. logging or HTTP calls).

CTL also has no concept of a "schema" for Contracts as there is no equivalent of an endpoint as in PAB. That is, effects written in Contract are not activated in some way and can instead be called normally from Purescript code. Note that despite the similar appearance of the kind signatures above, where Row Type appears in the signature of both Contracts, they are practically and conceptually unrelated. The extensible record contained in CTL's Contract allows users to easily extend the reader environment in which their contracts execute. For instance, you may wish to expose some global state across all of your contracts, perhaps a unique CurrencySymbol that will be created in one contract and read by several others. Instead of threading this state throughout as parameters, you could use a Ref (Maybe CurrencySymbol) (Refs are analogous to Haskell's IORef) and write your contracts with types signatures similar to forall (r :: Row Type). Contract (cs :: Ref (Maybe CurrencySymbol) | r) Unit. cs would then be accessible from your contracts using normal MonadReader methods (or more precisely those of MonadAsk in the case of Purescript).

For library users, CTL's Contract is less opaque than Plutus'. The Contract newtype can be unwrapped to expose the inner monad transformer stack, whose internal structure will be immediately recognizable to developers familiar transformers-style stacks. Contract also has instances for various typeclasses, making it possible to write mtl-style effects.

In contrast to the free-monad approach used by Plutus, CTL's Contract has uses a standard monadic eliminator to execute its effects (Contract.Monad.runContract), keeping with its more conventional transformer-oriented structure.

Finally, CTL's Contract is not parameterized by an error type as in Plutus. Contract actions defined by the library signal failure in various ways (e.g. returning Eithers, Maybes, or in the case of unrecoverable errors, throwing exceptions). Standardizing our error-handling approach is on our roadmap for future releases, however, most likely with a standardized error type containing polymorphic variants so that users may freely extend existing errors in the Contract interface.

API differences

Constraints and lookups

CTL has adapted Plutus' Alonzo-era constraints/lookups interface fairly closely and it functions largely the same. One key difference is that CTL does not, and cannot, have the notion of a "current" script. All scripts must be explicitly provided to CTL (serialized as CBOR, see below). This has led us to depart from Plutus' naming conventions for certain constraints/lookups:

Plutus CTL
mustPayToTheScript removed
mustPayToOtherScript mustPayToScript
otherScript validator
otherData datum
none mustPayToNativeScript
none mustSpendNativeScriptOutput

Additionally, we implement NativeScript (multi-signature phase-1 script) support, which is not covered by Plutus.

Babbage-era constraints

CIPs 0031-0033 brought several improvements to Plutus and are supported from the Babbage era onwards:

CTL has upgraded its constraints interface to work with these new features. At the time of writing, however, plutus-apps has not yet upgraded their constraints/lookups interface to support these new features. This means a direct comparison between plutus-apps and CTL regarding Babbage-era features is not currently possible. It also implies that, moving forward, CTL's constraints implementation will increasingly no longer match that of plutus-apps' to the same degree.

Typed scripts

Another difference between Plutus and CTL is our implementation of typed scripts. Recall that Plutus' ValidatorTypes class:

class ValidatorTypes (a :: Type) where
    type RedeemerType a :: Type
    type DatumType a :: Type

    type instance RedeemerType a = ()
    type instance DatumType  a = ()

Purescript lacks most of Haskell's more advanced type-level faculties, including type/data families. Purescript does, however, support functional dependencies, allowing us to encode ValidatorTypes as follows:

class ValidatorTypes :: Type -> Type -> Type -> Constraint
class
  ( DatumType validator datum
  , RedeemerType validator redeemer
  ) <=
  ValidatorTypes validator datum redeemer

class DatumType :: Type -> Type -> Constraint
class DatumType validator datum | validator -> datum

class RedeemerType :: Type -> Type -> Constraint
class RedeemerType validator redeemer | validator -> redeemer

Working with scripts

Using scripts from the frontend

As noted above, all scripts and various script newtypes (Validator, MintingPolicy, etc...) must be explicitly passed to CTL. Unlike Plutus, where on- and off-chain code can freely share Haskell values, scripts must be provided to CTL in a serialized format. The easiest way to do this is using Contract.TextEnvelope.textEnvelope along with the JS FFI. See the getting started guide for more details.

Applying arguments to parameterized scripts

CTL is currently unable to build full UPLC ASTs on the frontend (although support for this may be added in the future). This means that Plutus' applyCode, which is the default method for applying arguments to parameterized scripts, has no direct equivalent in CTL. We do, however, support a workaround for applying arguments to parameterized scripts. Contract.Scripts.applyArgs allows you to apply a list of PlutusData arguments to any type isomorphic to a PlutusScript. Using this allows you to dynamically apply arguments during contract execution, but also implies the following:

  • applyArgs must be effectful, as we use our Haskell server to do the actual script application

  • All of your domain types must have Contract.PlutusData.ToData instances (or some other way of converting them to PlutusData)

  • You must employ a workaround, illustrated by the following examples, in your off-chain code to ensure that the applied scripts are valid for both on- and off-chain code. This essentially consists of creating an wrapper which accepts Data arguments for your parameterized scripts:

    • PlutusTx:

      mkTestValidator :: Integer -> BuiltinData -> BuiltinData -> BuiltinData -> ()
      mkTestValidator _ _ _ _ = ()
      
      -- This is the wrapper function
      mkTestValidatorUntyped :: BuiltinData -> BuiltinData -> BuiltinData -> BuiltinData -> ()
      mkTestValidatorUntyped p =
        mkTestValidator
          (unsafeFromBuiltinData p)
      
      testScript :: Script
      testScript =
        fromCompiledCode
        $$(PlutusTx.compile [|| mkTestValidatorUntyped ||])
      
      testValidator :: Integer -> Validator
      testValidator params =
        mkValidatorScript
        -- `toBuiltinData` is redundant here but it makes the signature match
        ($$(PlutusTx.compile [|| mkTestValidatorUntyped ||]) `PlutusTx.applyCode` PlutusTx.liftCode (PlutusTx.toBuiltinData params))
    • Plutarch:

      mkTestValidator :: Term s (PInteger :--> PData :--> PData :--> PScriptContext :--> POpaque)
      mkTestValidator = plam $ \_i _datm _redm _ctx -> popaque $ pconstant ()
      
      mkTestValidatorUntyped :: Term s (PData :--> PData :--> PData :--> PScriptContext :--> POpaque)
      mkTestValidatorUntyped = plam $ \iData -> ptryFrom @(PAsData PInteger) iData $ \(_, i) -> mkTestValidator # i
      
      testScript :: Script
      testScript = compile mkTestValidatorUntyped
      
      testValidator :: Integer -> Validator
      testValidator i = mkValidator $ mkTestValidator # pconstant i