Tutorials
No Results

Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial

Intermediate · 1 hour

Development on Algorand using Raspberry Pi - Part 3

Prerequisites

Prerequisites are Part 1 and Part 2. That way, we have setup a Raspberry Pi as a node and installed python3 and JavaScript Alogrand SDK.

Motivation

To keep your node active, you need to regularly create a participation key and send the key registration transaction. This can be done in various ways - using CLI tools, SDKs, or a wallet. In this tutorial, we use Python to automate calls to the goal CLI tool to best demonstrate the individual steps. This may also be done in other available SDKs, but the scope of this tutorial is on JavaScript SDK usage, which is used for the offline signing and helps us build knowledge for future tutorials.

We cover the following points:

  • Learn which Algorand development account types exist
  • Introduce public key cryptography and how it relates to our solution
  • Setup a Raspberry Pi chain for participation key generation
  • Sign our transaction in an offline manner

Slides are available here.

Requirements

It is recommended to have a good understanding of public key cryptography, address generation, and transaction signing as covered in the following video:

Background

Algorand network offers various forms of accounts to its developers and also different SDK based on the language of preference. This tutorial teaches us how to operate node securely using offline authorization and setup Algorand JavaScript SDK on the Raspberry Pi. This way, our node can participate securely in the consensus mechanism more securely making the network more robust.

Steps

We assume that you followed the prerequisites installation on the Raspberry Pi described in the tutorials Part 1 and Part 2.

Now, when all prerequisites are installed, we start with the automation process. This can done in various ways, but in this tutorial we use goal and dev tools, together with JavaScript Algorand SDK.

Please find code below under Code Details

1. Generate standalone account

In order to sign our generated transaction offline we need to create a standalone account.

  • Navigate to cd ~/node
  • Run ./algokey generate
  • Store mnemonic info and public key
  • In order to connect our account to the SDK, we need the generated mnemonic info of the standalone account.

2. Modify SignFromFile.js

In SignFromFile.js happens the offline signing logic, using the generated standalone account. We need to update the signFromFile.js as follows:

  • Update the mnemonic field the extracted value of your standalone the account (Step 1)
  • Update the path from which should the transaction be read and the path to which the signed transaction should be stored.
  • Update the token of the node. It can be extracted from ~/node/testnetdata/algod.token
  • The file can be executed in terminal using node sendFile.js

Navigate to code.

3. Modify sendFile.js

Similarly, like previous file, we need to some changes which are as follows:

  • Update the token - extract from ~/node/testnetdata/algod.token
  • Update the path to read the signed transaction
  • The file can be executed in terminal using node sendFile.js

Navigate to code.

4. Modify setup.py

Before executing the setup.py, we need to do following changes. First, we update setup.py. For simplicity we do all steps in one file, but the signing part could have be done separately or once node is disconnected from the Internet. The changes needed to be done in the code:

  • Your public address of the node account
  • Path where you want to store your transaction file
  • If you want, modify delta value (better shorter than longer) aims to incorporate some processing delay.
  • You can also update the lifeSpan , which defines the validity period of the participation key.

Navigate to code.

5. Execution of setup.py

After all the files are modified, execute python3 setup.py and you will see a successful registration of your participation key!

6. Verify account is online

To verify the account is online run: ./goal account list -d testnetdata

Conclusion

  • Learned about Algorand development account types
    → KMD account - stored encrypted on disk
    → Standalone account - not stored
  • Setup a Raspberry Pi chain for participation key generation
    → Generate participation with a single script
    → Create our first transaction
    → Combined Dev Tools with SDK
    → Looked in detail on participation key management
    → Sign your transactions offline
  • Do not forget, if you cannot contribute, be offline!

Code details

Code: setup.py

setup.py

import os 
import sys
import subprocess
import math

publicKey = "UPDATE"

# Execute command in terminal
def execute_cmd(bashCmd):
    output = subprocess.check_output(bashCmd, shell=True, text=True)
    return output

# Check node status
def check_status():
    bashCmd = "~/node/goal node status -d ~/node/testnetdata"
    output = execute_cmd(bashCmd)
    outputLines = output.split("\n")
    # Taking last block info
    lastBlock = int(outputLines[0].split()[-1])
    # Taking genesis hash of current network (not needed later on)
    gh = outputLines[-2].split()[-1]
    print("Last commited block:", lastBlock, "\nGenesis hash:", gh)
    return lastBlock, gh

# Generate new participation key
def create_new_key(startRound, endRound, keyDilution):
    bashCmd = "~/node/goal account addpartkey -a {} --roundFirstValid={} --roundLastValid={} --keyDilution={} -d ~/node/testnetdata".format(publicKey, startRound, endRound, keyDilution)
    print("Adding given participation key:", bashCmd)
    output = execute_cmd(bashCmd)
    list_part_keys()

# Prepare a transaction
def create_transaction(startRound, endRound, outputPath):
    bashCmd ="~/node/goal account changeonlinestatus --address={} --fee=2000 --firstvalid={} --lastvalid={} --online=true --txfile={} -d ~/node/testnetdata".format(publicKey, startRound, endRound, outputPath)
    print("Creating a transaction:", bashCmd)
    output = execute_cmd(bashCmd)

# List participation keys
def list_part_keys():
    bashCmd = "~/node/goal account listpartkeys -d ~/node/testnetdata"
    output = execute_cmd(bashCmd)
    # Interesting lines 1 - X
    # Format - | Registered | Filename | Parent address | First round | Last round | First key |
    return output.split("\n")

# Remove unused keys
def clean_keys(output, lastBlock):
    outputLines = output
    lastBlocks = []
    numItems = len(outputLines)
    if numItems < 2:
        return lastBlock
    for regKey in outputLines[1:(len(outputLines) - 1)]:
        tmp = regKey.split("\t")
        firstRoundValue = int(tmp[3])
        lastRoundValue = int(tmp[4])
        name = tmp[1]
        if lastRoundValue < lastBlock:
            print("This can key be erased: {}".format(name))
        else:
            if firstRoundValue < lastBlock and lastRoundValue > lastBlock:
                print("This key is active")
            else:
                print("This key is waiting")
                lastBlocks.append(lastRoundValue)
                lastBlock = max(lastBlocks)
    return lastBlock

# Define range, default 1000
def compute_key_range(lastBlock, lifeSpan=1000):
    # Condition 1 - firt voting round < than the current voting round -- recommended at least once every two weeks - 38 400 ~ 40 000
    # Once first voting round >= than current voting round, only then we can submit a transaction
    # wait at least 320 rounds before the key is accepted

    # Example with to participation keys: 
    # Key1, voteFst = 30 000, voteLst = 70 000
    # Key2, voteFst = 70 000 - 320 (better 400), voteLst = 110 000
    # approx value between the lastBlock and time we want to register the key -> transaction will not be admitted unless send during the block value > startRound
    delta = 5
    # Initial setup 
    output = list_part_keys()
    if len(output) >= 3:
        latestLastKey = clean_keys(output, lastBlock)
        startRound =  latestLastKey + delta
        lastRound = latestLastKey + lifeSpan
    else:
        startRound = lastBlock + delta
        lastRound = startRound + lifeSpan
    return startRound, lastRound

# update path to signFromFile, optimally offline at another place

def call_sign(outputPath):
    bashCmd = "node signFromFile.js"
    output = execute_cmd(bashCmd)
    print(output)

def call_send(outputPath):
    bashCmd = "node sendFile.js"
    output = execute_cmd(bashCmd)
    print(output)

def main():
    outputPath = "UPDATE"
    # Find out last block and gh hash
    lastBlock, gh = check_status()
    # Compute from when to when should the key be valid
    startRound, endRound = compute_key_range(lastBlock)
    # Dilution function computed as recommended
    keyDilution = math.ceil(math.sqrt(endRound-startRound))
    # Create a participation key
    create_new_key(startRound, endRound, keyDilution)

    # Create a key registration transaction
    # Fixed to 1000
    transcationPeriod = lastBlock + 1000
    create_transaction(lastBlock, transactionPeriod, outputPath)

    print("Transaction created")

    # !!DO OFFLINE!! sign
    call_sign(outputPath)

    print("Transaction signed")

    # submit signed transaction
    call_send(outputPath)


if __name__ == "__main__":
    main()

Code: signFromFile.js

const algosdk = require('algosdk');
const fs = require('fs');

var client = null;

// make connection to node
async function setupClient() {
    if( client == null){
        // Can be found in ~/node/testnetdata/algod.token
        const token = "UPDATE";
        const server = "localhost";
        const port = 8080;
        let algodClient = new algosdk.Algod(token, server, port);
        client = algodClient;
    } else {
        return client;
    }
    return client;
}
// recover account for example
function recoverAccount(){

    const passphrase = "UPDATE"
    let myAccount = algosdk.mnemonicToSecretKey(passphrase);
    return myAccount;
}

async function readUnsignedTransactionFromFile() {

    try {
        // setup connection to node
        let algodClient = await setupClient();

        console.log("Algodclient: %s", algodClient)
        // recover account
        let myAccount = await recoverAccount();
        console.log("My address: %s", myAccount.addr)
        let params = await algodClient.getTransactionParams();

        // read transaction from file and sign it
        // Provide full path
        let txn = algosdk.decodeObj(fs.readFileSync('UPDATE'));  
        // Needs to be done, since goal format of transaction is not compatible with JS
        let txn_jsformat = {
            "from": myAccount.addr,
            "voteKey": txn["txn"].votekey,
            "selectionKey": txn["txn"].selkey,
            "voteFirst": txn["txn"].votefst,
            "voteLast": txn["txn"].votelst,
            "voteKeyDilution": txn["txn"].votekd,
            "fee": txn["txn"].fee,
            "type": 'keyreg',
            "firstRound": txn["txn"].fv,
            "lastRound": txn["txn"].lv,
            "genesisID": params.genesisID,
            "genesisHash": params.genesishashb64
        }
        // Sign transaction
        let signedTxn = algosdk.signTransaction(txn_jsformat, myAccount.sk);

        // Store -> ideally! and send later on
        fs.writeFileSync('UPDATE', algosdk.encodeObj(signedTxn))
    } catch (e) {
        console.log(e);
    }
};

async function sign() {
    await readUnsignedTransactionFromFile();
}
sign();

Code: sendFile.js

const algosdk = require('algosdk');
const fs = require('fs');

var client = null;

// make connection to node
async function setupClient() {
    if( client == null){
        // Can be found in ~/node/testnetdata/algo.token
        const token = "UPDATE";
        const server = "localhost";
        const port = 8080;
        let algodClient = new algosdk.Algod(token, server, port);
        client = algodClient;
    } else {
        return client;
    }
    return client;
}

// Function used to wait for a tx confirmation
var waitForConfirmation = async function(algodclient, txId) {
    while (true) {
        let lastround = (await algodclient.status()).lastRound;
        let pendingInfo = await algodclient.pendingTransactionInformation(txId);
        if (pendingInfo.round != null && pendingInfo.round > 0) {
            //Got the completed Transaction
            console.log("Transaction " + pendingInfo.tx + " confirmed in round " + pendingInfo.round);
            break;
        }
        await algodclient.statusAfterBlock(lastround + 1);
    }
};
async function readSignedSend() {

    try{
        // setup connection to node
        let algodClient = await setupClient();

        console.log("Algodclient: %s", algodClient)
        // Update path to transaction
        let txnSigned = algosdk.decodeObj(fs.readFileSync('UPDATE'));  

        let txId = txnSigned.txID;

        await algodClient.sendRawTransaction(txnSigned.blob);

        // Wait for transaction to be confirmed
        await waitForConfirmation(algodClient, txId);
    } catch ( e ){
        console.log( e );
    }   
};

async function send(){
    await readSignedSend();
}
send();

python

javascript

goal

run a node

offline authorization

March 25, 2021