Developers’ Corner: Notes on Multisig UTXO in Cardano

Nicolas Di Prima
dcSpark
Published in
6 min readJan 27, 2022

Over the past several months, dcSpark has allocated significant research and development efforts toward Cardano multisig transactions, with specific focus on building EVM bridges. As part of this, we are creating a Solidity smart contract that will allow the creation and coordination of Cardano multisig transactions using any EVM-compatible chain as middleware. This will be an important component for any future work involving decentralized bridges between Cardano and EVM-compatible blockchains.

During our research, we began investigating how to best protect the assets wrapped on an EVM-based Milkomeda sidechain. For this, we settled on an approach leveraging Cardano's "native script" functionality. In this article, I'd like to elaborate more on Cardano native scripts and how we will be using it.

The first thing to note is that native scripts are not smart contracts, and they are not linked in any way to Plutus. They allow Milkomeda Validators to collectively manage the wrapped assets and to guarantee that a rogue validator will not steal.

Cardano Native Script

As mentioned above, native scripts are not Plutus smart contracts. They are a set of rules that one can define in order to replace the witness of a UTxO. We already all use something very similar to the native script when performing our normal daily transactions, which operates as a “script” that says “the owner of the private key associated to the address controlling the UTxO needs to provide a valid signature (and the public key) in order to spend that UTxO”.

If we look at the composition of a transaction, we have Inputs and Outputs. The outputs are creating new UTxOs while the inputs are “spending” UTxOs. And for each input added to the transaction, we need to also have that number of witnesses.

Here we have 1 input. We expect 1 witness. And if you are using a normal spending address all you need to provide in the witness is the cryptographic signature and the public key. The public key needs to match the one set in the address of the UTxO being used as input. In a normal spending address you have the hash of the public key that is allowed to spend the UTxO.

In addition to these normal spending addresses, we also have Script Addresses. These contain the hash of the script which defines the rules on how a UTxO can be spent. To send assets to a Script Address, you don’t need to know what these rules are. However, when you try to spend the UTxO controlled by the Script Address, you need to give the original native script and its hash needs to match the one of the Script Address.

What is a Native Script

Once again, a native script is a set of rules that defines how you can spend a UTxO. The following is how you can define a multi-signature scheme for your UTxO (a multisig wallet). There are other things you can do as well, and for more information I encourage you to review the excellent Cardano Documentation.

If we take the transaction example above and modify it for native script, we may have something that looks like this:

We still have the inputs and outputs as usual. But now we have 2 witnesses even though we have only one input. And we have also attached our native script.

Here in this example, the native script may be something like:

Here we have defined that we need at least 2 signatures from the signers.

Going further, it is possible to do more complex scripts. Indeed, the native script offers other features such as time locks: you can enforce that the UTxO cannot be spent after a certain amount of time, or cannot be spent before a certain amount of time. Additionally, scripts are composable. So you can have:

So here in this case, you would have, one of the signers “key 2” or “key 3” after the given blockchain slot as passed or both of the signers together.

How to use native assets (dev)

Now let’s dive into the software side of things for my fellow developers struggling with a similar situation. So far, there is no wallet offering a simple use of Native Script. Last time I checked, the haskell wallet backend was looking into adding these but the feature was experimental at best. And we don’t have the luxury of running both the nodes and the wallet backend on all the targets we want to (Flint, ccvault and adalite, all require to run light in the user’s browser, so that rules out all IOHK’s libraries and tools).

The approach we took was to use the cardano-serialization-lib. It’s lightweight enough. It’s in Rust. And there are TypeScript bindings too (using the library compiled to WASM).

Constructing the Native Script

The first thing you will want to do is to build your native script.

The first thing to do is to build your NativeScript. We are using a simple model for Milkomeda, we already know our native script will look like the following:

And then to build it you have to use something like this:

This seems a bit convoluted and there are a lot of types and transformations to do. This is because the Native Scripts are composable. You can then compose scripts to increase the complexity of the spending requirements.

Generating the Native Script Address

Now that you have the script loaded. You will certainly want to generate the address and start receiving funds.

In order to do that I have split the operation into two functions. A function to get the hash of the native script. And a function to get the address. Because later I might want to only extract the hash for a different purpose.

I know the address names are odd. This is because cardano-serialization-lib has used IOHK’s specs to generate the different blockchain types. There is no stake here and there are no enterprise addresses.

The way we could rename these would be: Credential and Spending Address.

And that’s it you have now your address and you can start requesting funds from the cardano testnet faucet to do more tests.

Using Native Script in Transactions

There are some caveats when building transactions that require using native script. Some are linked to limitations in the cardano-serialization-lib because of the Cardano protocol.

Fees are not deterministic in Cardano

Indeed it is not possible to predict how much a transaction is going to cost before you have built it. Cardano is using CBOR for their encoding format. And there are different valid ways to encode certain values (such as numbers). It’s even more true once you add Native Script. Your native script may require you spend in one of the two way:

  • Either a time lock has expired
  • Or one of the signers is signing the transaction

Either you will need to add 64 bytes signatures and 32 bytes public key as witness or nothing at all.

So knowing in advance the fee you will need to pay when building a transaction including Native Script UTxOs will need considerations and synchronization with the different parties to try to get the best fee out of the transaction.

The other caveat is that cardano-serialization-lib does not support adding Native Script in the transaction builder.

This is mainly because of the first point. It is not possible to easily determine what is going to be the transaction fee in advance.

First thing to do is to create your transaction as usual. The way we have done it in Milkomeda is like so.

First you prepare you transaction builder:

Then you add your Outputs:

Now we don’t know what the fee will be in advance. So instead we have decided to go with the simplest approach which is to assume the worst fee. This is solving the caveat 1 we saw above

And then add your inputs. We are using a fake_address instead of the input’s address. This is because of caveat 2 we saw earlier. The transaction builder is trying to determine the fee of the transaction once it is finished but it cannot do that for a NativeScript. So we are cheating a bit and we are setting something else instead:

From there you are mostly done. You only need to finalize the transaction, add the native script in and provide the witnesses:

--

--