I tested Elements — Simplicity. Simplicity aims to go beyond the limits…
By: Louis Singer
Tags: Blockchain, Elements, Smart Contracts, Bitcoin, Simplicity

Simplicity source code

The first part of the article focuses on how the Haskell implementation of simplicity works. It helps to understand the language. The simplicity Haskell library source code is available here: https://github.com/ElementsProject/simplicity/tree/master/Haskell

If you want to explore the code, clone the simplicity repository, and go inside the Haskell folder:



cd simplicity/Haskell git clone https://github.com/ElementsProject/simplicity.git cd simplicity/Haskell

Introduction to Simplicity

Simplicity is a typed functional programming language. As a functional language, it consists of applying a combination of expressions. These expressions are created from 9 combinators.

A stack-based programming language

Simplicity scripts work as a Bitcoin script. It is a stack-based programming language. It means that the program manipulates a stack data structure.

Example of a stack manipulation

The expressions applied to the stack are low-level expressions that modify the state of the stack. This has several advantages, including being easily verifiable and efficient.

Simplicity defines 9 combinators: a set of low-level operations. A Simplicity program consists to apply a set of combinators.

Commitment Merkle root of a Simplicity program

The way that users can call simplicity scripts is very similar to a P2SH (pay to script hash). P2SH means that users do not commit the funds directly to the script but rather they commit to a hash of their Bitcoin script. The script is revealed only when the funds are redeemed. The protocol handles the script hash verification.

Simplicity works in much the same way except that it is not a linear hash of the script that is committed but the Merkle root of the script. All the operations (= the combinators) that compose the scripts form a Merkle tree. Then the root of the tree is committed on the blockchain.

Then, when a Simplicity script is executed, it must have the same Merkle root of the previously committed one. The advantage is that you don’t lose the possibility to check the integrity of the script (we only need to calculate the Merkle root). And the tree architecture allows executing the program step by step: a branch is revealed only if the execution of the previous branch returned true.

Simplicity Core

The Simplicity Core gathers all the common features. The core is independent of what blockchain Simplicity runs.

Types

Haskell Types module: https://github.com/ElementsProject/simplicity/blob/master/Haskell/Core/Simplicity/Ty.hs

Simplicity language uses three kind types:

The unit type 1 .

. The sum of two Simplicity types, A+B .

. The product of two Simplicity types, A*B .

The Haskell library implements them with several Haskell design patterns. Let’s see how it works.

First of all, there is TyC class with three instances:

intance TyC ()

instance (TyC a, TyC b) => TyC (Either a b)

instance (TyC a, TyC b) => TyC (a, b)

This class is using to bind the authorized Haskell types to Simplicity types. We called this a constraint. In our case TyC constraints us to use the following Haskell types:

The empty tuple type () : a special Haskell type which has a unique value: () . Often used to represent Unit.

: a special Haskell type which has a unique value: . Often used to represent Unit. The Either a b type, where a and b are types of class TyC . Either documentation.

type, where and are types of class . Either documentation. The pair type (a, b) , where a and b are types of class TyC .

Thus, 1 which is an Int hasn’t an instance of the TyC . Same thing for the following expressions:

-- The following expressions' types don't have an instance of the TyC class list :: [Int]

list = [1, 2, 6, 200] tuple3 :: (Int, String, String)

tuple3 = (3, "foo", "foo") eitherIntOrString :: Either Int String

eitherIntOrString = Left 3

Actually, the types allowed are empty tuples compositions. (Or compositions of empty tuples compositions).

-- The following expressions' types have a TyC instance emptyTuple :: ()

emptyTuple = () tupleOfEmptyTuples :: ((), ())

tupleOfEmptyTuples = ((), ()) either0 :: Either () ((), ())

either0 = Right tupleOfEmptyTuples either1 :: Either (((), ()), Either ((), ((), ())) ((), ())) ()

either1 = Right ()

⚠ It could be a bit confusing when we deal with empty tuples () . The () is both a type (type for empty tuples) and the value of an empty tuple.

Note that the TyC type class is build using a pattern so that its methods are not exported. Thus, anyone will not be able to create new instances of the class.

As you can see from the last examples, manipulating the empty tuples compositions is not very comfortable. The data-level type TyReflect will solve the problem and link all of that with the Simplicity types.

data TyReflect a where

OneR :: TyReflect ()

SumR :: (TyC a, TyC b) => TyReflect a -> TyReflect b -> TyReflect (Either a b)

ProdR :: (TyC a, TyC b) => TyReflect a -> TyReflect b -> TyReflect (a, b)

This is a Generalized Algebraic DataType (GADT). Which implements all the Simplicity types with three data constructors:

The Simplicity Unit type: OneR takes no argument and returns an empty tuple.

takes no argument and returns an empty tuple. All the types resulting from the sum of two other Simplicity types : SumR takes two TyReflect ( a and b ) and returns Either a b .

takes two ( and ) and returns . All the types resulting from the product of two other Simplicity types : SumR takes two TyReflect ( a and b ) and returns (a, b) .

Now we can rewrite the previous examples with our data types:

-- The following expressions' types have a TyC instance (Rewrite with TyReflect. emptyTuple :: TyReflect ()

emptyTuple = OneR tupleOfEmptyTuples :: TyReflect ((), ())

tupleOfEmptyTuples = ProdR OneR OneR either0 :: TyReflect (Either () ((), ()))

either0 = SumR OneR tupleOfEmptyTuples either1 :: TyReflect (Either (((), ()), Either ((), ((), ())) ((), ())) ())

either1 = SumR (ProdR (ProdR OneR OneR) (SumR (ProdR OneR (ProdR OneR OneR)) (ProdR OneR OneR))) OneR

For each type constrained by the TyC class, there is a TyReflect GADT associated.

The function reify returns the unique TyReflect value corresponding to the TyC type given as parameter.

The last element used to capture Simplicity type is the type alias Ty .

When we’ll write our smart contract, we cannot use the classic Haskell type like Bool, String, or whatever. All our data need to be compliant with the simplicity type.

Fortunately, with the three simplicity types, we can implement everything and so create our data type.

We can, for example, represent a single Bit:

type Bit = Either () ()

And canonically map it to the Haskell type Bool :

fromBit :: Bit -> Bool

fromBit (Left ()) = False

fromBit (Right ()) = True toBit :: Bool -> Bit

toBit False = Left ()

toBit True = Right ()

With Bit representation, we are now able to represent everything. Blockstream developers have provided a Word.hs file that shows you how to represent a Word (= a Bits vector) with the Bit type.

Terms and combinators

Now we have seen what are Simplicity types and how they work. We can apply terms to a simplicity value. A term is a Simplicity expression. To manipulate terms, Simplicity uses combinators.

The Haskell implementations of Simplicity’s combinators are located in the Core.hs file. Let’s describe the nine combinators:

Identity

iden :: TyC a => term a a

iden = id

Return the identity function for the term given as a parameter.

Composition

comp :: (TyC a, TyC b, TyC c) => term a b -> term b c -> term a c

comp s t = t . s

Takes two terms as parameters and returns the composition of these two terms.

comp can be used with the operator >>> .

Constant Unit

unit :: TyC a => term a ()

unit = const ()

This one is simple, it always returns () whatever the type or the value of the argument.

Left Injection

injl :: (TyC a, TyC b, TyC c) => term a b -> term a (Either b c)

injl t a = Left (t a)

We take a term t applied to a and compose it with the Left constructor of the Either data type.

Right Injection

injr :: (TyC a, TyC b, TyC c) => term a c -> term a (Either b c)

injr t a = Right (t a)

The same thing that the Left injection but use the Right constructor.

Left and right injection can be used to gather two Simplicity types inside tuples.

Match (Case)

The real term name is Case but this is a reserved keyword in Haskell. That’s why, the Haskell implementation of Simplicity’s case term is called match .

match :: (TyC a, TyC b, TyC c, TyC d) => term (a, c) d -> term (b, c) d -> term (Either a b, c) d

match s _ (Left a, c) = s (a, c)

match _ t (Right b, c) = t (b, c)

match takes two terms and, depending on the product given as parameter it will apply the first term or the second one. Match lets us implement conditions.

Pair

pair::(TyC a, TyC b, TyC c) => term a b -> term a c -> term a (b, c)

pair s t a = (s a, t a)

Pair can also by use with the combinator &&& .

This one applies two terms to value and returns results inside a pair.

Take

take :: (TyC a, TyC b, TyC c) => term a c -> term (a, b) c

take t (a, _) = t a

Take apply a term given as a parameter to the first component of a pair.

Drop

drop :: (TyC a, TyC b, TyC c) => term b c -> term (a, b) c

drop t (_, b) = t b

Drop applies a term given as a parameter to the second component of a pair.

Combinators in practice: Boolean example

The boolean example refers to the files Programs/Bit.hs and the type definition Ty/Bit.hs.

Represent a single bit with Simplicity is pretty simple. A bit has two possible values: false and true. We can use the Simplicity type 1+1 , or Either () () in Haskell.

type Bit = Either () ()

The toBit function is using to convert a boolean value to a bit and fromBit do the opposite.

With injections combinators, we can define the two boolean values.

true :: (Core term, TyC a) => term a Bit

true = injr unit false :: (Core term, TyC a) => term a Bit

false = injl unit

true is a term that always returns Right () . And false a term that always return Left () .

Then, implement the cond function that lets to use conditions. cond returns a term that applies s if it takes true as a parameter or t it takes false.

cond :: (Core term, TyC a, TyC b) => term a b -> term a b -> term (Bit, a) b

cond thn els = match (drop els) (drop thn)

This one is also an interesting thing, it implements a conditional function that takes two terms as parameters. Then it returns a term that takes a Boolean value (i.e a Bit) to determine which term needs to be applied.

Then, with cond, it’s now possible to implement some common boolean operators.

-- | Simplicity combinator that computes inverts the Bit result of an expression.

not :: (Core term, TyC a) => term a Bit -> term a Bit

not t = t &&& unit >>> cond false true -- | Simplicity combinator that computes the short-circut conjunction of the results of two expressions.

and :: (Core term, TyC a) => term a Bit -> term a Bit -> term a Bit

and s t = s &&& iden >>> cond t false -- | Simplicity combinator that computes the short-circut disjunction of the results of two expressions.

or :: (Core term, TyC a) => term a Bit -> term a Bit -> term a Bit

or s t = s &&& iden >>> cond true t

These three expressions return Simplicity terms. You can try the functions by importing the Haskell module Simplicity.Programs.Bit .

fromBit $ true ()

True

fromBit $ false ()

False

fromBit $ Simplicity.Programs.Bit.or false true ()

True

fromBit $ Simplicity.Programs.Bit.and false true ()

False

fromBit $ Simplicity.Programs.Bit.not true ()

False

Let’s draw the Abstract Syntax Tree of the not program:

AST of not t

This is how Simplicity programs are represented. Each script can be translated into a Merkle tree like this one. Obviously, the real tree of the program is much larger. In fact the cond , false and true can be replaced by their own trees. Same thing for the t term.

See the tree of not false :

The real tree of a simplicity program is the one that contains only the basic combinators. Each of them represents a node of the tree and has an associated value. With this representation, it is easy to recompute the root of a script to check its integrity.

This technique is based on Merkelized abstract syntax tree: MAST.

Simplicity primitives

The simplicity primitives provide features specific to a given blockchain. The core Simplicity contains the main feature whereas primitives provide features depending on the blockchain. For instance, the Elements’ primitive implement a term to issue an asset. The primitives of Bitcoin and Elements are provided inside the official source code repository.

The purpose of this section is to reproduce Russell O’Connor’s transaction (the first official Simplicity transaction). Also described in the Next-Gen Smart Contracting Webinar.

The transaction consists to use the program CheckSigHashAll, the official example.

The CheckSigHashAll.hs file provides a checkSigHashAll Simplicity expression that verifies Schnorr signature

over the Bitcoin specific transaction data hash produced by sigHashAll for a provided public key.

In fact the official example program do two things:

First, it computes a transaction digest following an obsolete BIP-Schnorr signature. Then, it checks if the signature given as parameter is valid for the computed transaction digest.