Pact Core Concepts Part 2 - Learn Pact in 20 Minutes
The Pact Core concepts series is a companion guide to the Real World Pact teaching repository. Written by Thomas Honeyman, a senior engineer at Awake Security, the series provides a first look at the essential concepts needed to write and test Pact programs on the scalable Chainweb blockchain. The series is a wonderful way to begin your Pact journey; after you’ve finished reading it, you are well-equipped to dive into the projects in the Real World Pact repository.
2. Learn Pact in 20 Minutes
Pact is a wonderful language for smart contract development. It boasts a slew of features specific to the blockchain environment — and, notably, a lack of features that tend to produce costly mistakes — and it's such a small language that you can master it in a few months.
However, Pact has a steep initial learning curve because so many concepts are a departure from standard programming languages you've seen before. It's an immutable, non-Turing-complete Lisp language with language constructs specifically designed for smart contract programming powerful features like fine-grained access control, sophisticated multi-signature authorization, on-chain data storage, cross-chain state transfers, and formal code verification. That's a lot to learn — especially if you're new to blockchain development!
This article is a crash course in the Pact language. We'll go through as many Pact snippets as we can in 20 minutes of reading time; by the time we're through you won't be an expert, but you'll be able to understand most Pact code you see in the wild. I recommend that you follow along in a Pact REPL session and/or by writing Pact code in a file named
twenty-minutes.pact
. You can see installation instructions on the Pact repo.This article was written with Pact 4.6. If you use Nix, you can drop into a Pact REPL session with the same executable I used in one line via pact-nix:
1$ nix run github:thomashoneyman/pact-nix#pact-4_6_0 2pact> 3
Alternately, you can get the executable in your shell:
1$ nix develop github:thomashoneyman/pact-nix#pact-4_6_0 2$ pact --version 34.6.0 4
Without further ado: let's go!
Pact code is a list of expressions, where an expression is a literal, s-expression, atom, or reference. Pact code can contain comments; these begin with a semicolon and are ignored.
Pact has several basic literal types:
1"hello" ; a sequence of characters (string), created with quotes 2'hello ; a unique string literal (symbol), created with a single quote 342 ; an unbounded positive or negative integer 4100.25 ; an unlimited precision positive or negative decimal 5true ; a true or false (boolean) value 6
It also supports lists and objects. Lists are ordered sequences of values enclosed in square brackets. Objects are key-value pairs enclosed in curly braces where keys are strings.
1[1 2] ; a homogeneous list of integers 2[1 true] ; a heterogeneous list 3[1, true] ; commas are optional in lists 4{ "a": 1, "b": [1 2] } ; objects have string keys 5{ 'a: 1, 'b: [1, 2] } ; symbols are string literals so they're ok too 6
S-expressions are enclosed in parentheses and begin with either an atom, reference, or a special form. An atom is a non-reserved bare identifier (ie. a variable) such as the name of a function.
When an s-expression begins with an atom or reference then it is interpreted as function application. Function parameters are separated by spaces.
1; an s-expression that begins with an atom (the 'reverse' identifier) and is 2; evaluated by applying the referenced reverse function to a list literal 3(reverse [1 2]) ; [2 1] 4 5; an s-expression that begins with a reference (atoms separated by periods) 6; and is evaluated by applying the balance function from the coin module to 7; a string literal 8(coin.balance "k:abc123") ; 1.23223 9
When the s-expression begins with a special form then the special form is interpreted. Pact has many of these which we'll explore later.
Pact has a type system. You can inspect the type of an expression — here are a few common ones, though Pact has more that we'll see later on:
1(typeof "hello") ; "string" 2(typeof 'my-key) ; "string" 3(typeof 1234) ; "integer" 4(typeof 1.234) ; "decimal" 5(typeof true) ; "bool" 6(typeof { "a": 1 }) ; "object*" 7(typeof []) ; "[<a>]", ie. a list of values of type 'a' 8(typeof reverse) ; "defun", ie. a function 9
You can bind values to variables with
let
. The bound variables only exist within the scope of the let body. Pact does not allow mutation, so you can't reassign variables.1(let 2 ; The first set of parentheses is a list of binding pairs — that's right, a 3 ; different list type from the syntax used to create list literals! Variables 4 ; can be annotated with their types with ':' 5 ( 6 (x:integer (+ 1 1)) ; bind the integer 2 to the name 'x' 7 (y:[integer] [2 3 4]) ; bind the list of integers [2 3 4] to the name 'y' 8 ) 9 ; After the binding pairs comes the let body, which must be an expression and 10 ; which can refer to the binding pairs. Below we take the first 2 (the value 11 ; of 'x') from the list 'y'. 12 (take x y) 13) 14; [2 3] 15
A binding pair in a
let
cannot refer to other binding pairs. For that, use let*
— but note that this incurs a higher gas cost because it is less performant, and you should prefer let
when possible.1(let* 2 ; we can refer to 'x' in 'y' because we used let*, but this will produce a 3 ; "cannot resolve x" error if you only use let — try it yourself! 4 ( (x:integer 2) (y:integer (* x x)) ) 5 (+ y x) 6) 7; 6 8
You can destructure objects with the
bind
function and the :=
binding operator:1(bind 2 ; First, we supply the object that we want to destructure 3 { "a": 1, "b": 2, "c": 3, "d": 4 } 4 ; Then, we bind the value of the "a" key to the name 'a-value', and the 5 ; value of the "d" key to the name 'd-value'. 6 { "a" := a-value, "d" := d-value } 7 ; Then comes the body of the bind expression, where we can refer to the atoms 8 ; we bound, just as we've previously done with 'let' expressions. 9 (+ a-value d-value) 10) 11; 5 12
Pact supports anonymous functions via the
lambda
keyword. A lambda takes a list of arguments and a body, which is an expression that can access those arguments (the same as a let or bind).Remember to try these snippets in a Pact REPL!
1(let 2 ; We bind the anonymous function denoted by the lambda to the name 'square'. 3 ; This function takes one argument (x) and multiplies it by itself. 4 ( (square (lambda (x) (* x x))) ) 5 (square 13) 6) 7; 169 8
Lambdas are especially useful in list-processing functions like
map
, fold
, filter
, and zip
. These list processing functions are used pervasively because Pact is not Turing-complete; that means it does not support loops or recursion.The
map
function applies a function of one argument to every value in a list.1; Here we use map to square every integer in a list. 2(map (lambda (x:integer) (* x x)) [1 2 3]) 3; 1 4 9 4
The
filter
function removes items from a list according to a predicate function (ie. a function that returns true
or false
).1; Here we use filter to remove values less than 10. 2(filter (lambda (x:integer) (>= x 10)) [1 10 100 1000]) 3; [10 100 1000] 4
The
zip
function merges two lists together with a combining function. The combining function takes two arguments, which represents the two list values at a given index, and returns a new value to place in the resulting list. If the list lengths differ then the resulting list is the length of the shortest list.1; Here we use zip to turn two lists into a list of objects. 2(zip (lambda (x:integer y:integer) { 'x: x, 'y: y }) [1 2] [3 4 5]) 3; [ { "x": 1, "y": 3 }, { "x": 2, "y": 4 } ] 4
The
fold
function reduces a list of values to a value with a reducer function and an initial accumulator value. The reducer function takes two arguments — the accumulated value so far and the next value from the list — and returns a new accumulator value.1; We can sum a list by adding all the values together. 2(fold (lambda (acc:integer val:integer) (+ acc val)) 0 [2 3 4]) 3; 9 4 5; Similarly, we can multiply all values in a list. 6(fold (lambda (acc:integer val:integer) (* acc val)) 1 [2 3 4]) 7; 24 8 9; Or we can convert a list of integers into a string by converting each integer 10; into a string and concatenating them together. 11(fold (lambda (acc:string val:integer) (+ acc (int-to-str 10 val))) "" [1 2 3]) 12; "123" 13 14; Pact supports partial application, so we could rewrite our sum and fold 15; functions to not use lambdas. 16(fold (+) 0 [2 3 4]) ; 9 17(fold (*) 1 [2 3 4]) ; 24 18
We now know how to create literal values, bind them to names (ie. create atoms), create anonymous functions, and use them to manipulate collections of values. There are many more utilities for working with literal values and collections in Pact which you can find in the built-in functions section of the Pact documentation. We'll see many more of them in this article, but that's a handy reference!
Next, let's turn to conditional logic. Pact supports two forms of conditional logic: the
if
and cond
functions. In Pact, functions are total, which means they always return a value. You can't handle only the true
condition of an if
statement, for example, unlike languages like JavaScript. Later' we'll see how to terminate a computation with an exception.The first conditional logic function is the traditional if-else expression. It takes a condition, an expression to return if true, and an expression to return if false.
1(if 2 ; The first expression is a true/false condition 3 (= 4 (+ 2 2)) 4 ; If the condition is true, then the first expression is returned 5 "All is well" 6 ; If not, then the second expression is returned 7 "Something horrible has gone wrong" 8) 9; "All is well" 10
The second conditional logic function is
cond
, which is technically a Pact special form rather than a normal function. It takes a series of if-else if-else expressions and turns them into nested if statements; it can't do anything that if-else can't do, but it's more convenient when you have many conditions.1(let 2 ((x:integer 5)) 3 ; cond takes a list of condition-return pairs (COND VALUE) and a default 4 ; value where if the condition is true then the value is returned, and if 5 ; none of the pairs match then the final default case is returned. 6 (cond 7 ((= x 1) "X is 1") 8 ((> x 10) "X is greater than 10") 9 ((> x 1) "X is greater than 1") 10 "X is less than 1" 11 ) 12) 13; "X is greater than 1" 14
Conditional logic is useful to insert branches in your code, but it shouldn't be used for errors. If an error has occurred in your code — like a function received an invalid value — then you should use the
enforce
function to throw an exception. In a blockchain environment this aborts the transaction.1(enforce true "This message is shown on error") 2; true 3 4(enforce false "This message is shown on error") 5; <interactive>:0:0: This message is shown on error 6; at <interactive>:0:0: (enforce false "This message is shown on error") 7
The
enforce
family of functions provide the only non-local exit allowed by Pact (the only exception to Pact's requirement that functions are total, ie. return a value for all inputs). There are other enforce
functions, such as enforce-keyset
, enforce-guard
, and enforce-one
.You can format strings with the
format
interpolation function.1(enforce (= 10 100) (format "The value {} is not equal to {}" [10, 100])) 2; <interactive>:0:0: The value 10 is not equal to 100 3
So far we've focused on Pact code that can be written anywhere. However, a large portion of Pact code can only be written inside of a module — the main unit of code organization in Pact. Defining a module in the REPL mimics deploying it to the blockchain, so we should start to use explicit transactions. You can begin, end, and roll back transactions in the REPL:
1(begin-tx) 2; "Begin Tx 0" 3(commit-tx) 4; "Commit Tx 0" 5(rollback-tx) ; Undoes the transaction effects, but not the transaction counter 6; "Rollback Tx" 7
It's a good practice to use
begin-tx
and commit-tx
in the REPL roughly around the points where you expect to do the same on Chainweb.A minimal Pact module has a unique name atom (no other modules on-chain can share the name), a governance function, and a body containing the API and data definitions.
1(begin-tx) 2(module example GOVERNANCE 3 (defcap GOVERNANCE () true) 4 (defconst TEN 10)) 5; "Loaded module example, hash G1MU80sMEUZxC2E5NQINJ7Pfe03S8nd1I-gdVZ_WPrk" 6(commit-tx) 7
defconst
introduces a constant. We can access it by referring to its name within the module:1(* 2 example.TEN) 2; 20 3
defcap
introduces a capability. A capability is a predicate function used for access control. The governance capability controls smart contract upgrades: you can only upgrade a module (ie. to fix a bug) if its governance function is satisfied. For example, this module cannot be upgraded:1; This module cannot be upgraded because the capability cannot be satisfied 2(module example GOVERNANCE 3 (defcap GOVERNANCE () false)) 4
You can also govern a module via a keyset reference. A keyset reference is a string identifying a particular keyset that has been defined on-chain; you can't define a module with a reference to a keyset that doesn't exist yet.
1(module example "my-keyset" 2 (defconst TEN 10)) 3; <interactive>:0:0: No such keyset: 'my-keyset 4
So what is a keyset? A keyset combines a list of public keys with a predicate function that determines how many of those keys must have signed a payload for the keyset to be satisfied. You can't write a keyset directly in Pact code, so here are a few keysets in their JSON form:
1// all keys must sign to satisfy the keyset 2{ "keys": ["pubkey1", "pubkey2"], "pred": "keys-all" } 3 4// any key can be used to sign to satisfy the keyset 5{ "keys": ["pubkey1", "pubkey2"], "pred": "keys-any" } 6 7// if the predicate function is omitted it's the same as 'keys-all' 8{ "keys": ["pubkey1", "pubkey2"] } 9 10// the same is true if the keyset is just a list of keys 11["pubkey1", "pubkey2"] 12 13// you can also specify a predicate function from a module for more 14// sophisticated situations 15{ "keys": ["pubkey1", "pubkey2"], "pred": "example.my-keyset-predicate" 16
A keyset reference is a string identifying a keyset that has been registered on Chainweb with
define-keyset
. We can't write a keyset directly in Pact, but we can write one in JSON, attach it to a transaction, and then read the data from the transaction in Pact. Let's do that in the REPL:1; Pact code is always executed as part of a transaction. The env-data function 2; sets a JSON payload on that transaction. 3(env-data { "my-keyset": ["pubkey1"], "my-decimal": 1.12 }) 4; "Setting transaction data" 5 6; We can then read from the transaction payload using the read-msg 7(read-msg "my-keyset") ; ["pubkey1"] 8(read-msg "my-decimal") ; 1.12 9 10; However, read-msg can only decipher simple Pact types. It's not suitable 11; when you have a more complicated type like a keyset in mind. 12(typeof (read-msg "my-keyset")) ; "[*]" 13 14; Instead, you can use the read- function that matches the type you are 15; trying to decode. 16(typeof (read-keyset "my-keyset")) ; "keyset" 17 18; Be careful! If you assert the wrong type, weird things can happen. 19(read-integer "my-decimal") ; 1 20(read-integer "my-keyset") 21; <interactive>:0:0: read-integer: parse failed: Failure parsing integer: Array [String "pubkey1"]: ["pubkey1"] 22
A brief aside: on Chainweb, all modules, interfaces, and keyset references must be deployed to a namespace. A namespace is a unique prefix;
free
and user
can be used by anyone, while anyone can create a principal namespace. Principal namespaces are generated, though, so if you want a vanity namespace like my-app
it must be granted by the Kadena team.The REPL doesn't include any namespaces because it isn't an actual blockchain. Some Pact code, though, requires that you are in a namespace, such as defining a keyset reference. We can define and enter a namespace with
define-namespace
and namespace
:1(define-namespace 'free (read-keyset 'my-keyset) (read-keyset 'my-keyset)) 2; "Namespace defined: free" 3 4(namespace 'free) 5; "Namespace set to free" 6 7; We should go ahead and "re-deploy" our example module to the 'free namespace 8; so we are faithfully mimicking Chainweb. 9(module example GOVERNANCE 10 (defcap GOVERNANCE () false)) 11; "Loaded module free.example, hash sT4jbDj2GhE_AGZG4A93FT5aAb-5Wfi_QTLS1WkCbJk" 12
One last thing: you can only register a keyset on-chain with
define-keyset
in a transaction that satisfies the keyset (ie. was signed by all required keys). You can simulate signing a transaction with the env-sigs
function.1; This transaction is signed with the private key associated with 'pubkey1' and 2; the signature is not scoped to any capabilities 3(env-sigs [ { "key": "pubkey1", "caps": [] } ]) 4; "Setting transaction signatures/caps" 5 6; This transaction is also signed by pubkey1, but this time the signer has also 7; signed for the GOVERNANCE capability. 8(env-sigs [ { "key": "pubkey1", "caps": [ (free.example.GOVERNANCE) ] } ]) 9; "Setting transaction signatures/caps" 10
We can now put it all together and define a keyset:
1; These two lines would not be in the smart contract code, but rather would 2; be on the transaction itself. 3(env-data { "admin-keyset": [ "pubkey1" ] }) 4(env-sigs [ { "key": "pubkey1", "caps": [] } ]) 5 6; First we enter a namespace 7(namespace "free") 8 9; Then we define our keyset reference 10(define-keyset "free.admin-keyset" (read-keyset "admin-keyset")) 11 12; Then we use it to govern the module we are deploying, which means the module 13; can only be upgraded (on Chainweb) in a transaction signed by this keyset. 14(module example "free.admin-keyset" 15 (defconst TEN 10)) 16; "Loaded module free.example, hash oxlUPEWJuToByJ47pe7RwyG8BDiSmfFuJ_QOoxrQ7hc" 17
You can use a keyset reference to govern a Pact module, or you can use a governance capability and the
enforce-keyset
function, which will throw an exception if the given keyset is not satisfied by the transaction signatures.1; In practice this is the same as the previous module we defined, except that 2; an upgrade would sign with the GOVERNANCE capability instead of just signing 3; the transaction in general. 4(module example GOVERNANCE 5 (defcap GOVERNANCE () (enforce-keyset "free.admin-keyset"))) 6
You can define more than constants and capabilities in a Pact module. You can also define functions, object types (schemas), database tables, formal verification properties, and more.
Functions are defined with the
defun
special form. They have a name, a list of arguments, and a function body. You can add type annotations for the arguments and the return type.1(module example "free.admin-keyset" 2 (defun square:integer (x:integer) 3 (* x x)) 4 (defun sum:integer (xs:[integer]) 5 (fold (+) 0 xs)) 6) 7; "Loaded module free.example, hash rIBzCpDKTx0bC3FH84q98U7Emt0yzRJ7qoNHHJaAORc" 8 9(free.example.square 3) ; 9 10(free.example.sum [1 2 3]) ; 6 11
Modules, functions, and many other Pact forms can have documentation strings:
1(module example "free.admin-keyset" 2 @doc "An example module" 3 4 (defun square (x:integer) 5 @doc "A function to square integers" 6 (* x x)) 7 8 (defun sum (xs:[integer]) 9 @doc "A function to sum a list" 10 (fold (+) 0 xs)) 11) 12
You can write multiline strings with backslash escapes.
1(module example "free.admin-keyset" 2 @doc "An example module that has a very long \ 3 \description of its contents." 4 5 (defun add1 (x:integer) (+ 1 x)) 6) 7
Pact modules can also define object types (schemas) with the
defschema
special form.1(module example "free.admin-keyset" 2 ; A schema takes an atom that is used to refer to this type and then 3 ; a list of fields, optionally associated with types. 4 (defschema person 5 @doc "Schema for a person object type" 6 first:string 7 last:string 8 age:integer) 9 10 ; We can then use this new type in our code. 11 (defun get-age:integer (x:object{person}) 12 (at 'age x)) 13 14 (defun can-drink-america:bool (x:object{person}) 15 (>= (get-age x) 21)) 16) 17 18(free.example.get-age { "first": "Eliud", "last": "Kipchoge", "age": 38 }) 19; 38 20 21; Specifying the type gets us better error messages from the Pact interpreter 22(free.example.get-age { "first": "Eliud", "age": 38 }) 23; <interactive>:10:26: Missing fields for {person}: [last:string] 24
Pact is a unique language in that it allows you to define and interact with database tables directly in your modules with the
deftable
special form. Pact has many functions for dealing with databases, which we'll see below.1(begin-tx) 2(namespace 'free) 3(module example GOV 4 (defcap GOV () (enforce false "No governance.")) 5 6 (defschema runner 7 country:string 8 age:integer) 9 10 ; We can use this schema to define a table for persistent on-chain storage. 11 ; Tables always have string keys, and each row is an object with the 12 ; specified schema type. 13 (deftable runner-table:{runner} 14 @doc "A table for runner ages") 15 16 (defun add-runner (name:string entry:object{runner}) 17 ; We can insert rows into the table (will fail if the key already exists) 18 (insert runner-table name entry)) 19 20 (defun get-runner:object{runner} (name:string) 21 ; We can read a value from the table (will fail if the key is missing) 22 (read runner-table name)) 23 24 (defun get-runner-age:integer (name:string) 25 ; with-read is a combination of read and bind: it lets you read a row and 26 ; bind its fields to variable names. 27 (with-read runner-table name 28 ; here we bind only the "age" field to the age name 29 { "age" := age } 30 ; and then we return it. 31 age)) 32 33 (defun get-runner-age-default:integer (name:string) 34 ; with-default-read is like with-read, but it returns the specified default 35 ; value instead of throwing an exception on failure. 36 (with-default-read runner-table name 37 ; We provide a default value in the case the row cannot be found. 38 { "age": 0, "country": "Uzbekistan" } 39 { "age" := age } 40 age)) 41 42 (defun under-30:[object{runner}] () 43 ; We can also select many rows using a filter function. 44 (select runner-table (where 'age (> 30)))) 45 46 (defun names:[string] () 47 ; Or read all the keys used in the table 48 (keys runner-table)) 49) 50; "Loaded module free.example, hash 9uMpahkbL4dgdUHe1tZPVwQy2A4A-kWhsCSeQSeK7A0" 51
Defining a table is not quite enough — we also need to use the
create-table
top-level function outside our module to create the table. Without it you'll see an error:1(free.example.names) 2; <interactive>:21:4: : Failure: Database exception: query: no such table: USER_free.example_runner-table 3; at <interactive>:21:4: (keys (deftable runner-table:(defschema runner [country...) 4; at <interactive>:0:0: (names) 5
But everything is OK if we remember to create the table.
1(create-table free.example.runner-table) 2; "TableCreated" 3
We can now interact with our database.
1(free.example.add-runner "Eliud Kipchoge" { 'age: 38, 'country: "Kenya" }) 2; "Write succeeded" 3(free.example.add-runner "Brigid Kosgei" { 'age: 29, 'country: "Kenya" }) 4; "Write succeeded" 5(free.example.get-runner "Eliud Kipchoge") 6; { "age": 38, "country": "Kenya" } 7(free.example.get-runner "John Titus") 8; <interactive>:15:4: read: row not found: John Titus 9(free.example.under-30) 10; [{ "age": 29, "country": "Kenya" }] 11(free.example.names) 12; ["Brigid Kosgei" "Eliud Kipchoge"] 13(free.example.get-runner-age "John Titus") 14; <interactive>:15:4: read: row not found: John Titus 15(free.example.get-runner-age-default "John Titus") 16; 0 17
In Pact, data access is constrained to only functions defined in the same module or in a transaction where governance of the module has been granted. Since we're in the module deployment we can still freely access our tables outside the module:
1; This can only be run at the top-level because governance of the module has 2; been granted. 3(read free.example.runner-table "Brigid Kosgei") 4; { "age": 38, "country": "Kenya" } 5
However, once we commit our current transaction we can no longer acquire governance (since the governance capability is always
false
), and can no longer access tables directly. Tables should be accessed through functions instead.1(commit-tx) 2(read free.example.runner-table "Brigid Kosgei") 3; <interactive>:3:5: No governance 4
You'll find that most Pact modules consist of one or more databases, a selection of functions that manipulate the database, and a set of capabilities that control access to sensitive functions.
We have only a few minutes left, so let's talk about guards and capabilities. A guard is a predicate function over some environment that enables a pass-fail operation. The most typical guard is a keyset, like we've seen before. Assuming you've been following along in the REPL:
1; You can look at the contents of a keyset reference with describe-keyset 2(typeof (describe-keyset "free.example-keyset")) 3; "keyset" 4 5; You can enforce a keyset guard with enforce-keyset. The below will fail, 6; indicating that the transaction signatures do not satisfy the keyset. 7(env-sigs []) 8(enforce-keyset "free.example-keyset") 9; <interactive>:0:0: Keyset failure (keys-all): 'free.example-keyset 10 11; We can fix it by signing the transaction with the required keys. 12(env-sigs [ { "key": "pubkey1", "caps": [] } ]) 13(enforce-keyset "free.example-keyset") 14; true 15
Keysets are just one of several types of guard. Some functions expect to work with an arbitrary guard (not just keysets), so you can turn a keyset reference into a more general guard with
keyset-ref-guard
.1(typeof (keyset-ref-guard "free.example-keyset")) 2; "guard" 3
Arbitrary guards are enforced with
enforce-guard
.1(enforce-guard (keyset-ref-guard "free.example-keyset")) 2; true 3 4; Pact will interpret strings as keyset references by default, so you don't 5; actually have to use keyset-ref-guard with enforce-guard 6(enforce-guard "free.example-keyset") 7; true 8
The two most common guards are capability guards, which let you enforce that a particular capability has been acquired, and user guards, which lets you turn an arbitrary predicate function into a guard.
1(begin-tx) 2(module guard-example "free.example-keyset" 3 (defcap CAP_SUCCEED () (enforce true "succeed")) 4 (defcap CAP_FAIL () (enforce false "fail")) 5 (defun succeed () (enforce true "succeed")) 6 (defun fail () (enforce false "fail")) 7) 8 9(create-capability-guard (free.guard-example.CAP_SUCCEED)) 10; CapabilityGuard {name: free.guard-example.CAP_SUCCEED,args: [],pactId: } 11(typeof (create-capability-guard (free.guard-example.CAP_FAIL))) 12; "guard" 13(create-user-guard (free.guard-example.succeed)) 14; UserGuard {fun: free.guard-example.succeed,args: []} 15(typeof (create-user-guard (free.guard-example.fail))) 16; "guard" 17 18; Each guard can be enforced with enforce-guard. Note that the user guard 19; functions are simple predicates and can be called directly: 20(enforce-guard (create-user-guard (free.guard-example.succeed))) 21; true 22(enforce-guard (create-user-guard (free.guard-example.fail))) 23; <interactive>:5:17: fail 24 25; But capabilities must be "acquired" — they aren't simple functions. 26(enforce-guard (create-capability-guard (free.guard-example.CAP_SUCCEED))) 27; <interactive>:0:0: Capability not acquired 28(commit-tx) 29
You can acquire a capability with
with-capability
. However, capabilities are like database tables, in that access can only be granted by code within the same module or when the transaction has governance priviliges. We'll see more about with-capability
in a moment.Guards are especially useful because they can be stored in tables. For example, you can store a list of user accounts along with a guard that must be satisfied to modify the account. That way the only way to modify the database record is by having the keyset or satisfying the account guard.
1(begin-tx) 2(module guard-example GOV 3 (defcap GOV () true) 4 5 (defcap CAP_SUCCEED () (enforce true "succeed")) 6 7 (defcap UPDATE_ADDRESS (name:string) 8 (with-read account-table name 9 { "guard" := guard } 10 ; On this line we enforce that the guard associated with the account 11 ; is satisfied before processing the update. 12 (enforce-guard guard)) 13 14 (defschema account 15 name:string 16 address:string 17 ; a 'guard' is a suitable type for storage in a database 18 guard:guard) 19 20 (deftable account-table:{account}) 21 22 (defun succeed () (enforce true "succeed")) 23 24 (defun change-address (name:string new-address:string) 25 (with-capability (UPDATE_ADDRESS name) 26 (update account-table name { "address": new-address }))) 27) 28 29(create-table free.guard-example.account-table) 30 31; Recall that access to capabilities and tables is only allowed in module 32; functions or when governance is granted for a module. Right now we're in 33; the deployment transaction so we can freely access both. 34 35(insert free.guard-example.account-table "Rick" 36 { "name": "Rick" 37 , "address": "1111 Pine St" 38 , "guard": (create-user-guard (free.guard-example.succeed)) 39 }) 40; "Write succeeded" 41 42(insert free.guard-example.account-table "Morty" 43 { "name": "Morty" 44 , "address": "1111 Pine St" 45 , "guard": (create-capability-guard (free.guard-example.CAP_SUCCEED)) 46 }) 47; "Write succeeded" 48 49; We can change Rick's address because it is protected by a guard that 50; always evaluates to 'true' 51(free.guard-example.change-address "Rick" "1234 Pine St") 52; [true "Write succeeded"] 53 54; We can't just change Morty's address because it is protected by a 55; capability — one that is always true, but still must be acquired. 56(free.guard-example.change-address "Morty" "1234 Pine St") 57; <interactive>:15:8: Capability not acquired 58 59; We can acquire a capability via with-capability, but not at the 60; top-level. We'll do it inside a let instead. 61(let ((_ 0)) 62 ; with-capability attempts to acquire a capability, failing if the 63 ; predicate function fails (for example, if the capability required 64 ; a signature on the transaction and there was none). in this case, 65 ; the predicate for the capability always returns true, so we're ok. 66 (with-capability (free.guard-example.CAP_SUCCEED) 67 (free.guard-example.change-address "Morty" "1234 Pine St"))) 68; [true "Write succeeded"] 69 70(commit-tx) 71
There are a few important functions for working with capabilities. We've seen
with-capability
, which acquires a capability for the scope of its body, allowing you to take sensitive actions. There is also require-capability
which enforces that a capability has been acquired with with-capability
.1; Require that CAPABILITY has already been acquired with with-capability for 2; subsequent lines of code to run — essentially, enforce the capability. 3(require-capability (CAPABILITY)) 4 5; It's like to writing this: 6(enforce-guard (create-capability-guard (CAPABILITY))) 7
If you want all the nitty-gritty details on how the Pact capability system works, please see the Capability Theory in Pact wiki entry.
And with that our time is up! You should be able to get the gist of most of the Pact code you encounter in the wild, although you certainly won't be proficient from simply scanning these snippets.
Writing Pact is a different experience from reading it because you both must understand Pact and solve your problem at the same time, translating the solution into usable Pact code.
For more Pact material you may want to check out:
- The Pact language documentation
- The Real World Pact project series
Have fun writing some Pact!
Author:
Thomas Honeyman
Senior Engineer
Date:
Apr 27, 2023
Read:
28 mins read
Explore more tutorials
Tutorial
Building a voting dApp with Pact and React
Jan 20, 2023
Jermaine Jong
Software Engineer
Tutorial
Pact Core Concepts Part 1 - Introduction to Blockchain Development with Kadena
Apr 20, 2023
Thomas Honeyman
Senior Engineer
Tutorial
Pact Core Concepts Part 3 - Testing and Formal Verification in the Pact REPL
May 04, 2023
Thomas Honeyman
Senior Engineer