I tested Elements — Simplicity

Louis Singer
ON-X Blockchain (Chain-Accelerator)
20 min readMay 6, 2020

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 help 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 Russell O’Connor.

This article was written with the help of Nicolas Cantu. Thanks also to Russell’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:

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.
  • 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:

-- The following expressions' types don't have an instance of the TyC classlist :: [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 instanceemptyTuple :: ()
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.
  • 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:

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

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:

  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:

s = k - ed

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) :

Sa = Ka - (e * Da)
Sb = Kb - (e * Db)

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 :

Sa + Sb = Ka + Kb - e * Da - e * Db
= (Ka + Kb) - e * (Da + Db)

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

s(k, d) = k - e * d
# property allowing multisignatures
s(k1, d1) + s(k2, d2) = s(k1 + k2, d1 + d2)
# public key to verify = (d1 + d2)G

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:

$ cd $HOME
$ ./elementsEnv.sh

Create a new address:

$ ADDRESS=$(btc getnewaddress)

And generate some blocks:

$ btc generatetoaddress 102 $ADDRESS

Then use ghci with the Haskell package Simplicity.

ghci -package Simplicity

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

:m + Simplicity.Bitcoin.Jets Simplicity.LibSecp256k1.Schnorr Simplicity.Serialization Simplicity.MerkleRoot Simplicity.Digest Codec.Binary.Bech32 Simplicity.Bitcoin.Programs.CheckSigHashAll.Lib Simplicity.Digest Simplicity.Bitcoin.DataTypes Simplicity.Bitcoin.Dag Simplicity.Bitcoin.Inference

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

:set prompt  " > "

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

let asProgram = id :: a () () -> a () ()
let makeBinaryProgram = putTermLengthCode . asProgram

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.

let order = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
let priv = order `div` 2
let pubkey = XOnlyPubKey 0x00000000000000000000003b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63

Now we can create our program.

-- Signature to verify with pubKey
let dummysig = Sig 0 0
-- Create the binary of the simplicity program pkwCheckSigHashAll applied to pubkey and dummysig
let binaryDummySig = makeBinaryProgram $ pkwCheckSigHashAll pubkey dummysig

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.

let parseBinaryProgram bp = let Right x = evalStreamWithError getTermLengthCode bp in asProgram x

Next, compute the Merkle root of our program:

let cmr = commitmentRoot . parseBinaryProgram $ binaryDummySig

Let’s display the hash with a function showHash .

let showHash = flip Numeric.showHex "" . integerHash256

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:

showHash cmr

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:

:m + Data.Text

And then use bech32 to generate an address:

let Right hrp = humanReadablePartFromText . Data.String.fromString $ "bcrt"
let simplictySegwitVersion = (-1) `mod` 32

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

let Right address = encode hrp (dataPartFromWords [toEnum simplictySegwitVersion] <> (dataPartFromBytes . Data.Serialize.encode $ cmr))

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:

address

You’ll probably get something like that:

"bcrt1lfvl3m3f0zea3s4smzapjm07258mxt8g7guut4rmmuqzzy60l7c4qfucdch"

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

TXID=$(btc sendtoaddress "bcrt1lfvl3m3f0zea3s4smzapjm07258mxt8g7guut4rmmuqzzy60l7c4qfucdch" 1.0001 "" "" false)

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:

PSBT=$(btc createpsbt "[{\"txid\": \"$TXID\", \"vout\": 1, \"sequence\": 4293967294}]" "[{\"$ADDRESS\": \"1.0\"}]")

Then, update the outputs of the PSBT:

PSBT=$(btc utxoupdatepsbt $PSBT)

We can decode the transaction and visualize its details:

btc decodepsbt $PSBT | jq 

The transaction looks like the following:

{
"tx": {
"txid": "b29d917349c5c9e5dbcbd11ca3c85d011c4d8462ea46afdbd18f336b05adfbff",
"hash": "b29d917349c5c9e5dbcbd11ca3c85d011c4d8462ea46afdbd18f336b05adfbff",
"version": 2,
"size": 83,
"vsize": 83,
"weight": 332,
"locktime": 0,
"vin": [
{
"txid": "6da5a566c22ec74f4a197e4a7cde48e9f4822796bb80b3030fd18626937b6f08",
"vout": 1,
"scriptSig": {
"asm": "",
"hex": ""
},
"sequence": 4294967294
}
],
"vout": [
{
"value": 1,
"n": 0,
"scriptPubKey": {
"asm": "OP_HASH160 21bb834e651666a16cb414879fb08f8d9e21a12f OP_EQUAL",
"hex": "a91421bb834e651666a16cb414879fb08f8d9e21a12f87",
"reqSigs": 1,
"type": "scripthash",
"addresses": [
"2MvKavEsw2tvJWMt8cjYjipo9DkVwarby16"
]
}
}
]
},
"unknown": {},
"inputs": [
{
"witness_utxo": {
"amount": 1.0001,
"scriptPubKey": {
"asm": "-1 4b3f1dc52f167b18561b17432dbfcaa1f6659d1e4738ba8f7be0042269fff62a",
"hex": "4f204b3f1dc52f167b18561b17432dbfcaa1f6659d1e4738ba8f7be0042269fff62a",
"type": "witness_unknown",
"address": "bcrt1lfvl3m3f0zea3s4smzapjm07258mxt8g7guut4rmmuqzzy60l7c4qfucdch"
}
}
}
],
"outputs": [
{}
],
"fee": 0.0001
}

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

btc decodepsbt $PSBT | jq ".tx.vin[0].txid" | tr -d '"'

You’ll probably get the following result:

386105c4a514fc6808f4d607734df2249300730c520e60b4e42636b6a33d03ca

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

btc decodepsbt $PSBT | jq ".tx.vout[0].scriptPubKey.hex" | tr -d '"'

Who looks like this:

a91421bb834e651666a16cb414879fb08f8d9e21a12f87

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

fg

First, you can remove the Data.Text module.

:m - Data.Text

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

# Replace inputTxid by yours.
let inputTxid = "386105c4a514fc6808f4d607734df2249300730c520e60b4e42636b6a33d03ca"
let inputVout = 1
let inputValue = 100010000
let inputSequence = 4293967294

Same thing with the output script hex:

# replace my outputScript hex value 
let outputScript = "a91421bb834e651666a16cb414879fb08f8d9e21a12f87"
let outputValue = 100000000

Some utility functions:

let groupBy2 = Data.List.unfoldr (\l -> if null l then Nothing else Just (splitAt 2 l))
let readHex = fst . head . Numeric.readHex
let readTxid = readHex . concat . reverse . groupBy2
let parseScript = Data.ByteString.Lazy.pack . fmap readHex . groupBy2
let sigHashAllPrefix = Data.Digest.Pure.SHA.padSHA1 . Data.ByteString.Lazy.fromStrict $ Data.ByteString.Char8.pack "Simplicity\USSignature\GS" <> Data.Serialize.encode Simplicity.Bitcoin.Programs.CheckSigHashAll.Lib.sigAllCMR

And the mkSigHashAll expression (a big one):

let mkSigHashAll tx ix = bslHash . Data.Serialize.runPutLazy $ do { Data.Serialize.putLazyByteString sigHashAllPrefix; Data.Serialize.put (sigTxInputsHash tx); Data.Serialize.put (sigTxOutputsHash tx); Data.Serialize.putWord64be (sigTxiValue (sigTxIn tx Data.Array.! ix)); Data.Serialize.putWord32be ix; Data.Serialize.putWord32be (sigTxLock tx); Data.Serialize.putWord32be (sigTxVersion tx) }

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:

let inputs = Data.Array.listArray (0, 0) [SigTxInput { sigTxiPreviousOutpoint = Outpoint (Lens.Family2.review (Lens.Family2.over be256) (readTxid inputTxid)) inputVout, sigTxiValue = inputValue, sigTxiSequence = inputSequence }]

And the outputs:

let outputs = Data.Array.listArray (0,0) [TxOutput {txoValue = outputValue, txoScript = parseScript outputScript }]

Then we put them together in one transaction.

let tx = SigTx { sigTxVersion = 2, sigTxIn = inputs, sigTxOut = outputs, sigTxLock = 0 }

We can now apply mkSigHashAll to our transaction.

let sigHashAll = mkSigHashAll tx 0

sigHashAll is the transaction digest of tx .

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

let r = 0x00000000000000000000003b78ce563f89a0ed9414f5aa28ad0d96d6795f9c63 :: Simplicity.Word.Word256let schnorrTag = Data.Serialize.encode . bsHash $ Data.ByteString.Char8.pack "BIPSchnorr"let e = integerHash256 . bsHash $ schnorrTag <> schnorrTag <> Data.Serialize.encode r <> Data.Serialize.encode pubkey <> Data.Serialize.encode sigHashAlllet k = order `div` 2let sig = Sig r (fromInteger ((k + priv * e) `mod` order))

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 .

let binary = makeBinaryProgram $ pkwCheckSigHashAll pubkey 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 .

showHash . commitmentRoot . parseBinaryProgram $ binary

My Merkle root is the following:

"4b3f1dc52f167b18561b17432dbfcaa1f6659d1e4738ba8f7be0042269fff62a"

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.

let disassemble x = jetDag (x :: JetDag JetType () ())let stripTypeAnnotations = Lens.Family2.set (traverse . tyAnnotation) ()let showAssembly = zipWith (\i x -> show i ++ ": " ++ show (fmap (i -) x)) [0..]let assembly = showAssembly . stripTypeAnnotations . disassemble . parseBinaryProgram $ binarylet showList = mapM_ putStrLnlet showRange x i a = showList $ (take i . drop x) a

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:

showRange 0 10 assembly 
-- Result:
0: Unit ()
1: Injl () () () 0
2: Pair () () () 1 1
3: Pair () () () 2 2
4: Pair () () () 3 3
5: Pair () () () 4 4
6: Pair () () () 5 5
7: Pair () () () 6 6
8: Injr () () () 0
9: Pair () () () 8 8

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:

let writeBinaryToFile fn = Data.ByteString.writeFile fn . Data.Serialize.runPut . putBitStreamwriteBinaryToFile "/dev/shm/checksighashall.simplicity" binary

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.

PSBT_EDITED=$(hal psbt edit --input-idx 0 --final-script-witness $(hexdump -v -e'1/1 "%02x"' /dev/shm/checksighashall.simplicity) $PSBT)

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

REDEEM_TX=$(hal psbt finalize $PSBT_EDITED)

You can display the transaction with decoderawtransaction :

btc decoderawtransaction $REDEEM_TX

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

REDEEM_TXID=$(btc sendrawtransaction $REDEEM_TX)

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.

btc generatetoaddress 1 $ADDRESS

We’re now able to get our transaction.

btc gettransaction $REDEEM_TXID

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

HEX=$(btc gettransaction $REDEEM_TXID | jq '.hex' | tr -d '"')
btc decoderawtransaction $HEX

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 :)

--

--