© Karan Singh Garewal 2020
K. S. GarewalPractical Blockchains and Cryptocurrencieshttps://doi.org/10.1007/978-1-4842-5893-4_17

17. Helium Wallet Construction

Karan Singh Garewal1 
(1)
Toronto, ON, Canada
 

Introduction

In this chapter, we will examine the construction of Helium wallets. In particular, we discuss the following topics:
  1. 1.

    What is a wallet

     
  2. 2.

    The Helium wallet interface

     
  3. 3.

    Helium wallet program code

     
  4. 4.

    Helium wallet Pytest code

     

The Bitcoin reference wallet implementation relies upon access to the complete Bitcoin blockchain. Similarly, any Helium node that has the complete Helium blockchain or has access to this blockchain can create and maintain a wallet.

This chapter creates a basic command-line Helium wallet that implements core functionality. Once we have an implementation of core functionality, we can embellish our wallet with additional capabilities. For example, we can wrap a GUI around our command-line wallet.

What Is a Wallet

A Helium wallet is an object that performs the following functions:
  1. 1.

    Creates and maintains private-public key pairs

     
  2. 2.

    Records the heliums received by the wallet holder

     
  3. 3.

    Records the heliums transferred to other entities

     
  4. 4.

    Records the final helium balance of the wallet holder (as of the time of the query)

     
  5. 5.

    Creates transactions and propagates them on the Helium network

     
  6. 6.

    Saves wallet data to files

     

The Helium Wallet Interface

A Helium wallet maintains the following dictionary data structure:
wallet_state = {
             "keys": [],          # list of private-public key tuples
             "received": [],      # values received by the wallet holder
             "spent": [],         # values transferred by the wallet holder
             "received_last_block_scanned": 0,
             "spent_last_block_scanned": 0
                }

wallet_state describes the state of the the wallet at a given point in time.

wallet[“keys”] is a list of the private-public key-pair tuples owned by the wallet holder. The wallet application can create private-public keys.

wallet[“received”] is a list of all the values received by the wallet holder. Each element in this list has the structure:
{
   "value": value
   "blockno": block_height
   "fragmentid: transaction["transactionid"] + "_" + str(index)
   "public_key" = key_pair[1]
}

wallet[“spent”] is a list of all the values transferred by the wallet holder. Each element in this list has the same structure as wallet[“received”].

wallet[“received_last_block_scanned”] is the last block of the blockchain that was scanned when the blockchain was scanned for values received.

wallet[“spent_last_block_scanned”] is the last block of the blockchain that was scanned when the blockchain was scanned for values transferred by the wallet holder.

Our Helium wallet contains the following interface functions:

Key Management
create_keys() -> "public, private key tuple"

This function creates a public-private key pair that can be used in a transaction.

Transaction Record
value_received(block_height -> "integer" = 0) ->   "boolean"

value_received creates a list of helium values that have been received by the wallet holder. block_height is an optional parameter that directs the function to get the values received since and including the block with the given height. This function returns False if the function fails and True otherwise.

value_received creates or updates the wallet_state[“received”] key value.

The algorithm for this function relies upon the observation that a vout element of a transaction that transfers value to the wallet holder contains a public key generated by the wallet holder in the encoded form RIPEMD-160(SHA-256(public_key)).

The pseudo-code for this function is
let values_received = []
for each block where block["height"] >= block_height:
    for each transaction trx in the block:
      for each vout array element in trx:
            for each public_key, pk, owned by the wallet holder:
                if RIPEMD-160(SHA-256(pk)) == vout_element["ScriptPubKey][3]:
                   values_received.append {
                                           value: vout_element[value]
                                           block: block["height"]
                                           transaction: trx[transactionid]
                                           public_key: pk
                                           }
The function value_spent creates a list of helium values that have been transferred by the wallet holder to other entities. block_height is an optional parameter that directs the function to get the values transferred after the block with the given height (inclusive of this block).
value_spent(block_height -> "integer" = 0) ->   "list"
This function relies on the observation that each value transferred by the wallet holder to some entity will be identified by a vin element in the transaction that contains the wallet holder’s public key. The following pseudo-code implements this functionality:
let values_spent = []
for each block where block["height"] >= block_height:
    for each transaction trx in the block:
      for each vin list element, vin_element, in trx:
            for each public_key, pk, owned by the wallet holder:
                if pk == vin_element["ScriptPubKey][1]:
                    pointer = vin_element[transactionid] + "_" + \
                                  vin_element[vout_index]
                    values_spent.append {
                                            value: vin_element[value],
                                            block: block["height"],
                                            transaction: pointer,
                                            public_key: pk
                                       }
Persistence
save_wallet() → "boolean"
The save_wallet function saves wallet_state. In particular, wallet_state is persisted to the disk file wallet.dat:
load_wallet()   ->  "dictionary"

This function loads all of the wallet data from wallet.dat into the wallet_state structure.

Note that because the state of the wallet is persisted, we do not have to interrogate the entire blockchain to update the wallet. We simply need to examine the blocks from parameters received_last_block_scanned and spent_last_block_scanned onward.

The Helium Wallet Program Code

Copy the following program code into a file called wallet.py and save it in the wallet directory:
###########################################################################
# wallet.rb: a command-line wallet for Helium
###########################################################################
import hblockchain
import rcrypt
import pdb
import pickle
# the state of the wallet
wallet_state = {
                  "keys": [],          # list of private-public key tuples
                  "received": [],      # values received by wallet holder
                  "spent": [],         # values transferred by the wallet holder
                  "received_last_block_scanned": 0,
                  "spent_last_block_scanned": 0
                }
def create_keys():
    key_pair = rcrypt.make_ecc_keys()
    wallet_state["keys"].append(key_pair)
def initialize_wallet():
    wallet_state["received"] = []
    wallet_state["spent"] = []
    wallet_state["received_last_block_scanned"] = 0
    wallet_state["spent_last_block_scanned"] = 0
def value_received(blockno:"integer"=0) -> "list" :
    """
    obtains all of the helium values received by the wallet holder by examining
    transactions in the blockchain. Updates the wallet state.
    {
      value:  <integer>,
      ScriptPubKey: <list>
    }
    """
    hreceived = []
    rvalue = {}
    # get values received from the blockchain
    for block in hblockchain.blockchain:
        for transaction in block["tx"]:
            ctr = -1
            for vout in transaction["vout"]:
                ctr += 1
                for key_pair in wallet_state["keys"]:
                    if rcrypt.make_RIPEMD160_hash(rcrypt.make_SHA256_hash(key_pair[1])) \ == vout["ScriptPubKey"][2]:
                        rvalue["value"] = vout["value"]
                        rvalue["blockno"] = block["height"]
                        rvalue["fragmentid"] = transaction["transactionid"] + "_" + str(ctr)
                        rvalue["public_key"] = key_pair[1]
                        hreceived.append(rvalue)
                        break
    # update the wallet state
    if block["height"] > wallet_state["received_last_block_scanned"]:
        wallet_state["received_last_block_scanned"] = block["height"]
    for received in hreceived:
        wallet_state["received"].append(received)
    return
def value_spent(blockno:"integer"=0):
    """
    obtains all of the helium values transferred by the wallet holder by examining
    transactions in the blockchain. Update the wallet state.
    """
    hspent = []
    tvalue = {}
    # get values spent from  blockchain transactions
    for block in hblockchain.blockchain:
        for transaction in block["tx"]:
            ctr = -1
            for vin in transaction["vin"]:
                ctr += 1
                for key_pair in wallet_state["keys"]:
                    if rcrypt.make_RIPEMD160_hash(rcrypt.make_SHA256_hash(key_pair[1])) \ == vin["ScriptSig"][1]:
                        tvalue["value"] = vin["value"]
                        tvalue["blockno"] = block["height"]
                        tvalue["fragmentid"] = transaction["transactionid"] + "_" +  str(ctr)
                        tvalue["public_key"] = key_pair[1]
                        hspent.append(tvalue)
                        break
    # update the wallet state
    if block["height"] > wallet_state["spent_last_block_scanned"]:
        wallet_state["spent_last_block_scanned"] = block["height"]
    for spent in hspent:
        wallet_state["spent"].append(spent)
    return
def save_wallet() -> "bool":
    """
    saves the wallet state to a file
    """
    try:
        f = open('wallet.dat', 'wb')
        pickle.dump(wallet_state, f)
        f.close()
        return True
    except Exception as error:
        print(str(error))
        return False
def load_wallet() -> "bool":
    """
    loads the wallet state from a file
    """
    try:
        f = open('wallet.dat', 'rb')
        global wallet_state
        wallet_state = pickle.load(f)
        f.close()
        return True
    except Exception as error:
        print(str(error))
        return False

wallet.py contains functions that scan the blockchain and constructs a list of all of the transaction values received as well as the transaction values sent. Since the wallet state maintains the markers for the last block that has been scanned when these lists are constructed (received_last_block_scanned and spent_last_block_scanned), we do not need to re-scan the portion of the blockchain that is scanned to create these lists as long as the wallet only uses private-public keys that are created using the wallet’s create_keys function.

The initialize_wallet function can be called in the event that we decide to scan the blockchain again from the genesis block.

I will leave it as an exercise for the reader to write the code for a wallet function that receives a Helium address and creates a transaction that is propagated on the Helium network (refer to Chapter 9 for the algorithm that creates and transforms Helium addresses). Another piece of functionality that can be added to this wallet is to automatically consolidate small input values that are owned by the wallet holder into a larger value.

Helium Wallet Pytest Code

The following program code implements Pytests for our Helium wallet. These tests are integration tests that use a simulated blockchain. Copy this code into a file called test_wallet.py and save the file in the unit_tests directory. As usual, we can run these tests with
(virtual) $ pytest test_wallet.rb
Execution of these tests should indicate 13 passing tests:
######################################################
# test_wallet.rb
######################################################
import hchaindb
import hmining
import hconfig
import hblockchain
import rcrypt
import tx
import wallet
import json
import logging
import os
import pdb
import pytest
import secrets
import sys
import time
"""
   log debugging messages to the file debug.log
"""
logging.basicConfig(filename="debug.log",filemode="w",  \
format='client: %(asctime)s:%(levelname)s:%(message)s', level=logging.DEBUG)
def setup_module():
    """
    start the databases
    """
    # start the Chainstate Database
    ret = hchaindb.open_hchainstate("heliumdb")
    if ret == False:
        print("error: failed to start Chainstate database")
        return
    else: print("Chainstate Database running")
    # make a simulated blockchain with height 5
    make_blocks(5)
def teardown_module():
    """
    stop the databases
    """
    hchaindb.close_hchainstate()
# unspent transaction fragment values [{fragmentid:value}]
unspent_fragments = []
keys = []
######################################################
# Make A Synthetic Random Transaction For Testing
# receives a block no and a predicate indicating
# whether the transaction is a coinbase transaction
######################################################
def make_random_transaction(blockno, is_coinbase):
    txn = {}
    txn["version"] =  "1"
    txn["transactionid"] = rcrypt.make_uuid()
    if is_coinbase == True: txn["locktime"] = hconfig.conf["COINBASE_LOCKTIME"]
    else: txn["locktime"] = 0
    # the public-private key pair for this transaction
    transaction_keys = rcrypt.make_ecc_keys()
    global keys
    keys.append(transaction_keys)
    # previous transaction fragments spent by this transaction
    total_spendable = 0
    #######################
    # Build the vin array
    #######################
    txn["vin"] = []
    # genesis block transactions have no prior inputs.
    # coinbase transactions do not have any inputs
    if (blockno > 0) and (is_coinbase != True):
        max_inputs = secrets.randbelow(hconfig.conf["MAX_INPUTS"])
        if max_inputs == 0: max_inputs = hconfig.conf["MAX_INPUTS"] - 1
        # get some random previous unspent transaction
        # fragments to spend
        ind = 0
        ctr = 0
        while ind < max_inputs:
            # get a random unspent fragment from a previous block
            index = secrets.randbelow(len(unspent_fragments))
            frag_dict = unspent_fragments[index]
            if frag_dict["blockno"] == blockno or frag_dict["value"] < 10:
                ctr += 1
                if ctr == 10000:
                    print("failed to get random unspent fragment")
                    return False
                continue
            key = frag_dict["key"]
            unspent_fragments.pop(index)
            assert unspent_fragments.count(frag_dict) == 0
            total_spendable += frag_dict["value"]
            tmp = hchaindb.get_transaction(key)
            if tmp == False:
                print("cannot get fragment from chainstate: " + key)
            assert tmp["spent"] == False
            assert tmp["value"] > 0
            # create a random vin element
            key_array = key.split("_")
            signed = rcrypt.sign_message(frag_dict["privkey"], frag_dict["pubkey"])
            ScriptSig = []
            ScriptSig.append(signed)
            ScriptSig.append(frag_dict["pubkey"])
            txn["vin"].append({
                    "txid": key_array[0],
                    "vout_index": int(key_array[1]),
                    "ScriptSig": ScriptSig
                })
            ctr = 0
            ind += 1
    #####################
    # Build Vout list
    #####################
    txn["vout"] = []
    # genesis block
    if blockno == 0:
        total_spendable = secrets.randbelow(10000000) + 50000
    # we need at least one transaction output for non-coinbase
    # transactions
    if is_coinbase == True: max_outputs = 1
    else:
        max_outputs = secrets.randbelow(hconfig.conf["MAX_OUTPUTS"])
        if max_outputs == 0: max_outputs = hconfig.conf["MAX_OUTPUTS"]
    ind = 0
    while ind < max_outputs:
        tmp = rcrypt.make_SHA256_hash(transaction_keys[1])
        tmp = rcrypt.make_RIPEMD160_hash(tmp)
        ScriptPubKey = []
        ScriptPubKey.append("<DUP>")
        ScriptPubKey.append("<HASH-160>")
        ScriptPubKey.append(tmp)
        ScriptPubKey.append("<EQ-VERIFY>")
        ScriptPubKey.append("<CHECK-SIG>")
        if is_coinbase == True:
            value = hmining.mining_reward(blockno)
        else:
            amt = int(total_spendable/max_outputs)
            if amt == 0: break
            value = secrets.randbelow(amt)   # helium cents
            if value == 0: value = int(amt)
            total_spendable -= value
            assert value > 0
            assert total_spendable >= 0
        txn["vout"].append({
                  "value": value,
                  "ScriptPubKey": ScriptPubKey
                })
        # save the transaction fragment
        fragid = txn["transactionid"] + "_" + str(ind)
        fragment = {
                        "key": fragid,
                        "value":value,
                        "privkey":transaction_keys[0],
                        "pubkey":transaction_keys[1],
                        "blockno": blockno
                    }
        unspent_fragments.append(fragment)
        print("added to unspent fragments: " + fragment["key"])
        if total_spendable <= 50: break
        ind += 1
    return txn
########################################################
# Build some random Synthetic Blocks For Testing.
# Makes num_blocks, sequentially linking each
# block to the previous block through the prevblockhash
# attribute.
# Returns an array of synthetic blocks
#########################################################
def make_blocks(num_blocks):
    ctr = 0
    total_tx = 0
    blocks = []
    global unspent_fragments
    unspent_fragments.clear()
    hblockchain.blockchain.clear()
    while ctr < num_blocks:
        block = {}
        block["prevblockhash"] = ""
        block["version"] = hconfig.conf["VERSION_NO"]
        block["timestamp"] = int(time.time())
        block["difficulty_bits"] = hconfig.conf["DIFFICULTY_BITS"]
        block["nonce"] = hconfig.conf["NONCE"]
        block["merkle_root"] = ""
        block["height"] = ctr
        block["tx"] = []
        # make a random number of transactions for this block
        # genesis block is ctr == 0
        if ctr == 0: num_transactions = 200
        else:
            num_transactions = secrets.randbelow(50)
            if num_transactions <= 1: num_transactions = 25
        txctr = 0
        while txctr < num_transactions:
            if ctr > 0 and txctr == 0: coinbase_trans = True
            else: coinbase_trans = False
            trx = make_random_transaction(ctr, coinbase_trans)
            assert trx != False
            block["tx"].append(trx)
            total_tx += 1
            txctr += 1
        if ctr > 0:
            block["prevblockhash"] = \
               hblockchain.blockheader_hash(hblockchain.blockchain[ctr - 1])
        ret = hblockchain.merkle_root(block["tx"], True)
        assert ret != False
        block["merkle_root"] = ret
        ret = hblockchain.add_block(block)
        assert ret == True
        blocks.append(block)
        ctr+= 1
    print("blockchain height: " + str(blocks[-1]["height"]))
    print("total transactions count: " + str(total_tx))
    return blocks
def test_no_values_received():
    """
    get all of the transaction values received by the wallet-holder
    when the holder has no keys.
    """
    wallet.wallet_state["keys"] = []
    wallet.value_received(0)
    assert wallet.wallet_state["received"] == []
@pytest.mark.parametrize("index", [
     (0),
     (1),
     (11),
     (55),
])
def test_values_received(index):
    """
    test that each value received by the wallet owner pertains to a
    public key owned by the wallet owner.
    Note: At least 55 private-public keys must have been generated.
    """
    wallet.initialize_wallet()
    wallet.wallet_state["keys"] = []
    my_key_pairs = keys[:]
    for _ in range(index):
        key_index = secrets.randbelow(len(my_key_pairs))
        wallet.wallet_state["keys"].append(keys[key_index])
    # remove any duplicates
    wallet.wallet_state["keys"] = list(set(wallet.wallet_state["keys"]))
    my_public_keys = []
    for key_pair in wallet.wallet_state["keys"]:
        my_public_keys.append(key_pair[1])
    wallet.value_received(0)
    for received in wallet.wallet_state["received"]:
        assert received["public_key"] in my_public_keys
def test_one_received():
    """
    test that for a given public key owned by the wallet-holder, at least one
    transaction fragment exists in the wallet.
    """
    wallet.initialize_wallet()
    wallet.wallet_state["keys"] = []
    key_index = secrets.randbelow(len(keys))
    wallet.wallet_state["keys"].append(keys[key_index])
    ctr = 0
    wallet.value_received(0)
    for received in wallet.wallet_state["received"]:
        if received["public_key"] == wallet.wallet_state["keys"][0][1]: ctr  += 1
    assert ctr >= 1
@pytest.mark.parametrize("index", [
     (0),
     (1),
     (13),
     (49),
     (55)
])
def test_values_spent(index):
    """
    test that values spent pertain to public keys owned by the wallet owner.
    Note: At least 55 private-public keys must have been generated.
    """
    wallet.initialize_wallet()
    wallet.wallet_state["keys"] = []
    my_key_pairs = keys[:]
    for _ in range(index):
        key_index = secrets.randbelow(len(my_key_pairs))
        wallet.wallet_state["keys"].append(keys[key_index])
    # remove any duplicates
    wallet.wallet_state["keys"] = list(set(wallet.wallet_state["keys"]))
    my_public_keys = []
    for key_pair in wallet.wallet_state["keys"]:
        my_public_keys.append(key_pair[1])
    wallet.value_spent(0)
    for spent in wallet.wallet_state["spent"]:
        assert spent["public_key"] in my_public_keys
def test_received_and_spent():
    """
    test that if a transaction fragment is spent then it has also been
    received by the wallet owner.
    """
    wallet.initialize_wallet()
    wallet.wallet_state["keys"] = []
    key_index = secrets.randbelow(len(keys))
    wallet.wallet_state["keys"].append(keys[key_index])
    wallet.value_received(0)
    wallet.value_spent(0)
    assert len(wallet.wallet_state["received"]) >= 1
    for spent in wallet.wallet_state["spent"]:
        ctr = 0
        assert spent["public_key"] == wallet.wallet_state[keys][0][1]
        ptr = spent["fragmentid"]
        for received in wallet.wallet_state["received"]:
            if received["fragmentid"] == ptr: ctr += 1
        assert ctr == 1
        ctr = 0
def test_wallet_persistence():
    """
    test the persistence of a wallet to a file.
    """
    wallet.initialize_wallet()
    wallet.wallet_state["keys"] = []
    my_key_pairs = keys[:]
    for _ in range(25):
        key_index = secrets.randbelow(len(my_key_pairs))
        wallet.wallet_state["keys"].append(keys[key_index])
    # remove any duplicates
    wallet.wallet_state["keys"] = list(set(wallet.wallet_state["keys"]))
    wallet.value_spent(0)
    wallet.value_received(0)
    wallet_str = json.dumps(wallet.wallet_state)
    wallet_copy = json.loads(wallet_str)
    assert wallet.save_wallet() == True
    assert wallet.load_wallet() == True
    assert wallet.wallet_state["received"] == wallet_copy["received"]
    assert wallet.wallet_state["spent"] == wallet_copy["spent"]
    assert wallet.wallet_state["received_last_block_scanned"] == \
          wallet_copy["received_last_block_scanned"]
    assert wallet.wallet_state["spent_last_block_scanned"] == \
          wallet_copy["spent_last_block_scanned"]
    max = len(wallet_copy["keys"])
    ctr = 0
    for ctr in range(max):
        assert wallet.wallet_state["keys"][ctr][0] == wallet_copy["keys"][ctr][0]
        assert wallet.wallet_state["keys"][ctr][1] == wallet_copy["keys"][ctr][1]

Conclusion

In this chapter, we have constructed a basic command-line wallet from a blockchain. This wallet implements all of the core functionality that is required in a wallet. The wallet can be embellished by encrypting the disk file that persists the state of the wallet, wrapping the wallet in a GUI interface as well as providing other convenience features.

In the next and final chapter, we will return to the matter of creating a test network for the Helium network.