I tested Elements — Simplicity

Louis Singer
May 6 · 17 min read

Simplicity aims to go beyond the limits of Bitcoin scripting and allows to implement complex smart contracts on Bitcoin and Bitcoin-like blockchain (Elements, Liquid). We will see here the points that will allow you to understand how the language works.

The Simplicity’s technical references: https://github.com/ElementsProject/simplicity/blob/pdf/Simplicity-TR.pdf

Simplicity is a language aiming to be used through another programming language. For now, there are two implementations:

  • The formal implementation with Coq.
  • A more accessible implementation in Haskell.

Both languages are functional. The functional programming paradigm is based on the lambda calculus. It means that all the computations are represented with pure functions. This property makes the mathematical verification of expressions much simpler. Verifying a pure function consists only in evaluating its output for a given input.

If you want to learn more about functional programming, begin with the lambda calculus. The Computerphile Youtube channel has an excellent video about it: https://youtu.be/eis11j_iGMs

Back to Simplicity, We’ll use the Haskell library. Haskell is a very powerful functional language. If you want to learn it, there is the following very good course online: http://learnyouahaskell.com/chapters. It is necessary to understand Haskell’s concepts and syntax to follow this article.

The article is organized into two parts:

  1. Simplicity code source, describe how the language works and investigates the Haskell implementation.
  2. Simplicity first transaction: a more practical part that lets you reproduce the official first transaction by Russel O’Connor.

This article was written with the help of Nicolas Cantu. Thanks also to Russel’O’Connor for answering questions and sharing the transcript of the first transaction.


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:

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.

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.

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.

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:

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.
  • The Either a b type, where a and b are types of class TyC . 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:

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

⚠ 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.

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.
  • All the types resulting from the sum of two other Simplicity types : SumR takes two TyReflect ( a and b ) and returns Either a b .
  • 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:

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:

And canonically map it to the Haskell type Bool :

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. Let’s see how we can manipulate these types with combinators. The Haskell implementations of Simplicity’s combinators are located in the Core.hs file.

Identity

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

Composition

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

Constant Unit

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

Left Injection

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

Right Injection

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 the case is a reserved keyword in Haskell. That’s why, the Haskell implementation of Simplicity’s case term is calledmatch .

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

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

Take

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

Drop

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

A term is a composition of combinators. So, a simplicity program is just a term applied to a stack.

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.

Simplicity program as Merkle tree

A Simplicity program can be represented as a set of combinators. In other words, a Simplicity program is a set of other Simplicity programs.

On the blockchain, it works a bit like a P2SH. But you do not pay to the hash of the Simplicity script. Instead of hash, a script is represented by the root of its Merkle tree. This tree is obtained by cutting the program into several branches. When the script is executed, the program is executed branch after branch.

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 first transaction

The purpose of this section is to reproduce Russel 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:

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

⚠ The official example can’t be used in production. Its purpose is pedagogical: Simplicity will evolve, and the official example will become obsolete. Moreover, key generation is not done in a cryptographically secure way.


A word on Schnorr signature

The schnorr digital signature has the advantage of being mathematically simple. Given a message m, the Schnorr signature s is defined by the following expression:

Where:

  • k is a random nonce.
  • e = H(m) where H is a hashing function.
  • d is the private key. Public key p = d * G where G is a generator.

The schnorr signature of a message m is the pair (e, s) . it allows doing amazing things. Let’s see an example: multi-signature.

Let’s imagine that Alice and Bob want to sign the message m . If they sign the hash of the message e = H(m) :

Where Ka and Kb are the random nonces choose by Alice and Bob. Da and Db are the private keys.

Then, if we add Sa and Sb :

So the sum of two signatures is the signature of the message using the sum of the private keys as private key.

Other examples of Schnorr signatures applications: https://www.youtube.com/watch?v=XKatSGCZ-gE


Set up

The installation process is the same as the one we saw in the first part.

Let’s go

Environnement configuration:

Create a new address:

And generate some blocks:

Then use ghci with the Haskell package Simplicity.

Firstly, let’s import all the necessary Haskell modules:

This is a lot of modules, let’s change the prompt:

Then, create asProgram and makeBinaryProgram used to convert the Simplicity program to Binary.

The putTermLengthCode from the Simplicity.Bitcoin.Jets module just transform Simplicity expressions into a binary format.

Let’s create a public key for our Schnorr signature. We’ll use the XOnlyPubKey expression and a trivial public key value used for the official example.

Now we can create our program.

The binaryDummySig is the binary of our program. If you take a look at the pkwCheckSigHashAll source code, you’ll see that it returns a Term = a set of Simplicity combinators. You can also see that the signature (i.e the sig parameter) is the result of the witness function. It means that the sig will be excluded from the commitment Merkle Root computation. That’s why we use a dummysig for calculating our binaries.

Next, let’s define the parseBinaryProgram . It takes a binary program and returns its Simplicity expressions.

Next, compute the Merkle root of our program:

Let’s display the hash with a function showHash .

To resume, at this stage, the cmr expression will return the Merkle Root of the Simplicity program that verifies dummysig with pubkey .

You can use showHash to display the result on your screen:

What we computed here is the Merkle root of the pkwCheckSigHashAll program for the public key pubkey . The signature doesn’t count here.

For the next steps, we’ll use the module Data.Text , let’s import it:

And then use bech32 to generate an address:

hrp is the prefix of bitcoin addresses. We also need the segwit version (non-valid one in this example) to generate the address.

The encode, dataPartFromWords and dataPartFromBytes expressions come from the Codec.Binary.Bech32 module.

The expression above generates the bitcoin address according to the commitment Merkle Root of the program. Let’s display it:

You’ll probably get something like that:

Copy the address and put ghci in the background with ctrl + Z . Do not leave GHCI. We’ll use bitcoin-cli for a while.

Here we committed to a specific Simplicity program that verifies the Schnorr signature of a transaction digest for pubkey . This is the part very similar to P2SH.

From our newly transaction, let’s create a PSBT:

Then, update the outputs of the PSBT:

We can decode the transaction and visualize its details:

The transaction looks like the following:

We need the input’s txid of the PSBT transaction. You can use jq to select it:

You’ll probably get the following result:

Copy this value, we’ll need it soon. We also need the output script hex:

Who looks like this:

Then, go back on ghci with the fg command, do not forget to import the Simplicity package.

First, you can remove the Data.Text module.

Store the input txid inside an expression and create three other ones to store input configuration:

Same thing with the output script hex:

Some utility functions:

And the mkSigHashAll expression (a big one):

The mkSigHashAll expression uses Bitcoin primitives to get the transaction digest of tx. Here, the Haskell code do the same thing that the Simplicity program. We need to do that to get the signature (Note that it is not a secure way to get the digital signature. A real example should use libecp256k1 to compute the signature).

So we need to create the transaction to digest. Let’s create it using the values stored sooner.

First, create the inputs:

And the outputs:

Then we put them together in one transaction.

We can now apply mkSigHashAll to our transaction.

sigHashAll is the transaction digest of tx .

Then, Russel used some maths to get the digital signature using the transaction digest and the public key.

Note that the nonce’s value k here is not chosen randomly. That’s not a secure way to do things, don’t do that in production!

Then we can compute the binaries for the signature sig .

The binary is the simplicity program binaries that verify the schnorr signature sig for the public key pubkey .

Like the first time, we can display the Merkle root of the binary with showHash .

My Merkle root is the following:

The Commitment Merkle root hash must be the same that the first one. Only the sig parameter has changed, so the cmr is not affected.

Let’s write some utils function to “zoom” inside the binaries.

The assembly is our binaries write in a human-readable format.

The showList an expression can be used to see all the combinators of our program assembly. However, it can be very long, so the showRange expression can be used to select (and visualize) a range inside the program’s assembly.

Let’s take the first 10 expressions of the program:

This way you can explore the program in its Simplicity form. You can see here how Simplicity manipulates a stack of data.

Returning to our transaction, let’s write the binaries inside a file, we’ll use HAL to put the binaries inside a transaction:

Then leave ghci with CTRL+D and use HAL to create the raw transaction from the binary file.

First, we’ll use hal psbt edit to add the binary’s content to the witness data of our partially signed transaction.

Then transform our partially signed transaction to a transaction using hal psbt finalize .

You can display the transaction with decoderawtransaction :

Then let’s send the transaction on the network. This is when the Simplicity program runs.

Here, it is possible that you get an error code 26: mandatory-script-verify-flag-failed . This error can occur when the Merkle root of the Simplicity binaries used to build the transaction is not equal to the Merkle root of your program.

Next, validate the transaction by generating a block.

We’re now able to get our transaction.

If we select the hex value of the transaction and then decode it, we can see the outputs and the inputs.

We get the following result:

Congratulation! You’ve sent your first Simplicity transaction.

The txinwitness contains our simplicity program and the transaction is confirmed on the regtest network. The binaries of the REDEEM_TX must match the commitment Merkle root previously computed.


Simplicity and Elements

It is theoretically possible to repeat the above process on an Elements network. The difference with bitcoin is the way the transactions are constructed. In particular, if you want to use Elements’ confidential transactions, the primitives of the Simplicity library for Elements allow you to handle them.


See you soon :)

Chain Accelerator

Global network for blockchain startup acceleration

Louis Singer

Written by

Chain Accelerator

Global network for blockchain startup acceleration

More From Medium

More from Chain Accelerator

More on Elements from Chain Accelerator

More on Blockchain from Chain Accelerator

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store