Skip to content

Instantly share code, notes, and snippets.

@ilap
Last active November 20, 2023 17:30
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilap/5af151351dcf30a2954685b6edc0039b to your computer and use it in GitHub Desktop.
Save ilap/5af151351dcf30a2954685b6edc0039b to your computer and use it in GitHub Desktop.
Extracting Pool Staking keys from Ledger wallet

Introduction for Ledger wallet based addresses

DISCLAIMER: NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK

UPDATED: 16:51pm AEST 09/Aug/2020

There are two keypairs that are required to register a pool:

  1. reward account (costs and rewards) and
  2. owner stake (pledge) keypair.

Note: these keys can be the same when the owner and the operator are the same.

So, that would be nice if the pool operators/owner could track and controle their pool rewards from any wallet (Daedalus, Yoroi or any other wallet) that support stakings.

The aim of this document is achieve this, by using some Shelley wallet-, address- and key related tools.

The only problem is how to extract the staking key(s) from a wallet. Luckily there is the IOHK's cardano-wallet repo that contains cardano-address that can be used for this as it contains the BIP39, BIP32-ED25519 and master key generation functionalities which are required to achive this.

Create wallet and extract staking keys from it.

The following steps are required for extracting staking keys from a wallet:

  1. Generate a Ledger wallet and save the 24-word length mnemonic as usual.
  2. Use the javascript below to generate the Ledger's master (root) key.
  3. Derive the staking signing key from the master key using the 1852H/1815H/0H/2/0 (cardano-address derivation format).
  4. generate the staking verification key from the above signing key
  5. Used the extracted keypair or keypairs (if additional wallets is created for owner and operator) for pool registration and or pledge.

To create a Ledger wallet you need to have the Ledger's latest firmware and the Cardano app version 2.0.3 installed.

Downloads

Cardano-wallet binaries

From latest releases https://github.com/input-output-hk/cardano-wallet/releases or from Hydra: https://hydra.iohk.io/build/3662127/download/1/cardano-wallet-shelley-2020.7.28-linux64.tar.gz https://hydra.iohk.io/build/3662151/download/1/cardano-wallet-shelley-2020.7.28-macos64.tar.gz https://hydra.iohk.io/build/3662143/download/1/cardano-wallet-shelley-2020.7.28-win64.zip

Ledger master key generation

Please use the following javascript to generate the Ledger's master keys from your ledger (12, 18, 24) mnemonic. https://repl.it/@PalDorogi/ledger2pool#index.js

Install

package.json

{
  "name": "ledger2pool",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "ilap",
  "license": "MIT",
  "dependencies": {
    "bip39": "^3.0.2"
  },
  "devDependencies": {},
  "description": ""
}

Example

$ mkdir ledger && pushd ledger
$ cat << EOF >> package.json
{
  "name": "ledger2pool",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "ilap",
  "license": "MIT",
  "dependencies": {
    "bip39": "^3.0.2"
  },
  "devDependencies": {},
  "description": ""
}
EOF

$ npm i

# "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
# were used in this example
$ node index.js
Ledger Master Key: 402b03cd9c8bed9ba9f9bd6cd9c315ce9fcc59c7c25d37c85a36096617e69d418e35cb4a3b737afd007f0688618f21a8831643c0e6c77fc33c06026d2a0fc93832596435e70647d7d98ef102a32ea40319ca8fb6c851d7346d3bd8f9d1492658

## It generates CNTools compatible wallett for easy to use.
$ ./master2addr.sh ~/priv/wallet/LEDGER "402b03cd9c8bed9ba9f9bd6cd9c315ce9fcc59c7c25d37c85a36096617e69d418e35cb4a3b737afd007f0688618f21a8831643c0e6c77fc33c06026d2a0fc93832596435e70647d7d98ef102a32ea40319ca8fb6c851d7346d3bd8f9d1492658"
{
    "stake_reference": "by value",
    "stake_key_hash": "1d227aefa4b773149170885aadba30aab3127cc611ddbc4999def61c",
    "address_style": "Shelley",
    "spending_key_hash": "14c16d7f43243bd81478e68b9db53a8528fd4fb1078d58d54a7f1124",
    "network_tag": 1
}
Generated from 1852H/1815H/0H/{0,2}/0
addr1qy2vzmtlgvjrhkq50rngh8d482zj3l20kyrc6kx4ffl3zfqayfawlf9hwv2fzuygt2km5v92kvf8e3s3mk7ynxw77cwqf7zhh2
Important the base.addr and the base.addr_candidate must be the same
Also this address must be the same /w the 1st "/0" Ledger's generated Shelley
address, currently only in Adalite as now (09/Aug/2020).
1c1
< addr1qy2vzmtlgvjrhkq50rngh8d482zj3l20kyrc6kx4ffl3zfqayfawlf9hwv2fzuygt2km5v92kvf8e3s3mk7ynxw77cwqf7zhh2
---
> addr1qy2vzmtlgvjrhkq50rngh8d482zj3l20kyrc6kx4ffl3zfqayfawlf9hwv2fzuygt2km5v92kvf8e3s3mk7ynxw77cwqf7zhh2
\ No newline at end of file

$ cntools.sh
$ popd

main.js

const crypto = require("crypto");
const bip39 = require('bip39')

function toByteArray(hexString) {
  var result = new Uint8Array(hexString.length / 2)
  for (var i = 0; i < hexString.length; i += 2) {
    result[i / 2] = parseInt(hexString.substring(i, i + 2), 16)
  }
  return result;
}

const toHexString = bytes =>
    bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');

/* 
  Originally from Adrestia https://input-output-hk.github.io/adrestia/docs/key-concepts/hierarchical-deterministic-wallets/
  But, unfortunately it did not work, so had to figure it out and fix it.
*/
function generateLedgerMasterKey(seed, password) {

  let masterSeed = crypto.pbkdf2Sync(
    bip39.entropyToMnemonic(seed), 
    "mnemonic" + password, 
    2048, 
    64, 
    'sha512')
  
  // In the Adrestia's pseudo code it had
  // let message = "1" + seed, which was wrong.
  let message = new Uint8Array([1, ...masterSeed])
  let cc = crypto.createHmac('sha256',"ed25519 seed")
    .update(message)
    .digest()

  let i = hashRepeatedly(masterSeed);
  let tweaked = tweakBits(i)
  let masterKey = new Uint8Array([...tweaked, ...cc])

  return masterKey
}

function hashRepeatedly(message) {
    let i = crypto.createHmac('sha512',"ed25519 seed")
    .update(message)
    .digest()

    if (i[31] & 0b0010_0000) { 
        return hashRepeatedly(i);
    }
    return i;
}

function tweakBits(data) {
    // * clear the lowest 3 bits
    // * clear the highest bit
    // * set the highest 2nd bit
    data[0]  &= 0b1111_1000;
    data[31] &= 0b0111_1111;
    data[31] |= 0b0100_0000;

    return data;
}

(async () => {

  /*
  ledger addresses for 12-word abandon ... about
  /0 addr1qy2vzmtlgvjrhkq50rngh8d482zj3l20kyrc6kx4ffl3zfqayfawlf9hwv2fzuygt2km5v92kvf8e3s3mk7ynxw77cwqf7zhh2
  /1 addr1q9d9xypc9xnnstp2kas3r7mf7ylxn4sksfxxypvwgnc63vcayfawlf9hwv2fzuygt2km5v92kvf8e3s3mk7ynxw77cwqx9wh62
  1852'/1815'/0'/0/0
  */
  // !!!!! DO NOT USE YOUR REAL MNEMONIC IN REPL AS OTHERS COULD EASILY GET 
  // IT  FROM HERE!!!!  
  // SAVE ALL OF THE FILES AND MOVE THEM TO SOME AIRGAPPED SERVER
  //
  // DISCLAIMER: NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK
  const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
  const passphrase = ""

  const entropy = bip39.mnemonicToEntropy(mnemonic)

  const masterKey = generateLedgerMasterKey(entropy, passphrase)

  var masterString = toHexString(masterKey)
  console.log(`Leger Master Key: ${masterString}`)
})();

Script

#!/bin/bash

CADDR=${CADDR:=$( which cardano-address )}
[[ -z "$CADDR" ]] && {
        echo "cardano-address cannot be found, exiting..." >&2 ;
        exit 127
}

CCLI=${CCLI:=$( which cardano-cli )}
[[ -z "$CCLI" ]] && {
        echo "cardano-cli cannot be found, exiting..." >&2
        exit 127
}

BECH32=${BECH32:=$( which bech32 )}
[[ -z "$BECH32" ]] && {
        echo "bech32 cannot be found, exiting..." >&2
        exit 127
}

[[ "$#" -ne 2 ]] && {
       echo "usage: `basename $0` <ouptut dir> <Ledger Master Key>" >&2
echo "Masterkey is generated /w a javascript" >&2
       exit 127
}
OUT_DIR="$1"
[[ -e "$OUT_DIR"  ]] && {
       echo "The \"$OUT_DIR\" is already exist delete and run again." >&2
       exit 127
} || mkdir -p "$OUT_DIR" && pushd "$OUT_DIR" >/dev/null

shift
MASTERKEY="$*"

# Generate the master key from the mentiond javascript and derive the stake account keys
# as extended private key (XPrv)
echo "$MASTERKEY" | bech32 xprv > root.prv

cat root.prv |\
"$CADDR" key child 1852H/1815H/0H/2/0 > stake.xprv

cat root.prv |\
"$CADDR" key child 1852H/1815H/0H/0/0 > payment.xprv

TESTNET=0
MAINNET=1
NETWORK=$MAINNET

cat payment.xprv |\
"$CADDR" key public | tee payment.xpub |\
"$CADDR" address payment --network-tag $NETWORK |\
"$CADDR" address delegation $(cat stake.xprv | "$CADDR" key public | tee stake.xpub) |\
tee base.addr_candidate |\
"$CADDR" address inspect
echo "Generated from 1852H/1815H/0H/{0,2}/0"
cat base.addr_candidate
echo

# XPrv/XPub conversion to normal private and public key, keep in mind the
# keypars are not a valind Ed25519 signing keypairs.
TESTNET_MAGIC="--testnet-magic 42"
MAINNET_MAGIC="--mainnet"
MAGIC="$MAINNET_MAGIC"

SESKEY=$( cat stake.xprv | bech32 | cut -b -128 )$( cat stake.xpub | bech32)
PESKEY=$( cat payment.xprv | bech32 | cut -b -128 )$( cat payment.xpub | bech32)

cat << EOF2 > stake.skey
{
    "type": "StakeExtendedSigningKeyShelley_ed25519_bip32",
    "description": "",
    "cborHex": "5880$SESKEY"
}
EOF2

cat << EOF3 > payment.skey
{
    "type": "PaymentExtendedSigningKeyShelley_ed25519_bip32",
    "description": "Payment Signing Key",
    "cborHex": "5880$PESKEY"
}
EOF3

"$CCLI" shelley key verification-key --signing-key-file stake.skey --verification-key-file stake.evkey
"$CCLI" shelley key verification-key --signing-key-file payment.skey --verification-key-file payment.evkey

"$CCLI" shelley key non-extended-key --extended-verification-key-file payment.evkey --verification-key-file payment.vkey
"$CCLI" shelley key non-extended-key --extended-verification-key-file stake.evkey --verification-key-file stake.vkey


"$CCLI" shelley stake-address build --stake-verification-key-file stake.vkey $MAGIC > stake.addr
"$CCLI" shelley address build --payment-verification-key-file payment.vkey $MAGIC > payment.addr
"$CCLI" shelley address build \
    --payment-verification-key-file payment.vkey \
    --stake-verification-key-file stake.vkey \
    $MAGIC > base.addr

echo "Important the base.addr and the base.addr_candidate must be the same"
echo "Also this address must be the same /w the 1st /0 Ledgers generated Shelley"
echo "address, currently only in Adalite as now (09/Aug/2020)."
diff base.addr base.addr_candidate

popd >/dev/null
@jbpin
Copy link

jbpin commented Feb 27, 2021

Hi @ilap, thanks for all of your work so far. I have a question for you.
When I run

cardano-cli stake-address key-gen \
        --verification-key-file stake.vkey \
        --signing-key-file stake.skey

I get a key that don't have StakeSigningKeyShelley_ed25519_bip32 but StakeSigningKeyShelley_ed25519 and the cborHex do not start by 5880 but 5820

{
    "type": "StakeSigningKeyShelley_ed25519",
    "description": "Stake Signing Key",
    "cborHex": "58200467febacfa7d9f8e19940591b96dd3202c5a82b43ca5076dbcf934f4379d622"
}

Can you help me to understand the difference ? thanks

@HofmannZ
Copy link

Getting the error bech32 cannot be found, exiting.... But can't find the binary anywhere online. Any idea where it comes from?

@adamsthws
Copy link

Getting the error bech32 cannot be found, exiting.... But can't find the binary anywhere online. Any idea where it comes from?

After downloading the wallet, did you export the path?

eg...

wget https://hydra.iohk.io/build/3662127/download/1/cardano-wallet-shelley-2020.7.28-linux64.tar.gz

echo "f75e5b2b4cc5f373d6b1c1235818bcab696d86232cb2c5905b2d91b4805bae84 *cardano-wallet-shelley-2020.7.28-linux64.tar.gz" | shasum -a 256 --check

tar -xvf cardano-wallet-shelley-2020.7.28-linux64.tar.gz

export PATH="$(pwd)/cardano-wallet-shelley-2020.7.28:$PATH"

@shroomist
Copy link

just a quick report:

  1. I've used cardano-addresses for $CADDR
  2. I've used bech32 for BECH32
  3. generated mnemonic (15 word), root key, payment and stake keys with cardano-address
  4. successfully registered staking address after following your script

@ubani
Copy link

ubani commented Sep 2, 2022

FWIW and if someone stumbles into here having issues.

The script might need an update:

cat > extractPoolStakingKeys.sh << HERE
#!/bin/bash 

CADDR=\${CADDR:=\$( which cardano-address )}
[[ -z "\$CADDR" ]] && ( echo "cardano-address cannot be found, exiting..." >&2 ; exit 127 )

CCLI=\${CCLI:=\$( which cardano-cli )}
[[ -z "\$CCLI" ]] && ( echo "cardano-cli cannot be found, exiting..." >&2 ; exit 127 )

OUT_DIR="\$1"
[[ -e "\$OUT_DIR"  ]] && {
       	echo "The \"\$OUT_DIR\" is already exist delete and run again." >&2 
       	exit 127
} || mkdir -p "\$OUT_DIR" && pushd "\$OUT_DIR" >/dev/null

shift
MNEMONIC="\$*"

# Generate the master key from mnemonics and derive the stake account keys 
# as extended private and public keys (xpub, xprv)
echo "\$MNEMONIC" |\
"\$CADDR" key from-recovery-phrase Shelley > root.prv

cat root.prv |\
"\$CADDR" key child 1852H/1815H/0H/2/0 > stake.xprv

cat root.prv |\
"\$CADDR" key child 1852H/1815H/0H/0/0 > payment.xprv

TESTNET=0
MAINNET=1
NETWORK=\$MAINNET

cat payment.xprv |\
"\$CADDR" key public --with-chain-code | tee payment.xpub |\
"\$CADDR" address payment --network-tag \$NETWORK |\
"\$CADDR" address delegation \$(cat stake.xprv | "\$CADDR" key public --with-chain-code | tee stake.xpub) |\
tee base.addr_candidate |\
"\$CADDR" address inspect
echo "Generated from 1852H/1815H/0H/{0,2}/0"
cat base.addr_candidate
echo

# XPrv/XPub conversion to normal private and public key, keep in mind the 
# keypars are not a valid Ed25519 signing keypairs.
TESTNET_MAGIC="--testnet-magic 1097911063"
MAINNET_MAGIC="--mainnet"
MAGIC="\$MAINNET_MAGIC"

SESKEY=\$( cat stake.xprv | bech32 | cut -b -128 )\$( cat stake.xpub | bech32)
PESKEY=\$( cat payment.xprv | bech32 | cut -b -128 )\$( cat payment.xpub | bech32)

cat << EOF > stake.skey
{
    "type": "StakeExtendedSigningKeyShelley_ed25519_bip32",
    "description": "",
    "cborHex": "5880\$SESKEY"
}
EOF

cat << EOF > payment.skey
{
    "type": "PaymentExtendedSigningKeyShelley_ed25519_bip32",
    "description": "Payment Signing Key",
    "cborHex": "5880\$PESKEY"
}
EOF

"\$CCLI" key verification-key --signing-key-file stake.skey --verification-key-file stake.evkey
"\$CCLI" key verification-key --signing-key-file payment.skey --verification-key-file payment.evkey

"\$CCLI" key non-extended-key --extended-verification-key-file payment.evkey --verification-key-file payment.vkey
"\$CCLI" key non-extended-key --extended-verification-key-file stake.evkey --verification-key-file stake.vkey


"\$CCLI" stake-address build --stake-verification-key-file stake.vkey \$MAGIC > stake.addr
"\$CCLI" address build --payment-verification-key-file payment.vkey \$MAGIC > payment.addr
"\$CCLI" address build \
    --payment-verification-key-file payment.vkey \
    --stake-verification-key-file stake.vkey \
    \$MAGIC > base.addr

echo "Important the base.addr and the base.addr_candidate must be the same"
diff base.addr base.addr_candidate
popd >/dev/null
HERE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment