Cardano multi-signature addresses

Cardano multi-signature addresses

I heard a few times about multi-signature addresses on Cardano, but I never really saw an example using such an address. I found some documentation about this here, but I realized it is outdated and the commands are not working anymore as they are on this page. This is why I decided to write this post after doing some successful tests with multi-signature addresses.

But first, what is a multi-signature address? A multi-signature address is an address associated with multiple private keys, which can be in the possession of different persons, so that a transaction from that address can be performed only when all the private keys are used to sign the transaction.

I decided to create 3 private keys for my multi-signature address demo, and I did it on testnet, but it is the similar to mainnet. I am using Daedalus-testnet as my cardano node for my demo. I also created a Github repository with the files used in this demo, to be easier to create locally the files, in case someone wants to test this.

First thing I created was a file with a few environment variables that will be used by all the scripts. This also generates the folders required for the other scripts, in case they do not already exist, and the protocol parameters file, required by some of the commands. I called this file “env”:

#!/bin/bash


# for testnet
CARDANO_NET="testnet"
CARDANO_NET_PREFIX="--testnet-magic 1097911063"
# for mainnet
#CARDANO_NET="mainnet"
#CARDANO_NET_PREFIX="--mainnet"
#
KEYS_PATH=./wallet
ADDRESSES_PATH=./wallet
FILES_PATH=./files
POLICY_PATH=./policy
PROTOCOL_PARAMETERS=${FILES_PATH}/protocol-parameters.json
export CARDANO_NODE_SOCKET_PATH=~/.local/share/Daedalus/${CARDANO_NET}/cardano-node.socket

if [ ! -d ${KEYS_PATH} ] ; then
  mkdir -p ${KEYS_PATH}
fi

if [ ! -d ${ADDRESSES_PATH} ] ; then
  mkdir -p ${ADDRESSES_PATH}
fi

if [ ! -d ${FILES_PATH} ] ; then
  mkdir -p ${FILES_PATH}
fi

if [ ! -d ${POLICY_PATH} ] ; then
  mkdir -p ${POLICY_PATH}
fi

if [ ! -f ${PROTOCOL_PARAMETERS} ] ; then
  cardano-cli query protocol-parameters --out-file  ${PROTOCOL_PARAMETERS} ${CARDANO_NET_PREFIX}
fi

The first script will generate the 3 pairs of private and public keys used to control the multi-signature address. Because it is also possible to associate the address with a staking key and delegate it to a stake pool, I also created a stake keys pair and I will also generate later the address including the stake address. This is the first script, called “01-keys.sh”:

#!/bin/bash


. ./env

for i in {0..2}
do
  if [ -f "${KEYS_PATH}/payment-${i}.skey" ] ; then
    echo "Key already exists!"
  else
    cardano-cli address key-gen --verification-key-file ${KEYS_PATH}/payment-${i}.vkey --signing-key-file ${KEYS_PATH}/payment-${i}.skey
  fi
done

cardano-cli stake-address key-gen --verification-key-file ${KEYS_PATH}/stake.vkey --signing-key-file ${KEYS_PATH}/stake.skey

Executing this script with “. 01-keys.sh” will create the folders, will generate the 3 payment key pairs and the stake key pair in the “wallet” folder, and will also generate the protocol-parameters.json file in the “files” folder.

The next step is generating the policy script that will require all 3 payment keys to be used when doing a transaction. I called this script “02-policy_script.sh”:

#!/bin/bash


. ./env

KEYHASH0=$(cardano-cli address key-hash --payment-verification-key-file ${KEYS_PATH}/payment-0.vkey)
KEYHASH1=$(cardano-cli address key-hash --payment-verification-key-file ${KEYS_PATH}/payment-1.vkey)
KEYHASH2=$(cardano-cli address key-hash --payment-verification-key-file ${KEYS_PATH}/payment-2.vkey)


if [ ! -f ${POLICY_PATH}/policy.script ] ; then
cat << EOF >${POLICY_PATH}/policy.script
{
  "type": "all",
  "scripts":
  [
    {
      "type": "sig",
      "keyHash": "${KEYHASH0}"
    },
    {
      "type": "sig",
      "keyHash": "${KEYHASH1}"
    },
    {
      "type": "sig",
      "keyHash": "${KEYHASH2}"
    }
  ]
}
EOF
fi

Executing this (with “. 02-policy_script.sh”) will generate the policy/policy.script file.

The next step is to compute the multi-signature payment address from this policy script, which includes the hashes of the 3 payment verification keys generated in the first step. I computer the address both with and without the including the stake address. I called this script “03-script-addr.sh”:

#!/bin/bash


. ./env

cardano-cli address build \
--payment-script-file ${POLICY_PATH}/policy.script \
${CARDANO_NET_PREFIX} \
--out-file ${ADDRESSES_PATH}/script.addr

cardano-cli address build \
--payment-script-file ${POLICY_PATH}/policy.script \
--stake-verification-key-file ${KEYS_PATH}/stake.vkey \
${CARDANO_NET_PREFIX} \
--out-file ${ADDRESSES_PATH}/script-with-stake.addr

Don’t forget to execute this script: “. 03-script-addr.sh”. After that, you can send some testnet funds (tADA) from your wallet to this address (found in wallet/script.addr). You can also request 1000 tADA from the testnet faucet. I requested them from the faucet right now. You can check if you received the funds like this:

$ cardano-cli query utxo ${CARDANO_NET_PREFIX} --address $(<wallet/script.addr)
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
14d8610f16738b41c3d1f...224e1e792ceb1c9db279#0     0        1000000000 lovelace + TxOutDatumNone

As you can see, the 1000 tADA are there in my example (I censored a few characters from the transaction id).

The next step is to actually test sending the tADA from this address to a different address with a transaction. I created a file “wallet/dev_wallet.addr” where I wrote the address where I want to send the funds. The script used to create this transaction is “04-transaction.sh”:

#!/bin/bash


. ./env

ADDRESS=$(cat ${ADDRESSES_PATH}/script.addr)
DSTADDRESS=$(cat ${ADDRESSES_PATH}/dev_wallet.addr)

TRANS=$(cardano-cli query utxo ${CARDANO_NET_PREFIX} --address ${ADDRESS} | tail -n1)
UTXO=$(echo ${TRANS} | awk '{print $1}')
ID=$(echo ${TRANS} | awk '{print $2}')
TXIN=${UTXO}#${ID}

cardano-cli transaction build \
--tx-in ${TXIN} \
--change-address ${DSTADDRESS} \
--tx-in-script-file ${POLICY_PATH}/policy.script \
--witness-override 3 \
--out-file tx.raw \
${CARDANO_NET_PREFIX}

I created the transaction with the newer “cardano-cli transaction build” command, because this will also automatically compute the minimum required fees for the transaction, and we skip 2 steps (calculating the fee and generating the transaction with the correct fees) compared to using the “cardano-cli transaction build-raw” method. Also notice the “–tx-in-script-file” parameter, which is very important when using multi-signature addresses, and the “–witness-override 3” used to calculate the correct transaction fees, because we are using 3 private keys to sign the transaction later.

Executing this script (“. 04-transaction.sh”) will generate the “tx.raw” raw transaction file, and will also inform us about the fees for the transaction: “Estimated transaction fee: Lovelace 178657”.

We can examine the transaction file using this command:

$ cardano-cli transaction view --tx-body-file tx.raw 
auxiliary scripts: null
certificates: null
era: Alonzo
fee: 178657 Lovelace
inputs:
- 14d8610f16738b41...d6224e1e792ceb1c9db279#0
metadata: null
mint: null
outputs:
- address: addr_test1...yy33
  address era: Shelley
  amount:
    lovelace: 999821343
  datum: null
  network: Testnet
  payment credential:
    key hash: e98ef513e28e93b909183292cd27956ddd9939ec6afcbee8694386ab
  stake reference:
    key hash: 10e893f172924ccbe98d3629b9dce63b26664c1c567af0b31327e596
update proposal: null
validity range:
  lower bound: null
  upper bound: null
withdrawals: null

I censored the characters in the input UTxO and in the destination address in the output above.

Now the transaction needs to be signed. This can be done using “cardano-cli transaction sign” (the “05-sign.sh” script file), but this is only possible when one person has all the private keys, and the whole idea of multi-signature addresses is that the private keys are distributed to different persons. This is why we need to “witness” the transaction with all the different signature (private) payment keys, and assemble the signed transaction from all of them. This is done in the script “05-witness.sh”:

#!/bin/bash


. ./env

cardano-cli transaction witness \
--signing-key-file ${KEYS_PATH}/payment-0.skey \
--tx-body-file tx.raw \
--out-file payment-0.witness \
${CARDANO_NET_PREFIX}

cardano-cli transaction witness \
--signing-key-file ${KEYS_PATH}/payment-1.skey \
--tx-body-file tx.raw \
--out-file payment-1.witness \
${CARDANO_NET_PREFIX}

cardano-cli transaction witness \
--signing-key-file ${KEYS_PATH}/payment-2.skey \
--tx-body-file tx.raw \
--out-file payment-2.witness \
${CARDANO_NET_PREFIX}


cardano-cli transaction assemble \
--tx-body-file tx.raw \
--witness-file payment-0.witness \
--witness-file payment-1.witness \
--witness-file payment-2.witness \
--out-file tx.signed

If you test both ways and compare the results, you will see that the “tx.signed” generated files are identical. Don’t forget to execute the script: “. 05-witness.sh”. The file “tx.signed” will be generated, and the last step of the demo is to submit the signed transaction to a cardano node (using the “06-submit.sh” script):

#!/bin/bash


. ./env

cardano-cli transaction submit \
--tx-file tx.signed \
${CARDANO_NET_PREFIX}

Execute the script:

$ . 06-submit.sh 
Transaction successfully submitted.

After some seconds (next block being minted), the funds should be at the destination address (I censored a few characters from the transaction id):

$ cardano-cli query utxo ${CARDANO_NET_PREFIX} --address $(<wallet/dev_wallet.addr)
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
8902f1b5e18cc494a36f8...ac5653df5cfea3b550ce     0        999821343 lovelace + TxOutDatumNone

Subtracting the transaction fee from the 1000 tADA, we will see that the 999816899 are exactly the amount expected to be at the destination address:

$ expr 1000000000 - 178657
999821343

Also interrogating the script address will show that the 1000 tADA are no longer there:

$ cardano-cli query utxo ${CARDANO_NET_PREFIX} --address $(<wallet/script.addr)
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------

And this concludes my demo with multi-signature addresses on Cardano.

I also tested the scripts with funds being sent to the script address that includes the stake address (“wallet/script-with-stake.addr”), in case you were wondering. This type of address can be used to delegate the funds at a multi-signature address to a stake pool.