This post was first published on Medium.
We present an optimization technique to split a large contract into multiple smaller ones, to reduce its size drastically while maintaining correctness. We demonstrate how it works in contracts with large loops and many public functions.
Looping takes the following format in sCrypt:
Because the loop is statically unrolled, the maximal loop count maxLoopCount has to be known at compile time. If it is set too small, the contract may not unlock successfully and funds are burned forever. It is thus conservatively set for the worse case scenario and can often result in excessive script bloating, when the most frequently used loop count is significantly smaller than the worse case one.
We show how to use Pay to Contract Hash (P2CH) to reduce the loop count in contract IncrementeLocktime in our last article for the most prevalent cases, while keeping the contract working when the largest loop count is needed.
As the below diagram shows, we move the function partialSha256(), whose looping takes most of the contract size, to a separate contract PartialSha256. Each iteration in the loop processes one chunk in the preimage of SHA256. We call the function from the main contract IncrementLocktimeSplit using P2CH.
IncrementLocktimeSplit is the same with contract IncrementLocktime, except that partialSha256() is removed and indirectly called using P2CH, from Line 15 to 30.
Each element in the array calleeContractHashes is the hash of the contract PartialSha256, with a different MAX_CHUNKS. The array thus contains all MAX_CHUNKS supported by the main contract.
Contract PartialSha256 computes partialSha256() (the same function taken from the original contract IncrementLocktime) and stores the function call arguments and result value in the output, which is accessed by contract IncrementLocktimeSplit as we did in Inter-Contract call.
Compared to IncrementLocktime, IncrementLocktimeSplit is much smaller. It can dynamically call partialSha256() depending on the length of the data to be hashed, i.e., MAX_CHUNKS. In most cases, we only need MAX_CHUNKS of 1. In the rare case that locktime is split across the last two chunks, we use MAX_CHUNKS of 2. This brings significant savings since each additional chunk adds ~60KB to the final script.
Multiple Public Functions
We can break a contract with multiple public functions into multiple smaller contracts, each containing just one public function. We replace the original contract with a new Main contract that can call any of the smaller contract using the same technique in the above section.
Main Contract after Splitting
This size reduction is most prominent when each public function is large and there are many of them.
In addition to size reduction, this optimization allows contracts in other inputs of the same contract to identify which public function is called in the current main contract, which is otherwise unknownable due to the inability to access to unlocking script in OP_PUSH_TX. This can be accessed by injecting the smaller contract transaction, e.g., tx1.
We have illustrated how to reduce large contract size by breaking it into multiple smaller contracts. The example contracts are one-off and stateless. Reduction would be more significantly for stateful contracts, which are called consecutively and savings are accumulated.
An alternative to optimize contracts with many public functions is using Merklized Abstract Syntax Tree.
The idea originates from Sensible Contract, which uses it extensively in production.
Watch: CoinGeek New York presentation, Smart Contracts & Computation on Bitcoin
New to Bitcoin? Check out CoinGeek’s Bitcoin for Beginners section, the ultimate resource guide to learn more about Bitcoin—as originally envisioned by Satoshi Nakamoto—and blockchain.