The Resurrection of LLL: Part VI

In part 5 of this series we finally finished discussing the dispatcher and its support files. In this article I'll be presenting a contract that makes use of the dispatcher as its representative on the blockchain, going over what's necessary for a contract to integrate with the dispatcher.

arithmetic.lll

The example contract we'll be discussing in this article implements two simple functions that perform arithmetic on the given input. The point here isn't to teach arithmetic; rather it's to show a contract that integrates with the dispatcher without too much cruft getting in the way.

Since I've gone into great detail in previous articles about the inner workings of LLL itself, I'll be glossing over elements previously discussed to reduce repetition. Hopefully this will keep us focused on the important aspects of this contract.

initialize()

In order that a contract be accessible to the dispatcher it only has to implement one function: initialize(). This function must somehow set up a structure of return sizes for the functions in its contract. This is necessary because the dispatcher must pass the results of a function invocation back up to the caller, but the dispatcher has no knowledge of the data coming back from the function. By storing the return size for every function, the dispatcher can look up this size with the return-size macro we defined in utilities.lll.

In part 3 we discussed that the return size storage location for a given function is derived from adding together the contract's address and the function's ID. Given that, storing these sizes is easy. All we have to do is add together the contract's address stored in @@contract-address and a given function's ID and store the function's return size at the resulting storage location offset. For example, for the double function we do this:

(sstore (+ @@contract-address double) 32)

Recall that double is a constant definition that resolves to 0xeee97206. By adding these two numbers together we create a unique storage address for the function's return size which can be later accessed by the dispatcher via the return-size macro.

An aside

For testing purposes I've written each function so that it both logs an event and returns a value. This way, while testing I can both call a function and send a transaction to invoke that same function. Normally you would return a value on a call and emit an event when the function modifies storage in some way. I may come back at some point and clean this up, but it is only an example contract after all, so I may not.

log

After storing function return sizes, initialize() then emits an event heralding the successful completion of the transaction. This involves using LLL's log keyword.

The log keyword takes a varying number of input parameters depending on which variation you invoke. There are five variations: log0 to log4. The number indicates how many "topics" are included in the log output. Obviously log0 includes no topics. According to the Ethereum Contract ABI, Events

…are an abstraction of the Ethereum logging/event-watching protocol. Log entries provide the contract's address, a series of up to four topics and some arbitrary length binary data

For interoperabilty with the Ethereum ABI the first topic is always the sha3 of the event's signature. In Solidity this is declared with the event keyword. We don't have that facility in LLL so we need to construct the event ourselves. Let's use an example:

(mstore call-result true)
(log1 call-result 32 (sha3 0x00 (lit 0x00 "Initialized(bool)")))

The first two parameters after log1 are the memory location and size of the data you wish to log for this function. The third parameter produces the aforementioned sha3 of the event's signature. As previously discussed, sha3 takes two parameters: the location and size of the data to be hashed. The lit keyword in this case takes a string, stores it and returns the string's length. There are two other variations of lit usage that we don't delve into here.

The string supplied to lit represents the "signature" of the event as a function. In this case it's Initialized(bool). Knowing this signature, external applications can search for and filter transaction results to find specific events.

In our LLL code, we always need to use log1 or higher as we'll always be logging at least one topic in the form of the sha3 of the event's signature as described above.

replace(address)

There's a second function that a contract should implement in order to properly interoperate with the dispatcher: replace(address). This function isn't strictly mandatory, as it's not actually called by the dispatcher itself. But without it you wouldn't be able to replace a contract with another, and that's kind of the whole point of the dispatcher.

The purpose of replace() is to do the actual contract replacement. This involves disabling the current contract, storing the address of the replacement contract, then enabling that replacement. The code to do this is quite straightforward and similar to other code we've examined in detail. The whole function works almost identically to the dispatcher's pseudo-constructor with the exception of disabling the current contract and emitting an event upon completion. In this case we use log2 as we're including a topic that contains the address of the old contract in case an external agent wishes to re-enable that contract at some point for whatever reason.

double(uint256) and halve(uint256)

These two functions don't require much explanation at all. They both perform simple arithmetic on the supplied input. Both emit events and both return the result of their respective calculations. These functions exist merely to flesh out our example contract beyond the functions necessary for dispatcher interoperation. In a real contract this is where you would supply your own functions to realize its purpose. We'll be using these two functions when we test out our contract deployments in the next article.

Fallback

The only other thing worth noting in this example contract is the use of a catch-all at the end of the code in the form of

(jump invalid-location)

This is necessary because if the function ID provided doesn't match any of our precomputed function IDs, the contract has been called in error. In that case we want to cause an EVM error so that any ether that may have been sent to the contract is automatically returned to the caller. This is the equivalent of Solidity's fallback function which gets executed when there's no match to the supplied function ID or no function is specified at all. We could have executed any arbitrary code as a fallback; I have opted to simply throw an error in our case.

Contract-level modifiers

It's worth mentioning the use of a modifier here on the contract itself as opposed to on an individual function. The contract-enabled modifier simply checks the contract's enabled flag and bails if the contract has been disabled. As far as I know, in Solidity modifiers can only be applied to functions, not whole contracts.

Conclusion

Well that was a fairly straightforward! It brings us to the end of our discussion of LLL code—at least with respect to the dispatcher and its related files and contracts. In the next article I will be demonstrating how to compile, deploy and invoke functions on these contracts, both via RPC and web3 calls. Hopefully that will only require one article, but with the way I go on I wouldn't be surprised if it takes two more articles to conclude this series. I'll see you then!