This article is the second of a series of posts that we will be sharing to explain HASHWallet’s tailor-made operation language: HOLa. As we shared in our previous post, HOLa interprets easily traceable stacks of definitions and low-level instructions called macros sent from an external source. In this article, we would like to describe what these macros are and how to construct them.
This article contains technical concepts and terms that may not be familiar to a general audience. If you have any questions or need clarification on any of the concepts discussed in this post, please do not hesitate to contact us or leave us a comment below.
The structure of a HOLa macro
+-------+-----------------+
| | coin |
| +-----------------+
| Macro | ext. parameters |
| +-----------------+
| | run |
+-------+-----------------+
A HOLa macro has a defined structure. The core part (i.e., the HOLa operations to be executed), is specified and securely signed in what we have called the run of the macro. Most run operations require parameters to be passed. There are two main types of parameters: internal and external. Constant values, such as a static string are considered internal and are embedded inside the run. On the other hand, arguments like an address are treated as external parameters. The difference between these two types of parameters and how they are built will be made clear in further examples. Finally, if we are dealing with cryptocurrencies (generating addresses or signing transactions), we need to specify it in the coin attribute by introducing the corresponding BIP44 currency identifier (for example, the currency identifier for Bitcoin is 80000000).
The structure of a run
+-----+-----+
| | hdr |
| +-----+
| run | op |
| +-----+
| | sig |
+-----+-----+
The operations that are to be executed by the HASHWallet card are defined in the run. There are three elements that make up the run of a macro: hdr, op, and sig. The header, or hdr, must match the coin parameter of the macro if we are dealing with a specific cryptocurrency, otherwise, the value 0 is passed. The op is an array of bytes containing all HOLa operations to be executed and sig is the signature of the run block. This signature is obtained by concatenating hdr and op, hashing the resulting array, and signing with a specific curve. The resulting signature has a DER encoding (which is also used by Bitcoin), and only eSignus controls the private key with which it is signed, making this whole macros system efficient, easily verifiable, and secure.
op and OpCodes
To quickly identify the type of operation, we have put them into three categories: cryptographic, hardware, and basic. As their names suggest, cryptographic operations are those that handle encryptions such as SHA256, Keccak, or HASH160; hardware operations are used to manage the fingerprint sensor, whatever is shown in the card’s display, or handling the NFC reader; and finally, basic operations control arrays, logical and arithmetic operations, conditionals, etc.
Each operation has a hexadecimal code designated (the OpCode) and it is used to construct the stack of operations in the run. The list of operations and their corresponding OpCodes will be made public once the full HOLa documentation is published.
To illustrate how to construct the operations bytes array, let’s clear whatever is shown on the HASHWallet’s screen. In this case, we would instruct the device to execute the CLEAR_DISPLAY operation, which has the 4D OpCode assigned. Since every group of operations in a run must end with the RETURN instruction (OpCode: 80), the resulting array of bytes, or simply op, of this example would be the following: 4D80. Evidently, this is an elementary case as non of the operations used need any parameter.
Let’s take a step further and run the mandatory example of displaying “HOLa, World!” on our device’s screen.
The operations list would be:
[4D] CLEAR_DISPLAY
[42] PUT_TEXT(sec, lin, col, in)
[4C] UPDATE_DISPLAY
[80] RETURN
As you may have guessed, the resulting op would have some sort of 4D42{sec}{lin}{col}{in}4C80 shape where {*} would correspond to each of the PUT_TEXT (OpCode 42) parameters, since it requires four values.
So now, let’s dive into parameters and come back later to this example once we get a good grasp of what parameters are and how to define them.
Parameters and the mem array
As we have mentioned before, there are two main types of parameters: internal and external. Internal parameters are basically predefined constants that are specified inside the macro’s run and can be used unchanged as arguments of any operation that requires them. Predefined constants can be texts, indexes of an array of static definitions, or even the active wallet’s public key or the HASHWallet’s master public key, as these values are fixed. On the other hand, an external parameter is not predefined and will have a different value depending on execution factors, such as addresses, amount, fees, or timestamps.
In our basic “HOLa, World!” example, this string should be considered as an internal parameter since it will always be the same no matter when or who sends the macro instruction to the device.
To optimize performance and memory consumption, these parameters are further classified depending on the nature of their values. For internal parameters, we have const, imm, and obj, for constant byte arrays, fixed integers, or for referencing the type of the public key, respectively. For external variables, we have a u32 or u64 parameters list for uint32 or uint64 values correspondingly, and a generic blk list for byte arrays.
The resulting return value of an operation can be stored and used as an argument in subsequent operations. For this purpose, a mem array will be automatically created and maintained by the interpreter and will be used to store intermediate results during the execution. Also, the values stored in the mem array can be used as an index to access the contents of other arrays. For example, let’s say that we need to access an element in the blk array with an index that is given by the value of element 4 in the mem array. This would be notated as blk[mem[4]].
Parameter construction
The value of each parameter has a size of 1 byte, having the const parameter a special exception. The bits structure of each value depends on the type of parameter and is formed based on the following table:
+----------+-------+-----+-----+-----+-----+-----+-----+-----+-----+
| | imm | 1 | 1 | 0 | d4 | d3 | d2 | d1 | d0 |
| +-------+-----+-----+-----+-----+-----+-----+-----+-----+
| Internal | obj | 1 | 0 | 1 | r | 0 | 0 | k | t |
| +-------+-----+-----+-----+-----+-----+-----+-----+-----+
| | const | 1 | 0 | 0 | n4 | n3 | n2 | n1 | n0 |
+----------+-------+-----+-----+-----+-----+-----+-----+-----+-----+
| | u64 | 0 | 1 | 1 | i4 | i3 | i2 | i1 | i0 |
| +-------+-----+-----+-----+-----+-----+-----+-----+-----+
| External | u32 | 0 | 1 | 0 | i4 | i3 | i2 | i1 | i0 |
| +-------+-----+-----+-----+-----+-----+-----+-----+-----+
| | blk | 0 | 0 | 1 | ind | i3 | i2 | i1 | i0 |
+----------+-------+-----+-----+-----+-----+-----+-----+-----+-----+
| | mem | 0 | 0 | 0 | ind | i3 | i2 | i1 | i0 |
+----------+-------+-----+-----+-----+-----+-----+-----+-----+-----+
Where:
- d is the integer value in binary. This value may represent the index of a specific predefined array.
- r selects as argument the predefined object that stores the result of the last hardware executed operation.
- k will select the public key to reference. This value can be 0 if we are referring to the current wallet’s public key and 1 for the HASHWallet master public key.
- t will be 0 if we need the value of the public key, or 1 if we need its length.
- n will be the number of bytes of the constant value. The const parameter will precede the byte array that is passed in the run.
- i will be the referencing index of the corresponding values array.
- ind will indicate if the referencing index of the values array is determined by the value stored in the i index of the mem array.
In our previous example referencing the mem array, blk[mem[4]] will be equal to 34(hex) or 01110100(dec). Broken it into parts:
- 001 will indicate the blk array,
- 1 specifies that it is an indirect addressing (the index will refer to the mem array),
- 0100 points the fourth (4) element of the mem array.
This may seem complicated at first glance, but it becomes much more clearer and understandable through examples, so let’s bring back our “HOLa, World!” case.
Unlike PUT_TEXT, the CLEAR_DISPLAY, UPDATE_DISPLAY, and RETURN operations do not need parameters, so let’s focus on PUT_TEXT. This operation has four arguments:
- sec: the display’s section (header, body, or footer), is referenced with an imm internal or constant parameter, having 0 for the header, 1 for the body, and 2 for the footer as values.
- lin, col: these are the offset coordinates (line and column) and have predefined values as well.
- in: the input string (in our case HOLa, World!)
We want to put our text in the body section and at the top-left position, so:
- sec will be 11000001 or C1(hex), since 110 indicates that it is an imm parameter and 00001 points to the value 1(dec) for the body.
- lin and col will both be 11000000 or C0(hex), since, again, 110 indicates that it is an imm parameter with a 0(dec) value.
- Finally, “HOLa, World!” encoded to hex is 484F4C612C20576F726C6421. Since this is a constant value, we need to pass this argument as a const parameter. Therefore, 8C(hex) will precede the hex-encoded text. 8C(hex) comes from 10001100(bin) where 100 indicates that it is a const parameter and 01100 is the size of the hex-encoded text, which is 12(dec). So, our in argument will be: 8C484F4C612C20576F726C6421.
Thus, our PUT_TEXT operation with its corresponding arguments will be: 42C1C0C08C484F4C612C20576F726C6421, where:
- 42 is the opCode for the PUT_TEXT operation,
- C1 points to the body section of the screen,
- C0C0 are the line and column (both at 0),
- 8C484F4C612C20576F726C6421 is our “HOLa, World!” text preceded by the const value 8C(hex).
Therefore, the op section of the run of our macro will be 4D42C1C0C08C484F4C612C20576F726C64214C80.
HOLa Builder
As shown above, building a simple run for a macro is a straightforward task, but it can quickly become overextended, especially for complex macros such as those involved in validating and signing cryptographic transactions with the fingerprint reader and such. To facilitate and ensure proper construction of runs, eSignus has developed the HOLa-Builder (currently for internal use but intended for third-party access soon). HOLa-Builder is a web app that helps create, edit, and save macros intuitively with useful features like reordering operations, hex encoders, dynamic parameters and mem array referencing, validating, Bluetooth connectivity for testing on real devices (only available for Chrome), etc. It also provides a run decoder that deciphers the hex-encoded run and breaks it down to its designated operations and parameters so anyone can verify and validate any macro with ease.
Wrapping it up
In this article, we have learned how HOLa and its macros are built, how to reference operations by using opCodes, and how to build and pass the corresponding arguments. We have seen, in a superficial way, the potential and flexibility of this system, despite its simplicity. For further inquiries about HOLa, public and in-depth documentation with all the operations descriptions and their corresponding arguments will be provided soon.
As we mentioned in our previous post, the whole idea of HOLa is to have a non-programmable -and therefore, absolutely secure- hardware wallet but flexible enough to feature community-driven functionalities and become future-proof. As you might envision, by using this unique system, HASHWallet is able to operate with not only any cryptocurrency but with any transaction that requires a secure signing.
We will continue to post more information about HOLa so please stay tuned.