Multi-signature addresses on Cardano

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.

7 Likes

All I can say is AWESOME. Thanks so much for the tutorial.

1 Like

Great demo, thanks :+1:

Now how do you generate additional addresses from this policy.script file similar to how the wallets create new unused addresses?

Thank you!

You cannot create multiple addresses using the same key pairs. But you can generate as many key pairs as you want and generate new policies and new addresses.
You can also generate key pairs from a master key created with 12, 15 or 24 recovery words, just like a wallet is doing, if this is what you want.
As long as the generated addresses are using the same stake key, they will be seen as belonging to the same wallet by blockchain explorers.

Did you mean that you can create additional addresses from a master key which was originally generated by a wallet?

Also, do you know how to create a multisig address using hardware wallet keys?

Yes, you can generate as many key pairs (and addresses from them) starting from the master key generated from the recovery words. Take a look here for more details:

You should also be able to create multisig addresses protected by keys from hardware wallets, of course, because all you need are the public keys from different hardware wallets (that you will use to sign), and you can get them with this command:

cardano-hw-cli address key-gen \
--path 1852H/1815H/0H/0/0 \
--verification-key-file payment-0.vkey \
--hw-signing-file payment-0.hwsfile

The file payment-0.vkey is the verification (public) key from the first address in the hardware wallet. You can find cardano-hw-cli on github: Releases · vacuumlabs/cardano-hw-cli · GitHub.

I did not test this, though, but I see no reason why it wouldn’t work. If I have time later today, I will test it and update this thread.

1 Like

I tested the multi signature feature with 2 hardware wallets and almost everything is working, with a small difference. Because the Ledger firmware (I have Ledger hardware wallets) does not support Alonzo era transactions yet, you have to create the transaction with build-raw.
My transaction looked like this:

cardano-cli transaction build-raw \
--mary-era \
--tx-in ${TXIN} \
--tx-out ${DSTADDRESS}+4820000 \
--fee 180000 \
--tx-in-script-file ${POLICY_PATH}/policy.script \
--out-file tx.raw

After witnessing the transaction and submitting it, the 4.8 tADA were transferred.
As soon as Vacuum Labs will update the Ledger firmware to support Alonzo era transactions (this will happen soon: Error signing or witnessing alonzo-era transactions · Issue #114 · vacuumlabs/cardano-hw-cli · GitHub), the “transaction build” command should also work, as in the guide.

1 Like

I was still confused about how to generate additional addresses and the keys for these addresses. Then I read this post: Create a signing key from an address in Daedalus wallet (Resolved) - #7 by bwbush

Explaining:

The payment keys in the wallet are generated by the series 1852H/1815H/0H/0/0, 1852H/1815H/0H/0/1, 1852H/1815H/0H/0/2, …
The change keys in the wallet are generated by the series 1852H/1815H/0H/1/0, 1852H/1815H/0H/1/1, 1852H/1815H/0H/1/2, …

2 Likes

I explained here how to create an address where 2 from 3 signatures are required to sign transactions:

https://forum.cardano.org/t/re-help-needed-to-create-multisig-wallets-for-dao-opperations/91812/2?u=georgem1976

The link seems broken: Oops! That page doesn’t exist or is private.

OOps, that was a private message sent to me and I answered it, but I thought it is a post o nanother topic. Here it is:

It is not required to write a new article for 2 out of 3 signatures required to sign a transaction, because the difference is very small. I just tested it, to be 100% sure it works before posting it.

The policy for 2 of 3 required signatures looks like this:

{
  "type": "atLeast",
  "required": 2,
  "scripts":
  [
    {
      "type": "sig",
      "keyHash": "28cbb7e9f4c32f10eb6356294c07c367b2d4b5c831cf4cdd96322ab9"
    },
    {
      "type": "sig",
      "keyHash": "797f85f68c8c9a563f2e8a7c2c8fb066d49b800f9540ea5dfe526f16"
    },
    {
      "type": "sig",
      "keyHash": "cd8d28d53f7fb26d95c8b53d0e2e51e8f76b20dd064162ae604a804c"
    }
  ]
}

So the change would be in the “02-policy_script.sh” file, it becomes:


. ./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": "atLeast",
  "required": 2,
  "scripts":
  [
    {
      "type": "sig",
      "keyHash": "${KEYHASH0}"
    },
    {
      "type": "sig",
      "keyHash": "${KEYHASH1}"
    },
    {
      "type": "sig",
      "keyHash": "${KEYHASH2}"
    }
  ]
}
EOF
fi
3 Likes