The Resurrection of LLL: Part IV

In part 3 of this series I completed the exploration of the contract support files, finishing the discussion of modifiers, then going into the functions found in utilities.lll. I then started exploring dispatcher.lll itself. In this installment I'll continue with the breakdown of dispatcher.lll.

dispatcher.lll

Now we come to the CODE section of the dispatcher. You can see the entire section is enclosed by a returnlll and a seq. returnlll is a convenience macro predefined in the LLL compiler that does the housekeeping around returning the enclosed code, described briefly at the end of the previous article. The seq operator tells the compiler to evaluate the enclosed expressions in order. The seq itself evaluates to the result of the final expression given. Instead of

(seq
    (whatever one two)
    (whomever three four)
)

you can simply use { and a closing }:

{
    (whatever one two)
    (whomever three four)
}

This brings me to a discussion about coding style and clarity.

Coding style and clarity

I love clear, obvious code. As a result, I tend not to use many of the shortcuts provided by the LLL compiler. Of course if it makes sense to I will, but generally I prefer to use the long form. As a result you'll see that my LLL source looks a lot more like Lisp than standard examples of LLL code. I use seq instead of {}. I use mstore and sstore instead of [] and [[]]. To me, these shortcuts introduce an obscuring visual noise that doesn't help in the comprehension of the code.

I also follow Lisp's parenthesis placement conventions, keeping closing parentheses on the same line, with rare exceptions. Doing this emphasizes the indentation of code blocks and keeps source depth to a minimum. As I stated previously, Lisp developers generally use indentation to indicate code blocks; parentheses are mainly for the parser.

I do use the @ and @@ shortcuts for mload and sload, however. They're clearer in their meaning than the *store shortcut equivalents and don't introduce as much visual noise. But even then, if their use sacrifices clarity I revert to using the long form. For example, I use:

(sload (+ @@contract-address @short-hash))

I could have used:

@@(+ @@contract-address @short-hash)

But to me that loses clarity. I also prefer clarity over efficiency. For example, in the INIT section I initialize contract-address to zero. This isn't strictly necessary, as all memory and storage locations in the EVM are initialized to zero by default. But explicitly doing the initialization makes it clear that there is no valid contract address to start and makes the following code clearer:

(when (= @@contract-address 0x00) 
  (jump invalid-location))

CODE

The following will go into more detail than I originally intended, but I think it's important to understand how a function is called from web3 and how we can handle it in our LLL code. It's one of the main points I want to make—that LLL-based contracts can integrate into the standard development workflow in the same way as Solidity-based contracts. Once we get through the next block of code, I'll move up a level and discuss larger concepts.

After the enclosing seq, you'll see a section of code starting with this:

(when (= function-id dispatcher) ...

This code block is the equivalent of a constructor. In this case it initializes the dispatcher with the contract at the address passed in and then calls that contract's initialize function. The line above illustrates how to accept a function call from something like web3.

As described previously, function-id is a macro which resolves to

(bytes4 (calldataload 0))

(calldataload 0) returns 32 bytes of the data at position 0 of the call data. The call data is simply a hex string of data constructed by the caller. Any structure in this data has to be decided on beforehand, aside from the first four bytes. These four bytes represent the ID of the function the caller is invoking. As previously discussed, bytes4 is a macro that shifts a number 28 bytes to the right. When applied to the data returned from (calldataload 0), it isolates the function's ID from any data that might have been placed at position 4. Again, see the ABI examples for the format of the function call data.

Once we have the ID of the function the caller wishes to invoke, we compare it to a known function ID defined in constants.lll, represented here as dispatcher. So the whole line goes like this: If the function ID passed in is equal to the only known function ID for the dispatcher itself, we can execute the body of the function. Otherwise, execution continues below.

The next line is simply an enclosing seq for the body of the function. This line is also where I've chosen to apply any modifiers the function may have. In the case of the Dispatcher(address) function there are three modifiers: only-owner, already-initialized and no-contract-address. They are somewhat self-explanatory and all are defined in utilities.lll: Only the owner of the contract can execute this function, and it can't be executed if the contract has already been initialized or if no contract address has been passed in.

Now we get to the true body of the function. It consists of two main parts. The first stores the address of the contract to which the dispatcher will be delegating, and the second initializes that contract. To store the contract address we simply retrieve the data at position four of the call data. Remember, we've already determined it's non-zero; that's all we can really do to ensure an address has been provided. We then store that address at contract-address, which resolves to storage location 0x01. The next line simply enables the contract. It isn't as clear as the previous line, but it isn't complex either:

(sstore @@contract-address true)

This retrieves the address stored at contract-address and stores true, or 1, at the storage location represented by the address itself. To clarify, say the contract address is 0x754a5bec6ddd2b6a568203c3c2382137f8faa41c. This number gets stored at location 0x01 and the "enabled" flag for the contract is stored at location 0x754a5bec6ddd2b6a568203c3c2382137f8faa41c. This makes it easy to look up the flag for the contract.

Storage is sparse, whereas memory isn't. You can store something way up at 0x754a5bec6ddd2b6a568203c3c2382137f8faa41c without having to pay for the intervening storage locations. It's much like an associative array in other languages. The key in this case is the contract's address and the value is the boolean true or false. Conversely, memory is an expandable byte array. If you store something in memory at 0x1000 you're paying for every byte of memory from 0x00 to 0x1000. Something to keep in mind!

Conclusion

Well that was long and involved! We didn't even get through the dispatcher's constructor function. In the next article I'll finish this discussion and hopefully complete the examination of the dispatcher itself. Then I can move on to a contract that uses the dispatcher. That should be a lot more straightforward after everything we've discussed here. The main addition will be an explanation of how to format logging in LLL to conform to what web3 expects.