Using bitgo-utxo-lib to build a Zcash Sapling-compatible multisig transparent transaction

The next Zcash major network upgrade, Sapling, is scheduled to go live on October 28th.

We are not going to explain the benefits of the new version since there is plenty of coverage on the internet already (you can read all about it in our blog post: BitGo Ready for Zcash Sapling). Instead, this article is going to show you how to use it!

To do so, we are going to use bitgo-utxo-lib, an open source library used to build transactions for UTXO (unspent transaction output) coins, including Zcash, Bitcoin, Bitcoin Gold, Bitcoin Cash, Dash, and Litecoin.

Moreover, since at BitGo we support multisig wallets based on their security features, I’m going to show you how to spend ZEC (Zcash currency) from a multisig address after Zcash Sapling network upgrade.

If you are familiar with Zcash you should notice that this tutorial builds a transparent transaction, not a shielded one since these are more complex and not yet widely supported. One of Sapling’s goals is to change this by improving the performance of proof building and validation.

Prerequisites

The only piece of software you are going to need for this tutorial is npm. The rest is just plain JavaScript.

Zcash was forked from Bitcoin Core code (not a chain fork!) and the two share many concepts. This tutorial is targeted at developers familiar with either coin.

If you want to build a transaction yourself rather than following the example, you will need:

A multisig address, including the redeemscript and enough private keys to unlock the funds (in this example we will use 2 out of 3 signatures). There is a great answer in StackOverflow by Alex Melville, a fellow BitGo software engineer, on how to get these parameters from a bitcoin full node. You just have to do the same but in a Zcash one (commands are the same). A transaction funding the multisig address. This includes the transaction id, unspent index, and value of such transaction. A Zcash address to send the funds to.

Project setup

First, let’s create a new folder, initialize a default npm project, and add bitgo-utxo-lib to the project dependencies. To do so, open a terminal window and run:

$ mkdir sapling-example/

$ cd sapling-example/

$ npm init -y

$ npm install git+https://github.com/BitGo/bitgo-utxo-lib.git —-save

$ touch index.js

Start coding

Now that we have the project set up, open index.js file and import bitgo-utxo-lib by adding the following line at the top:

const bitGoUTXO = require(‘bitgo-utxo-lib’);

The next step is to instantiate the transaction builder object and configure it to craft transactions following the Zcash Sapling protocol.

bitgo-utxo-lib uses network objects to specify which protocol to use. These encapsulate blockchain specific parameters like bip32 version byte, wif prefix, consensusBranchId (Zcash specific), and more… You can see all network specific parameters in the network file in GitHub.

Add the following lines to the script:

// Choose the configuration for the transaction builder

const zecTestNetwork = bitGoUTXO.networks.zcashTest;

const builder = new bitGoUTXO.TransactionBuilder(zecTestNetwork);

Now that we have a builder, let’s set up the basic fields required by a Sapling transaction: version and group id.

Sapling version number is 4 and it succeeds Overwinter (version 3). Previous to that we had version 1 for the Sprout release.

Group id is a field introduced in the Overwinter upgrade as a network upgrade mechanism for transaction parsing.

The purpose of version group id is to allow unambiguous parsing of “loose” transactions, independent of the context of a block chain. Code that parses transactions is likely to be reused between blockchain branches as defined in [ZIP-200], and in that case the fOverwintered and version fields alone may be insufficient to determine the format to be used for parsing.

For Sapling, the id changed from 0x03C48270 (Overwinter) to 0x892F2085 .

You set these fields in the builder as follow:

// Required Zcash parameters

builder.setVersion(bitGoUTXO.Transaction.ZCASH_SAPLING_VERSION);

builder.setVersionGroupId(parseInt(‘0x892F2085’, 16));

There are two other optional, but popular, fields worth mentioning:

Lock time: Defines the block height after which the transaction can be included in a block by a miner. Zero means it can be immediately added to the blockchain.

Defines the block height after which the transaction can be included in a block by a miner. Zero means it can be immediately added to the blockchain. Expiry height: Defines the block height after which the transaction will be removed from the mempool if they have not been mined. Zero means there is no expiration time.

// Optional parameters

builder.setLockTime(0);

builder.setExpiryHeight(289507);

In this example, we disabled the lock time, meaning the transaction can be picked by a miner as soon as it shows up in the mempool but it can only be included in a block before the blockchain reaches block 289507.

Inputs and Outputs

In the UTXO model, a transaction is made from inputs and outputs.

From the new transaction perspective, inputs are the outputs of a funding transaction which have to be unspent (not referenced by any other transaction in the blockchain).

We have multiple ways to prepare and add inputs using the builder; in this example, we will directly reference the transaction id and output index with the unspent ZEC we will use:

// Build inputs

const transactionId = ‘1d7d9686ed5eeb7154c2fe659fd3eeea169057cb7bc996962945225139669b86’;

const outputIndex = 1; builder.addInput(transactionId, outputIndex);

Now, the outputs of a transaction are the different destinations we want the funds in the input to go. In this case, we will transfer most of the funds to a single address. To do so you only need the destination address and value.

// Build outputs

const destinationAddress = ‘tmKBPqa8qqKA7vrGq1AaXHSAr9vqa3GczzK’;

const transferValue = 199999000; builder.addOutput(destinationAddress, transferValue);

The total amount available in our input transaction is 2 ZEC (200000000 zatoshis), but as you can see, we are only transferring 199999000 zatoshis. This is because we need to pay a fee to the miners and the remaining 1000 zatoshis are used for that.

Signing

Now that we have our builder with the Zcash Sapling parameters, the inputs to use and the outputs to send the funds to, we have to sign the transaction in order to unlock the funds.

Since the funds used by this transaction are in a 2-of-3 multisig address, we require 2 signatures to unlock the funds.

When we create a multisig address with the 3 public keys of the joint owners' account, we are given a redeemscript. In our example, this looks like:

5221021dbb31392fa4857601d5ce2225429923688fede8c2d69e547542cbd88240903a2103c8249e0c474d95e09bb04254d342ef1177f8ca92d2a57356a16df25a4635a5382102fae89068c5c63426f83f0bd5492c4fd757e1f4b575b5d6d05592e8ba519bfd6e53ae

We can decode the raw hex using bitgo-utxo-lib like this:

const redeemScript = Buffer.from(redeemScriptHex, ‘hex’);

console.log(bitGoUTXO.script.toASM(redeemScript));

Then we can see the content in a human readable format:

OP_2 021dbb31392fa4857601d5ce2225429923688fede8c2d69e547542cbd88240903a 03c8249e0c474d95e09bb04254d342ef1177f8ca92d2a57356a16df25a4635a538 02fae89068c5c63426f83f0bd5492c4fd757e1f4b575b5d6d05592e8ba519bfd6e OP_3 OP_CHECKMULTISIG

The script is executed every time we try to move funds out of this address and what it’s saying is that to unlock the funds, it needs at least 2 ( OP_2 ) out of 3 ( OP_3 ) ECDSA signatures to match ( OP_CHECKMULTISIG ) one of the public keys ( 021… 03c… 02f… ). The combination of the signatures with the public keys proves the transaction was created by the real owner of the address in question.

We will need one last parameter, hashType , in order to tell the builder how to sign the transaction. In this example, we are using the default value, SIGHASH_ALL, which means that every output and input in the transaction will be signed. Other hash types like SIGHASH_NONE sign the input but leave the outputs unsigned so miners can later change the destination address. We obviously don’t want the latter.

The Zcash protocol requires us to sign the transaction with the value of the inputs. In other words:

All sighash types commit to the amount being spent by the signed input [ZIP-143].

This extra bit of information helps offline signing devices to calculate the exact amount being spent and transaction fees without having to pull the inputs transaction from the network. This helps with the implementation of lightweight, air-gapped wallets. In our example, the amount is in the constant inputValueToSign .

Add the following lines to the script in order to sign the transaction output at index 0:

// Sign

const redeemScriptHex = ‘5221021dbb31392fa4857601d5ce2225429923688fede8c2d69e547542cbd88240903a2103c8249e0c474d95e09bb04254d342ef1177f8ca92d2a57356a16df25a4635a5382102fae89068c5c63426f83f0bd5492c4fd757e1f4b575b5d6d05592e8ba519bfd6e53ae’;

const redeemScript = Buffer.from(redeemScriptHex, ‘hex’);

const hashType = bitGoUTXO.Transaction.SIGHASH_ALL;

const inputValueToSign = 200000000; // 1st signature

const testPrivateKey1 = ‘cVmRcxsNdhiCigzrBfpv51JtExBPehtMNzUs9CBvymu1B3ch7LLa’;

const keyPair1 = bitGoUTXO.ECPair.fromWIF(

testPrivateKey1, zecTestNetwork); builder.sign(0, keyPair1, redeemScript, hashType, inputValueToSign); // 2nd signature

const testPrivateKey2 = ‘cV2mApzXqoGcGzyoDy5aaiZqQtV5G1HeEuoM1cgpEoiGAeagPeV2’;

const keyPair2 = bitGoUTXO.ECPair.fromWIF(

testPrivateKey2, zecTestNetwork); builder.sign(0, keyPair2, redeemScript, hashType, inputValueToSign);

If we had more outputs, like a change address, we would have to sign those as well by repeating builder.sign step with a different index.

Final Step

Once the transaction builder has the version, group id, inputs, outputs, and signatures, we are ready to build our Zcash Sapling transaction hex by adding the following lines:

// Build final transaction

const signedTransaction = builder.build();

console.log(signedTransaction.toHex());

The build function generates the script signature for each input depending on the script type provided.

In the command line window run:

$ node index.js

You should see the following transaction hex as output:

0400008085202f8901869b6639512245299696c97bcb579016eaeed39f65fec25471eb5eed86967d1d01000000fdfe0000483045022100d95c63feac1c62bfc558d9ada74319b7b71380085ce23bb63419619b1de7392e0220398c8f9dbc13e73520c008fe6a9e473c8baf8849764733f4ffa61efe6f86a0bc01483045022100c77d6c1249a824fc661e0940006c74c2e8f531af64fe9ba06e460d0427a6f1ea0220614fa19bc026a4e0ed71d88df30ebd246cc9ef3d9de8f1bf975a2dc0e9994c48014c695221021dbb31392fa4857601d5ce2225429923688fede8c2d69e547542cbd88240903a2103c8249e0c474d95e09bb04254d342ef1177f8ca92d2a57356a16df25a4635a5382102fae89068c5c63426f83f0bd5492c4fd757e1f4b575b5d6d05592e8ba519bfd6e53aeffffffff0118beeb0b000000001976a91467d674a78a010c82c168718ba42a6bbb1e124af088ac00000000e36a04000000000000000000000000

If you got the same transaction hex, then congrats! You built your first Zcash Sapling compatible transaction.

Finally, to broadcast it into the Zcash network, we have to find a node to do it for us. I used https://explorer.testnet.z.cash/tx/send since it is updated with the latest Sapling compatible software.

Broadcasting a raw transaction using a public full node

Once we hit the “Send transaction” button, our transaction will go to the mempool and wait for a miner to pick it up. This particular transaction can be seen here: https://explorer.testnet.z.cash/tx/20ec1ae4eb2082499c0014a520c13266b18dd134d65274de4f3adca701c9042f

Note that if you try to broadcast this very same transaction you will get the message:

This is because the expiry height value has been passed already. If you try to change it for a valid one and broadcast it, you will get:

And that’s because the funds in the input used in this example have been used already when doing this tutorial.

Conclusion

We’ve seen how to spend the funds from a Zcash multisig address by manually building a Sapling compatible transaction with bitgo-utxo-lib and broadcasting it using a public node.

Most part of this tutorial works for other cryptocurrencies like Bitcoin, you just have to change the network and make sure you are using the right parameters. For instance, version group id and expiry height only apply for Zcash, other coins don’t use these.

You can find the full example in GitHub gist: https://gist.github.com/argjv/289c80dbe89c43179f6f543ed94283ea#file-zecsaplingmultisig-js