AnyHedge Case Study: The Positive Impact of Native Introspection and Better Math
As discussed recently on BCH Network Discussions, scripting for Bitcoin Cash is likely to improve in the next network upgrade with native introspection and better math. Let's use the AnyHedge smart contract to see how these upgrades can increase the utility and value of BCH.
The basic design of the AnyHedge contract is very simple - just a handful of calculations. In 2019, using @pein 's accessible description of BCH smart contracts and Spedn compiler, we wrote the first pseudo-code of the contract in about an hour, and converted it to a real contract in about a day. The result looked good, but as we have written before in "What kind of limits does BCH scripting have?", scripting limitations made the simple version of the contract behave badly in terms of precision and how much value the contract could handle. Additionally, the contract code was large and complex due to the covenant technique that enables meaningful smart contracts.
We made some painful trade-offs, switched to the CashScript compiler and ended up with reasonable precision and larger values at the expense of greatly increased complexity that you can read about in the contract, increased size that led to fewer features, and complex contract boundaries that we analyzed at great length.
The small section of the AnyHedge contract in the image gives a sense of the complexity needed to make AnyHedge functional given the contract limitations.
Regarding covenant technique, the CashScript compiler does a great job of handling it, but all the size and complexity is still there in the final low level code.
The combination of contract complexity and covenant technique used up all the space available in scripting limits, forcing us to put aside ideas such as tradeable contracts which required more code and data.
Let's look at what happens if we get the network upgrades.
Transaction Introspection, whether native or not, allows us to inspect the details of the current transaction within a script. This technique is the heart of BCH smart contracts. Picking out one specific example of how the change from Non-Native to Native Introspection affects AnyHedge, we will focus on the one-input rule. Detailed technical changes made possible for the AnyHedge contract are here.
Ensuring that the entire payout of a contract comes from exactly one input is an important design decision for AnyHedge. It enforces the best practice of funding a contract in a single transaction instead of the risky prospect of the two parties funding a contract separately. It also removes exploit possibilities involved with handling multiple inputs.
The idea is simple, and with Native Introspection, the high level code is equally simple:
# With Native Introspection # Directly confirm that there is exactly one input require(tx.inputs.length == 1)
With the current scripting system using non-native introspection, it only takes about two lines of high level code to get the same result, but there is a large amount of complexity and inefficiency hiding behind it:
# Without Native Introspection # First, perform the technique that enables non-native introspection. # This is actually handled internally by CashScript but shown # as pseudo-code for the purposes of this article. require(checkSig(signature, publicKey)) require(checkDataSig(signature, preimage, publicKey)) # Indirectly confirm that there is exactly one input require(tx.hashPrevouts == hash256(tx.outpoint))
Warning: Without native introspection, we need to use the non-native introspection technique. A full explanation would take a lot of space, but we have tried to include just enough to make it clear how much work and complexity is involved. You can just scan the description if you would like to go on with the rest of the article.
Before thinking about the details of what information we want to introspect in the transaction data, we need access to the transaction data itself. In current scripting, there is no direct access to general transaction data except through one point - the signature of the current input, and this is quite indirect access. The first step is to do the standard confirmation that the user-provided signature (generated by your wallet) matches the internal data available to the validating node. Note that we don't provide the data because the validating node is comparing against its internal representation of the transaction:
Now we have a critical piece of information - that the signature is based on data that exactly matches the inaccessible transaction data.
That allows us to do the second step of the non-native introspection technique - confirm that the same signature matches the data that we provide to the script. We have called our data "preimage" in line with the naming used for the same data that is internal to the validating node:
require(checkDataSig(signature, preimage, publicKey))
We have convinced the script that it has a piece of data from the user that exactly matches the actual data of this transaction. Only now, we can move on to the introspection part.
Unfortunately, the introspection part is also complex. We have the preimage but it is not a convenient data construct. It is just a blob of data. There is a significant part of the code not shown that cuts the preimage data up into smaller pieces in order to get just the parts that we need. This process is very risky for non-expert programmers as it opens up vectors for attack with maliciously constructed preimage data.
We will move on and assume that we have extracted two key pieces of information from the user-provided preimage. This last step is the third puzzle that we need to solve on the way to our goal - confirm that the transaction has exactly one input:
hashPrevoutsis the hash of all inputs in this transaction. That is the only piece of information available about the inputs. It is only a hash so we can't check anything directly about the contents. We will have to reconstruct the actual contents ourselves and confirm that the hash of the actual contents matches this value.
outpointwhich confusingly is the current input that the script is handling (scripts handle one input at a time).
Finally we can confirm that the the hash of all inputs (the only information available about all inputs) is equal to the hash of the current input:
require(tx.hashPrevouts == hash256(tx.outpoint))
(End of technical section)
After all of that, we have finally confirmed that the two hashed values are indeed equal. In other words, we confirmed that this one input is equal to all transaction inputs. In other words, we confirmed that there is exactly one input.
To reinforce the point, here again is the same result using Native Introspection where the script is allowed access to the data instead of going through an elaborate process:
// With Native Introspection require(tx.inputs.length == 1)
Safe: The low level code, whether created by a scripting wizard or a compiler, is much less complex, with less attack surface for exploits.
Efficient: For the network, the transaction itself is smaller and nodes can execute the contract faster, using significantly less resources. Especially, native introspection reduces signature and hashing operations which are by far the most expensive things a script can do.
It is important to note that above we described the changes involved with a single line of code. There are more changes in the AnyHedge contract, and the reduction in complexity is similar for every one of them.
Fun for Builders: Native Introspection removes a lot of concern and worry from the app building process, making it more fun and attractive to developers and entrepreneurs.
Bigger numbers is the main part of the proposed math upgrade for BCH scripting. In straightforward terms, scripts currently are limited to 32 bit numbers. The upgrade allows 64 bit numbers. Detailed technical changes made possible for the AnyHedge contract are here.
To provide an example of why this is important, consider the size of numbers when dealing with Satoshis (all value numbers in transactions are Satoshis):
// Biggest positive 32 bit number = (2^31)-1 2,147,483,647 // Size of 1 BCH in Satoshis = 10^8 100,000,000 // Biggest positive 64 bit number = (2^63)-1 9,223,372,036,854,775,807
You can see that the 32-bit number and 1 BCH are very close in size. The result is that even the most simple calculations cannot deal with more than 21 BCH directly. With the still basic calculations that AnyHedge needs to do, the amounts that a script can handle directly are much much smaller. Without deriving all the details, an AnyHedge contract would only be able to handle a $0.21 cent contract.
Not only size, but also precision of calculations is a critical issue in scripts because there is no such thing as decimal places. Everything is done in terms of whole numbers. In other words, 3/4 is not equal to 0.75. It is equal to zero with a remainder of 3.
Explaining the size and precision issues in detail would take many pages of discussion. As a proxy for that, if you are interested you can read the following:
Discussion of size issues in the comments of the contract code
Safe, Precise, Fun for Builders: Most of the issues involved in complexity, size and precision simply go away with the scale of 64 bit numbers. In other words, it will be harder for a smart contract developer to make mistakes. With AnyHedge specifically, the contract will be able to handle a much larger range of values. For example with USD it will handle contracts from cents to billions of dollars, and the calculations will be precise across the range. Again, it can't be emphasized enough that app development becomes more accessible and engaging by removing the sharp edges.
Bitcoin has not had multiplication since 2010 when a chainsplit-causing bug was found with the "left shift" operator. When removing that operator, a long list of other operators were also removed, including multiplication.
In addition to Big Numbers, the math upgrade also brings back the multiplication operator. Detailed technical changes made possible for the AnyHedge contract are here.
Regarding effects on AnyHedge, the current AnyHedge contract uses pre-multiplied numbers which are then divided in the contract as a kind of fake multiplication. Switching from division-as-fake-multiplication to actual multiplication, AnyHedge behavior mostly stays the same on the surface. However the precision improves significantly due to a subtle issue involving how prices are represented. For example with USD, currently prices are represented as Cents per BCH. With multiplication, we get better precision of Satoshis per Cent.
When CashScript has upgraded to work on testnet with the introspection and math upgrades, we will update this article or publish a new one that measures in detail what the efficiency gains are and show more definitively how much simpler the final low level code will be.
Easier to write UTXO contracts for #everyone, not just us
We hope that the examples above convinced you about the improvements in complexity, size and safety that the upcoming upgrade brings. Importantly, this is much more than just us or our contracts: developers who spend less time worrying about complex bugs, who have more space to work with, and generally have an easier time writing contracts are happy developers. Expect to see more diverse apps using smart contracts on the BCH main chain.
Ten months ago, General Protocols made and published a painful decision not to support the above improvements for BCH Network in the 2021 May upgrade ("Raising the Bar on Bitcoin Cash Upgrades"). We thought that prioritizing the stability of the network and establishment of the CHIP process were more important steps toward long term success. Now that the CHIP process is becoming more accepted as a replacement for central decision making, General Protocols very much looks forward to these improvements that will be useful not only AnyHedge, but all BCH smart contracts.
General Protocols Blog
This article forms part of the General Protocols Blog, a collection of cross-platform links showcasing our team's community activity, Bitcoin Cash projects, UTXO development, and general crypto musings.