Debugging
Debugging is no walk in the park. This is especially true for debugging complex smart contracts. Luckily there are strategies that can make it easier for developers to discover bugs in their contracts.
Categories of Bugs
There are 2 broad categories of smart contract bugs:
1. Bug in Transaction Building
The first category of bugs is a bug in the transaction building meaning the 'invocation' of your smart contracts fails. This means the bug is in the usage of the CashScript Transaction builder and you need to carefully review the shape of your transaction and whether it matches the requirements imposed by the smart contract UTXOs.
2. Bug in Contract Logic
The second category of bugs is a bug in the smart contract logic which prohibits valid spending, this results in the shape of the Transaction builder not matching with the contracts simply because there is a coding error in the contract! Carefully review the logic in the failing line and if needed check the documentation so you are sure about the functionality of your CashScript contract code.
Whatever category your bug falls into, the first step of debugging is understanding what line in your CashScript contract is making your transaction get rejected. Afterwards, investigation needs to start whether it's a transaction building bug or a bug in contract logic.
Debugging Tools
The Transaction Builder has deep integration with libauth to enable local transaction evaluation, without actual interaction with the Bitcoin Cash network. This allows for fully integrated debugging functionality.
Error messages
If a CashScript transaction is evaluated with .debug() or is sent to a network and rejected, then the transaction will be evaluated locally using libauth to provide the failure reason and debug information. Here is an example of what a CashScript error message looks like:
HodlVault.cash:23 Require statement failed at input 0 in contract HodlVault.cash at line 23.
Failing statement: require(price >= priceTarget)
Bitauth IDE: [link]
Read the error message to see which line in the CashScript contract causes the transaction validation to fail. Investigate whether the contract function invocation is the issue (on the TypeScript SDK side) or whether the issue is in the CashScript contract itself (so you'd need to update your contract and recompile the artifact). If it is not clear why the CashScript contract is failing on that line, then you can use the following two strategies: console logging & Bitauth IDE stack trace.
Console Logging
To help with debugging you can add console.log statements to your CashScript contract file to log variables. This way you investigate whether the variables have the expected values when they get to the failing require statement in the CashScript file. After adding the console.log statements, recompile your contract so they are added to your contract's Artifact.
Bitauth IDE
Whenever a transaction fails, there will be a link in the console to open your smart contract transaction in the BitAuth IDE. This will allow you to inspect the transaction in detail, and see exactly why the transaction failed. In the BitAuth IDE you will see the raw BCH Script mapping to each line in your CashScript contract. Find the failing line and investigate the failing OpCode. You can break up the failing line, one opcode at a time, to see how the stack evolves and ends with your require failure.
It's also possible to export the transaction for step-by-step debugging in the BitAuth IDE without failure. To do so, you can call the getBitauthUri() function on the transaction. This will return a URI that can be opened in the BitAuth IDE.
const uri = transactionBuilder.getBitauthUri();
It is unsafe to debug transactions on mainnet using the BitAuth IDE as private keys will be exposed to BitAuth IDE and transmitted over the network.
The Bitauth IDE will show you the two-way mapping between the CashScript contract code and the generated opcodes. User-defined functions are included with the same mapping: each function definition is rendered as a push group annotated with the function's own source lines, and imported functions are annotated with the file they are imported from.
Here is a Bitauth IDE link for an example HalfTimeVault contract, which uses a half() function imported from math.cash. Note the source-mapped function definition (OP_DEFINE) at the top, the OP_INVOKE call sites, and the >>> annotation rows:
// "HalfTimeVault" contract constructor parameters
<timeout> // int = <0x90d003>
<owner> // pubkey = <0x034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa>
// bytecode
/* >>> function half (imported from math.cash) */
< /* function half(int amount) returns (int) { */
OP_2 OP_DIV /* return amount / 2; */
> OP_0 OP_DEFINE /* } */
/* */
/* pragma cashscript ^0.14.0; */
/* */
/* import "./math.cash"; */
/* */
/* contract HalfTimeVault(pubkey owner, int timeout) { */
/* // Early claims must leave half the coins in the vault */
OP_2 OP_PICK OP_0 OP_NUMEQUAL OP_IF /* function claimEarly(sig ownerSig) { */
OP_3 OP_ROLL OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(ownerSig, owner)); */
OP_0 OP_INVOKE OP_CHECKLOCKTIMEVERIFY OP_DROP /* require(tx.time >= half(timeout)); */
OP_INPUTINDEX OP_UTXOVALUE /* int vaultValue = tx.inputs[this.activeInputIndex].value; */
OP_0 OP_OUTPUTVALUE OP_SWAP OP_0 OP_INVOKE OP_GREATERTHANOREQUAL /* require(tx.outputs[0].value >= half(vaultValue)); */
OP_NIP /* >>> scope cleanup */
OP_ELSE /* } */
/* */
/* // After the full timeout, the owner can claim everything */
OP_ROT OP_1 OP_NUMEQUALVERIFY /* function claim(sig ownerSig) { */
OP_ROT OP_SWAP OP_CHECKSIGVERIFY /* require(checkSig(ownerSig, owner)); */
OP_CHECKLOCKTIMEVERIFY OP_DROP /* require(tx.time >= timeout); */
OP_1 /* } */
OP_ENDIF /* } */