/algo-diploma

A short and sweet stateful smart contract to issue college graduates their diplomas on the Algorand blockchain.

Primary LanguagePythonMIT LicenseMIT

Issuing University Diplomas on Algorand

Usage

  1. Set up algod and goal to connect to the testnet or a private network and create a few user accounts. Follow the tutorial for assistance: https://developer.algorand.org/docs/run-a-node/setup/install/

  2. Install the required Python packages: pip3 install -r requirements.txt

  3. Once you have a few test accounts with a balance, list them in the config.yml file:

registrar: "alice"

alice:
        mnemonic: "<MNEMONIC HERE>"

bob:
        mnemonic: "<MNEMONIC HERE>"

charlie:
        mnemonic: "<MNEMONIC HERE>"

APP_ID: -1

This example config file lists three users, of which alice is the registrar. The role of a registrar is to issue diplomas to the students, in this case, bob and charlie.

  1. Make the PyTEAL contract code: make

  2. Deploy the smart contract: python3 run_diploma.py deploy This command will print an identifying number for APP_ID. Be sure to record this in the config.yml.

  3. Use the DApp:

  4. Issue a diploma to bob: python3 run_diploma.py issue-diploma bob "MIT,2020,BSc,Computer Science and Engineering"

  5. Transfer registrar duties to charlie: python3 run_diploma.py charlie Be sure to update the registrar in the config.yml accordingly.

  6. For more commands run: python3 run_diploma.py help

Testing

All of the unit tests are located in the tests directory. The code in the tests directory is a prepared example for how to use the AlgoPytest testing framework to test a single smart contract Algorand application. The code in conftest.py demonstrates how to initialize AlgoPytest and then the test files with names beginning with test_ demonstrate how to properly test all parts of the Algorand Diploma Smart Contract.

Running the Tests

  1. Testing this DApp requires the AlgoPytest plugin. Follow the installation instructions as well as the usage regarding starting the sandbox and setting up the relevant environment variables.

    • In most cases, the relevant AlgoPytest environemnt variables to set are SANDBOX_DIR and INITIAL_FUNDS_ACCOUNT.
    • Since this DApp is not an installable Python package, you will need to add the assets directory to your PYTHONPATH environment variable. That may be done with export PYTHONPATH=$PYTHONPATH:/path/to/algo-diploma/assets/ for example.
  2. Simply run the tests by executing pytestin the base directory of algo-diploma.

Table of Contents

  1. Usage
  2. Testing
  3. Table of Contents
  4. Overview
  5. Explaining the DApp
  6. Conclusion

Overview

This DApp demonstrates how the Algorand blockchain can facilitate diploma issuance for graduating university students. Graduating students can opt-in to this DApp. And then a privileged registrar account can issue specific diplomas to specific students. Students get their diploma recorded under their account which any 3rd party can audit and verify its integrity.

Additionally, the registrar can perform other operations for posterity. One such operation is transferring registrar duties to another account. After such transfer, the designated account will become the new registrar and the old registrar will lose all privileges. The registrar may also revoke diplomas if needed and also delete the entire DApp.

A detailed overview of all possible operations for both students and registrar can be found by running: python3 run_diploma.py help

Explaining the DApp

Roles

  • Registrar: The admin of the DApp. The registrar can issue and revoke diplomas. They also have the power to update and delete the DApp from the blockchain.
  • Student: A regular account in this DApp. They can receive a diploma from the registrar. They must opt-in to receive a diploma and have the option to leave the DApp at will.

Smart Contract Code

This DApp is expressed as a stateful Algorand smart contract. It is written in PyTEAL following the suggested development guidelines. The smart contract source code is accessible here.

Storage

There is one global bytes field and one local bytes field (per opted-in account). The global field holds the address of the current registrar. The account with this address has all of the registrar privileges of this DApp. The local field per account is where an issued diploma is recorded. A diploma is represented as a bytes array of common metadata such as issuing institution, year, degree title and type, etc.

Overall DApp Architecture

In this DApp, there is an account designated as a registrar and all other accounts are simply students. A global storage variable named "registrar" delineates which account is the registrar. A local storage variable in each account stores any issued diploma metadata. Accounts must opt-in to this DApp in order to receive a diploma if one is issued to them.

This DApp implements a number of commands. A command is a sequence of PyTEAL operations which are called based on some conditional control-flow logic depending on a string argument passed in. Each command has its respective sanity checks, such as checking whether the caller is the registrar when necessary or the number of arguments supplied.

This DApp supports three commands, diploma issuance, diploma revocation, and registrar reassignment. Diploma issuance is handled by writing the diploma metadata to the local storage of the account. Diploma revocation is handled by clearing the account's local storage. Lastly, registrar reassignment is handled by overwriting the global storage "registrar" variable with the new registrar's account address.

Contract Logic

var_registrar = Bytes("registrar")
is_registrar = Txn.sender() == App.globalGet(var_registrar)

program = Cond(
        [Txn.application_id() == Int(0), init_contract],
        [Txn.on_completion() == OnComplete.DeleteApplication, Return(is_registrar)],
        [Txn.on_completion() == OnComplete.UpdateApplication, Return(is_registrar)],
        [Txn.on_completion() == OnComplete.OptIn, Return(Int(1))],
        [Txn.on_completion() == OnComplete.CloseOut, Return(Int(1))],
        [Txn.application_args[0] == Bytes("issue_diploma"), issue_diploma],
        [Txn.application_args[0] == Bytes("revoke_diploma"), revoke_diploma],
        [Txn.application_args[0] == Bytes("reassign_registrar"), reassign_registrar]
)

This is the contract logic that directs how the DApp reacts to certain commands. The following breakdown explains this logic line-by-line.

  • Txn.application_id() == Int(0): When a smart contract is initially deployed, the Txn.application_id() will be 0. This is the time to call init_contractto record the creator as the initial registrar of the DApp.
  • Txn.on_completion() == OnComplete.DeleteApplication: Only allow the current registrar to delete this DApp.
  • Txn.on_completion() == OnComplete.UpdateApplication: Only allow the current registrar to update this DApp.
  • Txn.on_completion() == OnComplete.OptIn: Allow any account to opt-in to this DApp
  • Txn.on_completion() == OnComplete.CloseOut: Allow any account to leave this DApp
  • Txn.application_args[0] == Bytes("issue_diploma"): Run the issue_diploma logic of this DApp.
  • Txn.application_args[0] == Bytes("revoke_diploma"): Run the revoke_diploma logic of this DApp.
  • Txn.application_args[0] == Bytes("reassign_registrar"): Run the reassign_registrar logic of this DApp.

Contract Initialization

init_contract = Seq([
        App.globalPut(var_registrar, Txn.sender()),
        Return(Int(1))
])

This block records the creator of the DApp as the initial registrar. This block is only called when Txn.application_id() == Int(0) signalling that the smart contract is currently being deployed. Thus the Txn.sender() is the creator account currently deploying the DApp.

Diploma Issuance

diploma_metadata = Txn.application_args[1]
issue_diploma = Seq([
        Assert(is_registrar),
        Assert(Txn.application_args.length() == Int(2)),
        App.localPut(Int(1), Bytes("diploma"), diploma_metadata),
        Return(Int(1))
])

This block issues a diploma to an account. This code block is invoked by the "issue_diploma" command of this DApp. It takes an additional argument diploma_metadata = Txn.application_args[1] and an account Int(1) which is the account to receive the diploma metadata. The body of the block performs some sanity checks, that the caller is the registrar and that precisely two arguments are passed. If those checks pass, then the account at index 1, the one passed in, will receive the diploma metadata.

Revoke Diploma

revoke_diploma = Seq([
        Assert(is_registrar),
        Assert(Txn.application_args.length() == Int(1)),
        App.localDel(Int(1), Bytes("diploma")),
        Return(Int(1))
])

This block revokes the diploma of an account. This code is invoked by the "revoke_diploma" command of this DApp. It takes no additional arguments, only an account Int(1) which is the account that will get its diploma revoked. Like above, the body of this block performs the same sanity checks, that the caller is the registrar and that there is only one argument passed. If these checks pass, then the account at index 1 will lose its diploma metadata.

Registrar Reassignment

new_registrar = Txn.accounts[1]
reassign_registrar = Seq([
        Assert(is_registrar),
        Assert(Txn.application_args.length() == Int(1)),
        App.globalPut(var_registrar, new_registrar),
        Return(Int(1))
])

This block reassigns the registrar of this DApp. This code is invoked by the "reassign_registrar" command of this DApp. It takes no additional arguments, only an account Txn.accounts[1] which is the account that will become the new registrar. This block performs some sanity checks before reassigning the registrar.

DApp Interface Program

This DApp is interfaced with a Python program using the Algorand SDK. This program is used to deploy the DApp as well as invoke the various DApp commands. Much of the SDK code and helper functions are borrowed from an example Algorand SDK app here.

This SDK program has a host of functions. The help message from python3 run_diploma.py help lists the available functions:

Available commands:
        deploy: Deploy this smart contract for the first time
        update: Update this smart contract with new TEAL code
        opt-in <account-name>: Opt-in an account into this smart contract
        close-out <account-name>: Close-out an account from this smart contract
        delete <creator-name>: Delete this smart contract
        clear <account-name>: Clear this smart contract
        issue-diploma <account-name> <diploma-metadata>: Issue a degree to an account
        revoke-diploma <account-name>: Nullify the diploma of an account
        inspect <account-name>: Inspect an account's diploma on the Algorand blockchain
        inspect-global <creator-name>: Inspect this smart contract's global state
        reassign-registrar <account-name>: Assign an account to be the current registrar
        help: Print this help message

Any arguments taken by each function are listed with angle brackets <...>.

DApp Maintenance

The deploy, update, delete, and clear functions are used by a DApp administrator to maintain the DApp.

Deploy

Deployment of a smart contract begins with compiling the TEAL programs to a base64 encoded binary (the PyTEAL contract code is converted to TEAL by running make).

# Read the smart contract source files
smart_contract_file = open("./assets/diploma_smart_contract.teal", "rb")
smart_contract_source = smart_contract_file.read()
smart_contract_program = common.compile_program(algod_client, smart_contract_source)

clear_program_file = open("./assets/clear_program.teal", "rb")
clear_program_source = clear_program_file.read()
clear_program = common.compile_program(algod_client, clear_program_source)

Then a transaction is sent into the network signaling the deployment of a new smart contract. The creator of this DApp is the account that sends this transaction. In this case, the registrar at the time of deployment is the DApp creator. The formulation of this transaction is handled by the Python SDK.

txn = transaction.ApplicationCreateTxn(
        sender, params, on_complete, \
        smart_contract_program, clear_program, \
        global_schema, local_schema)

The global_schema and local_schema outline how many global and local storage variables this contract will use. For this DApp, both only indicate one Bytes storage variable each.

Update

Updating the source code of this DApp begins with compiling the TEAL programs, similarly to deployment. However, the update event involves a different type of transaction. Most notably, the app_id of the DApp must be submitted to point to which DApp to update. Only the current registrar may update this DApp. The Python SDK creates the transaction to update a smart contract as follows:

txn = transaction.ApplicationUpdateTxn(sender, params, app_id, \
                                        smart_contract_program, clear_program)
Delete

DApp deletion is performed by sending a specific transaction with the app_id of the DApp to be deleted. Only the current registrar may delete this DApp. This is handled concisely with the Python SDK.

txn = transaction.ApplicationDeleteTxn(sender, params, app_id)
Clear

Any account can remove their participation from the DApp. This is done by sending a clearing transaction to the network. This implicitly runs the clear_program of the DApp to perform any residual clean up and regardless of whether the clear program succeeds or not, the user will be removed from the DApp. The Python SDK creates the clear transaction as follows:

txn = transaction.ApplicationClearStateTxn(sender, params, app_id)

DApp Common Usage

The opt-in, close-out, issue-diploma, revoke-diploma, and reassign-registrar functions are the most common to be called by any user of this DApp.

Opt-In

Since this DApp utilizes local storage, any account wishing to participate in this DApp to receive a diploma must opt-in to it. This is performed by sending a specific opt-in transaction with the app_id of the DApp to join. The Python SDK creates the opt-in transaction as follows:

txn = transaction.ApplicationOptInTxn(sender, params, app_id)
Close-Out

Separate from the clear function, an account can leave a DApp by closing out of it. An account leaves a DApp only if a close-out transaction with the corresponding app_id succeeds, differing from the clear function which unconditionally removes an account. In the case of this DApp, close-out always succeeds. The Python SDK creates the close-out transaction as follows:

txn = transaction.ApplicationCloseOutTxn(sender, params, app_id)
Issue Diploma

The registrar issues a diploma by performing a call to the smart contract of this DApp. A call to the DApp is simply a transaction with the appropriate arguments required by the call. In the case of diploma issuance, that is the receiving account of the diploma as well as the diploma metadata -- degree type, institution, year, etc. This is facilitated by the Python SDK as a NoOp application call as follows:

txn = transaction.ApplicationNoOpTxn(sender, params, index, app_args, accounts)

The first argument of the app_args is the string "issue-diploma" which designates this call to issue a diploma.

Revoke Diploma

Similar to the diploma issuance, the registrar calls the DApp by sending a transaction with the appropriate arguments. In this case, this is only the account that will get its diploma revoked. This is facilitated by the same Python SDK call as above, but where the first argument of the app_args is the string "revoke-diploma".

Reassign Registrar

Similar to the other calls, registrar reassignment is a call to the DApp by the current registrar. Passed along is the account of the registrar to-be. The Python SDK call is the same as above, but the first argument of the app_args is the string "reassign-registrar".

DApp Inspection

The inspect and inspect-global functions allow a third party to view the state variables of this DApp. These functions are most useful to determine the integrity and validity of a student's diploma for example.

Inspect

Any 3rd party with access to the Algorand blockchain can inspect an account's local storage to view their diploma. The account to inspect is passed in as an argument to the DApp Interface Program. The local Bytes storage variables are base 64 encoded, so inspection decodes the value to be human-readable. A read_local_state helper function adopted from an example Algorand SDK app serves this function.

common.read_local_state(algod_client, pub_keys[account], app_id)
Inspect Global

A 3rd party can also inspect the global Bytes storage variable to see the current registrar. Global variables are stored in the creator account, the account that deployed the DApp. The DApp Interface Program uses the registrar during deployment as the creator account. However, even after registrar reassignment, the original creator account must be passed in to inspect the global current registrar variable. A read_global_state helper function from an example Algorand SDK app serves this function.

common.read_global_state(algod_client, pub_keys[creator], app_id)

Conclusion

This DApp exemplifies a simple yet tangible use-case for the Algorand blockchain. This diploma application is not complicated, yet covers many fundamental concepts for Algorand smart contract programming. Key concepts explored include:

  • Writing a PyTEAL smart contract adhering to all of the best standard practices
    • Supports the full life cycle of a smart contract, from deployment to deletion
    • Multiple account types with different privilege levels
    • Permission checking to ensure application security and integrity
    • Supports multiple commands with different behaviors
  • Writing an interface program using an Algorand SDK
    • An easy interface to develop, maintain and use the DApp

This solution is intended for learning purposes only. It does not cover error checking and other edge cases therefore, should not be used as a production application. This code is free to be used as outlined by the MIT License.