Optimizing Contracts
CashScript contracts are transpiled from a solidity syntax to Bitcoin Script by the cashc
compiler. Bitcoin Script is a lower level language (a list of stack-based operations) where each available operation is mapped to a single byte.
Depending on the complexity of the contract or system design, it may sometimes be useful to optimize the Bitcoin Script by tweaking the contract in CashScript before it is compiled. Below are some ideas to get started.
Tricks and tips.
The cashc
compiler does some optimisations automatically, but by ordering statements in a specific way, the compiler is better able to optimise it. Trial & error is a big part of it, but here's some tricks that may help:
- It's best to "consume" values as soon as possible ("consume" meaning their final use in the contract). This frees up space on the stack.
- Use/consume values as close to their declaration as possible, both for variables and for parameters. This avoids having to do deep stack operations. This example from anyhedge illustrates consuming values immediately.
- Avoid if-statements if possible, instead try to "inline" them. This is because the compiler cannot know which branches will be taken, and therefore cannot optimise those branches as well. This example from anyhedge illustrates inlining flow control.
Example Workflow
For simplicity and security, the compiler separates the data and logic of the redeem script. All the data used by the redeem script gets pushed to the stack, and the redeem script is then the same regardless of the parameters being used in a particular instance of a contract.
As an example, below is a simple hodl contract by mainnet-pat:
// hodl.cash
pragma cashscript ^0.9.0;
contract hodl(
int locktime,
bytes20 pubkeyHash
) {
function spend(pubkey ownerPubkey, sig ownerSig) {
require(tx.time >= locktime);
require(hash160(ownerPubkey) == pubkeyHash);
require(checkSig(ownerSig, ownerPubkey));
}
}
For the above script, this spend
path transpiles to a redeem script of:
<pubkeyHash> <locktime> b1 75 78 a9 88 ac
Here the data are pushed first, and the operations corresponding to the bytes in Bitcoin Script have short names as follows:
hex | Operation Code |
---|---|
0xb1 | OP_CHECKLOCKTIMEVERIFY |
0x17 | OP_DROP |
0x57 | OP_OVER |
0xa9 | OP_HASH160 |
0x88 | OP_EQUALVERIFY |
0xac | OP_CHECKSIG |
In the artifact generated by cashc
, the redeem script is listed as follows:
cashc hodl.cash | grep bytecode;
"bytecode": "OP_CHECKLOCKTIMEVERIFY OP_DROP OP_OVER OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG"
As you can see, in this case, the logic to spend time-locked funds and verify the spender takes up six (6) bytes of information.
The simplest workflow to optimize the redeem script of hodl.cash
, or any contract, would be to compile it directly from the command line with cashc
then look at the bytecode
field on the generated artifact to see if there are any unnecessary or duplicative operations.
In this toy example, the redeem script is so small there isn't a lot of need or use in making it smaller. The anyhedge contracts are again a great example of a more complex progressive optimization over time.
Lastly
There are two important alternative approaches to optimization to consider.
OP_NOP
We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
It's worth considering whether optimizing the redeem script is necessary at all. If the contract is accepted by the network, and there is no glaring inefficiency in the bytecode, perhaps the best optimization is to not to obsess prematurely about things like blocksize.
Hyper Introspect-imization
Finally, there have been a number of novel approaches taken since 2009 to approximate higher level functionality with bitcoin's deliberately restricted instruction set. These techniques include using a cumbersome old covenant style of storing data, OP_CODESEPARATOR
to split contracts around an old 520-byte limitation, and hiding redeem paths in "sidecars".
It's very important to be up-to-date on documentation covering introspection, as it greatly simplifies using state and writing more complex contracts. If a contract has been developed from an old example or a pre-introspection design pattern, the fastest way to achieve significant optimization is to utilize the the latest introspection design patterns.