Aeternity Smart Contract Development

aeterntity_logo

Aeternity is a public blockchain with its own crypto token AE. This blockchain goes beyond the Smart Contracts concept brought by Ethereum and adds new components into its protocol, such as state channels, oracles and human-readable names (AENS) for contract and user addresses. The main Aeternity software is built in Erlang, which is known for its high-availability and scalability characteristics. The Aeternity team has many experienced Erlang developers, which should help to build robust blockchain technology.

One of the interesting properties of the Aeternity platform is support of the Ethereum Virtual Machine, so that Solidity contracts can be deployed to Aeternity Network as well. Besides that, there are two new languages, called Sophia and Varna. In this article, we are going to use the Sophia language to implement our smart contracts.

Certification using Smart Contract

Let’s imagine some real life scenario, where trust would be guaranteed by blockchain technology using smart contracts.

Credentials and certificates are good examples to demonstrate the function of oracles, smart contracts and the public blockchain in general. In the area of certification, a person gets their certificate by doing trainings and taking exams. Every training may bring them a particular amount of points. These points eventually allow participating in an exam, which can result in a particular certification, in case they pass.

We model our system as a pair of the following contracts:

  • Examinee: stores achieved certification levels and trainings. It encapsulates business logic for granting access to the next level of certification
  • ExamVerifier: calculates the grade based on the exam points and potentially on external information. It will be our oracle entity

We assume that there must be an owner of this application. Such an owner deploys an Examinee contract for every new student who wants to start their certification path.

ExamVerifier can be deployed only one time and may exists as a singleton object. However, a particular category of certification may require another ExamVerifier instance. We leave this as an open question to the smart contract deployer. We will look at the deployment process later.

Oracles

smartphone_photo

Blockchain oracle(s) is not the company behind the SQL database and Java, but actually a source of information from the physical world. The Oxford Dictionary of English defines “oracle” as follows:

  • a place at which divine advice or prophecy was sought

  • a person or thing regarded as an infallible authority on something: for instance, “he reigned supreme as the Colonial Office’s oracle on Africa”

An oracle can be considered as an external expert or adviser. Oracles help blockchain contracts to facilitate particular business logic. They report some information, which may lead to a new state of a smart contract. Anybody can make a query to an oracle and expect a result. An example of an oracle query is “What will the weather be like tomorrow: sunny or rainy?”. Eventually, the oracle will report: sunny. Importantly, oracles do not predict the future, they report facts. There can be many oracles, so that the smart contract can ask the same question multiple times, and then decide which answer to trust. How to use oracles is up to the smart contract logic. IoT devices, such as sensors, can be another example of oracles. An oracle as an entity could be modelled as a smart contract as well. However, having them as part of the protocol, like in Aeternity, makes it easier to design an application on top of a blockchain. A good explanation of oracles can be found at the Aeternity blog.

Code in Sophia

Before we look at Sophia code, let us highlight a couple of points about this language to make reading the code easier.

Language highlights:

  • Sophia is very similar to the Reason language and based on OCaml
  • it uses indentations for code blocks like Python instead of curly braces like C
  • it requires special constructs to mutate the contract state
  • variable types are set on the right hand side; the colon sign is used to separate variable name and type
  • it has type aliases, generic types, pattern matching, closures

In general, it is much easier and nicer to write code in Sophia, than in Solidity.

Let’s start with with a simple contract.

ExamVerifier contract

contract ExamVerifier =
  // type aliases for convenience  
  type intOracle = oracle(int, int)
  type intQuery = oracle_query(int, int)

  // definition of a state object   
  record state =
    {o: intOracle,
     exams: map(address, intQuery),
     qfee: int}

  // constructor function
  public stateful function init(qfee: int) =
    let ttl = RelativeTTL(400)
    { o = Oracle.register(Contract.address, qfee, ttl),
      exams = {},
      qfee = qfee}

  // puts new pair of a user address and a query
  public stateful function askGrade(examinee: address, q: intQuery) =
    put(state{exams @ e = e{ [examinee] = q }  })    

  // returns a pointer to encapsulated oracle object
  public function getOracle(): intOracle = state.o

  // calculates exam grade based on earlier provided points. 
  // Oracle answer(exam grade) will be published to blockchain
  public stateful function gradeExam(examinee: address): option(int) =
    let query = Map.lookup(examinee, state.exams)
    switch(query)
      Some(q) =>
        let points : int = Oracle.get_question(state.o, q)
        let grade = calcGrade(points)
        Oracle.respond(state.o, q, grade)
        put(state{exams @ e = Map.delete(examinee, e)})
        Some(grade)
      _ => None
  
  // simple function for demo purposes to map points value to grade value
  private function calcGrade(points: int): int =
    if (points >= 80) 1
    elif (points >= 60) 2
    elif (points >= 40) 3
    elif (points >= 20) 4
    else 5

State variables:

  • o – is a reference to a newly registered Aeternity oracle object, which will be used inside the current contract and by the Examinee contract as well
  • exams – a hash map to store examinee addresses with their query objects
  • qfee – an oracle fee for making a query. This is the amount of AE tokens that needs to be paid by examinee contract

The ExamVerifier smart contract wraps an oracle object, which is created as part of the contract constructor. An Aeternity oracle can be registered and used off-chain as well. Aeternity Node provides a REST API for that. The current implementation of a wrapping oracle inside the contract is just our design choice.

Business Use Case of Oracles

In general, our oracle owner can be a certification institute, which has off-chain access to examinee data. This organization may have business incentives to take and respond to queries.

An examinee goes to any certification provider nearby to take an exam. Upon result submission to the blockchain, the examinee contract asks the certification institute for the exam grade. This request includes a fee, which will be given to the certification institute. This fee will be set in advance by the certification provider in the examinee contract, so that the examinee just needs to pay once and off-chain for their exam submission.

Examinee contract

// API of the remote contract
contract ExamVerifier =
  function askGrade : (address, oracle_query(int, int)) => ()
  function getOracle : () => oracle(int, int)

// main contract
contract Examinee =  
  type intQuery = oracle_query(int, int)
   
   // two user-defined data types to keep foundation level certification data 
   // and regular trainings
  record foundationLevel = 
    { date: int, points: int, grade: option(int), oQuery: option(intQuery) }  
  record training = 
    { name: string, date: int, provider: address, trainer: string, points: int }

  // definition of contract state
  record state = 
    { foundation : option(foundationLevel),      
      trainings: list(training),      
      identHash: string,      
      examVerifier: ExamVerifier,      
      totalPts: int,
      maxPtsPerTraining: int,
      minPtsForAdvanced: int,
      minGradeForFoundation: int,
      oracleFee: int}
  
  // helper function to abort transaction
  private function require(b: bool, err: string) = if (!b) abort(err)

  // constructor function
  public stateful function init(identHash: string, examVerifier: ExamVerifier,
                                maxPtsPerTraining: int, minPtsForAdvanced: int, 
                                minGradeForFoundation: int, oracleFee: int) =
    require(identHash != "", "identHash must be non-empty")    
    { foundation = None,      
      trainings = [],      
      identHash = identHash,      
      examVerifier = examVerifier,      
      totalPts = 0,
      maxPtsPerTraining = maxPtsPerTraining,
      minPtsForAdvanced = minPtsForAdvanced,
      minGradeForFoundation = minGradeForFoundation,
      oracleFee = oracleFee}

  // adds new training entry 
  public stateful function addTraining(name: string, date: int, provider: address, 
    trainer: string, points: int): int =    
    require(points >=0, "points can't be negative")
    require(points =< state.maxPtsPerTraining, 
        "points can't be greater than maxPtsPerTraining")
    
    let t = {name = name, date = date, provider = provider, 
        trainer = trainer, points = points}
    put(state{ 
      trainings @ ts = t::ts,      
      totalPts @ tp = tp + points})
    state.totalPts
  
  // stores date and points of the user exam and calls ExamVerifier oracle 
  // to ask for grade calculation
  public stateful function setFoundationLevel(date: int, points: int) =    
    let oQuery = askOracle(Contract.address, points)
    let fl = {date = date, points = points, grade = None, oQuery = Some(oQuery)}
    put(state{foundation = Some(fl)})

  // looks up the oracle object from remote contract and 
  // makes a payable query to calculate exam grade
  private function askOracle(examinee: address, points: int): intQuery  =
    let o = state.examVerifier.getOracle()
    let q = Oracle.query(o, points, state.oracleFee, RelativeTTL(50), RelativeTTL(50))
    state.examVerifier.askGrade(examinee, q)
    q
    
  ....
  
  // see second part further

Every function above has a short description of its logic. There are functions to mutate state and trigger remote contracts. The code is quite straightforward so far. To summarize the first part of this contract, we have the following ‘write’ functions:

  • addTraining
  • setFoundationLevel

We also need a couple of functions like:

  • canRequestHomework – returns true, if the examinee can take the next certification level, which includes some hypothetical homework, otherwise returns false

  • retrieveExamGrade – to look up an answer of the oracle contract from the blockchain state and update Examinee contract state

Examinee contract, continued

We have designed the foundationLevel certificate as an option of the foundationLevel record type. Sophia does not have null/undefined values, and that is really great. Instead, it has variant types and the default variant type for undefined values is option:

type option('a) = None | Some('a)

Before we implement the remaining functions, let us define some helpers for option type:

// second part
  private function getOrElse(o: option('a), or: () => 'a): 'a =
    switch(o)
      Some(a) => a
      _ => or() 

  private stateful function flatMap(o: option('a), f: ('a) => option('b)): option('b) =
    switch(o)
      Some(a) => f(a)
      _ => None

  private stateful function map(o: option('a), f: ('a) => ('b)): option('b) =
    flatMap(o, (a) => Some(f(a)))

These functions should be familiar to functional programmers. Basically, we have created a monad for the existing option type. Now, it will be easier to implement our functions, which need to deal with variant types like option. Get ready for functional programming:

// third part

  // returns true if foundation exam took place, and total training points is equal 
  // or more than required for the next certification level,  and  
  // for foundation level certification grade is at required threshold. 
  // Otherwise returns false.
  public function canRequestHomework(): bool =
    switch (state.foundation)
      Some(f) => state.totalPts >= state.minPtsForAdvanced && enoughGrade(f.grade)
      _ => false
  
  // used by canRequestHomework function
  private function enoughGrade(f: option(int)): bool =
    getOrElse(map(f, (g) => g =< state.minGradeForFoundation), () => false)
  
  // returns some of int, if foundation certification took place 
  // and oracle has already provided particular exam grade
  public function getFoundationGrade(): option(int) =
    flatMap(state.foundation, (f) => f.grade)

  // retrieve oracle answer, if any, from the blockchain state 
  // and stores as part of the foundation certificate state.
  // Also for unit testing purposes, it returns exam grade back, if it is available.
  public stateful function retrieveExamGrade(): option(int) =
    flatMap(state.foundation, (f) =>
      flatMap(f.oQuery, (q) =>
        let o = state.examVerifier.getOracle()
        let grade = Oracle.get_answer(o, q)
        map(grade, (g) =>
          let updated = {date = f.date, points = f.points, 
            grade = Some(g), oQuery = f.oQuery}
          put(state{ foundation = Some(updated) })
          g)))

Having no flatMap, map and getOrElse would lead to clumsy code with nested pattern matching like:

switch(o)
  Some(a) => some code
    switch(o1)
      Some(a) => some code
      _ => some another code
  _ => some another code

It is important to mention that Sophia is a strongly typed language with type inference. Type inference helps to reduce amount of code, which needs to be written. For example:

let updated = {date = f.date, points = f.points, grade = Some(g), oQuery = f.oQuery}

will be inferred to record foundationLevel, which is defined in the contract.

At this point, we are ready to deploy both contracts to the Aeternity blockchain.

Deployment

Aeternity offers the Forgae framework for development of decentralized applications. It helps to set up a project template, compile, unit test and deploy smart contracts. It comes with a CLI and a local Aeternity node.

First, we need to launch the local node:

forgae node

then, we can compile all existing contracts with:

forgae compile

and finally, deploy to the local node

forgae deploy

We have skipped a big part related to project setup with Forgae, as well as the process for writing deployment scripts, unit tests and web-application code. Most of these topics we will cover in the next blogpost about Aeternity Smart Contract development.

Summary

Aeternity is a promising blockchain, which brings a great language to implement better quality smart contracts than in Solidity. Smart contract developers and multiple blockchain companies realised that language support to build correct and robust code is very important. At the end of the day, these contracts move money, which can be stolen – which already happened in the case of the DAO hack in the Ethereum blockchain. Functional programming is the best available tool today for developers to write correct business code.

The Aeternity team is developing a number of tools to support developers in building decentralized applications on top of the Aeternity blockchain. There is still a lot to do in terms of developer experience and tooling. However, existing frameworks like Forgae are a great boost in developers productivity. It took a while for Ethereum community to build the Truffle framework, which is similar to Aeternity Forgae.

Further reading and links

  1. Sophia Language Documentation
  2. Sophia Introduction and Tutorial
  3. Monad in Functional Programming
  4. Smartphone image source

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen