Search code examples
pythongocryptographycosmos-sdk

How to generate hd wallet keys & addresses given seed phrase with cosmos sdk?


I am trying to generate hd wallet private keys , public keys and addresess using cosmos sdk. Below is the equivalent implementation in python which generates the keys , address as expected but when trying to generated in golang using cosmos sdk it won't generate same keys. Any inputs for equivalent golang version of the python implementation is much appreciated. Thank you.

Python

seed = Mnemonic.to_seed("blast about old claw current first paste risk involve victory edit current", passphrase="")
print("Seed: ", seed.hex())
purpose = 44
coinType = 118
account = 0
change = 0

hdwallet = HDWallet()
hdwallet.from_seed(seed=seed.hex())

for addressIndex in range(1):
    hdwallet.clean_derivation()
    hdwallet.from_index(purpose, hardened=True)
    hdwallet.from_index(coinType, hardened=True)
    hdwallet.from_index(account, hardened=True)
    hdwallet.from_index(change)
    hdwallet.from_index(addressIndex, hardened=True)

    print("---")
    print("Derivation Path: ", hdwallet.path())
    print("Private Key: ", hdwallet.private_key())
    print("Public Key: ", hdwallet.public_key())

    readdr_bytes = b"\x04" + bytearray.fromhex(hdwallet.public_key())
    readdr_bytes5 = bech32.convertbits(readdr_bytes, 8, 5)
    wallet_addr = bech32.bech32_encode("atom", readdr_bytes5)
    print("Wallet Address: ", wallet_addr)

OUTPUT

Derivation Path: m/44'/118'/0'/0/0' Private Key: 69668f2378b43009b16b5c6eb5e405d9224ca2a326a65a17919e567105fa4e5a Public Key: 03de79435cbc8a799efc24cdce7d3b180fb014d5f19949fb8d61de3f21b9f6c1f8 Wallet Address: atom1qspau72rtj7g57v7lsjvmnna8vvqlvq56hcejj0m34sau0eph8mvr7qgl9avu

GoLang ( Gernerating differnt keys )

import (
    "github.com/tendermint/tendermint/crypto/secp256k1"
    "github.com/cosmos/cosmos-sdk/crypto/hd"
    "github.com/cosmos/go-bip39"
    "github.com/decred/dcrd/bech32"
 )

path := hd.BIP44Params{
    Purpose:      44,
    CoinType:     118,
    Account:      0,
    Change:       false,
    AddressIndex: 0,
}

seed := bip39.NewSeed("blast about old claw current first paste risk involve victory edit current","")

master, ch := hd.ComputeMastersFromSeed(seed)
priv, err := hd.DerivePrivateKeyForPath(master, ch, path.String())
if err != nil {
    log.Fatal(err)
}
var privKey = secp256k1.GenPrivKeySecp256k1(priv)
pubKey := privKey.PubKey()
fmt.Println(hex.EncodeToString(pubKey.Bytes()))

decodeString, _ := hex.DecodeString(fmt.Sprintf("04%x", pubKey.Bytes()))

// Convert test data to base32:
conv, err := bech32.ConvertBits(decodeString, 8, 5, true)
if err != nil {
    fmt.Println("Error:", err)
}
encoded, err := bech32.Encode("atom", conv)
if err != nil {
    fmt.Println("Error:", err)
}

// Show the encoded data.
fmt.Println("Atom address:", encoded)

OUTPUT


Derivation Path: m/44'/1022'/0'/0/0' Private Key: 84925813eac8c1dc39f170e93b3bebe0bf961ac9c46507e858ce178a1a715c26 Public Key: 0204a0bad86cafed2daf1b4080a3e908afcf524e2a9c24e20817920c478d537cc1 Wallet Address: atom1qsp3yaurlt463pl6pekgae4yudlcwk2dhxt93cxz5d5ymw3j8xmngaqef5j7p


Solution

  • The results of both codes differ because of two issues:

    • In the Go Code, the private key is derived incorrectly:

      In the Python code, the path m/44'/118'/0'/0/0' is used, as the output of hdwallet.path() shows. In the Go code, in contrast, the path m/44'/118'/0'/0/0 is used, as shown by the output of path.String().

      To use the path of the Python code in the Go code, the path can be e.g. specified directly. For this the line:

      priv, err := hd.DerivePrivateKeyForPath(master, ch, path.String())
      

      has to be replaced by:

      priv, err := hd.DerivePrivateKeyForPath(master, ch, "m/44'/118'/0'/0/0'")
      

      and the path variable can be removed.

      With this change, the Go code provides the same private key as the Python code.

    • In the Go Code, the private key is imported incorrectly. Therefore, the public key is determined wrongly. To fix this, the line:

      var privKey = secp256k1.GenPrivKeySecp256k1(priv)
      

      must be replaced by:

      var privKey = secp256k1.PrivKey(priv) 
      

      With this change, the Go code gives the same public key as the Python code.

    The rest of the Go code is functionally equivalent to the Python code, so the Go code produces the same address as the Python code.

    The complete Go code is:

    package main
    
    import (
        "encoding/hex"
        "fmt"
        "log"
    
        "github.com/cosmos/cosmos-sdk/crypto/hd"
        "github.com/cosmos/go-bip39"
        "github.com/decred/dcrd/bech32"
        "github.com/tendermint/tendermint/crypto/secp256k1"
    )
    
    func main() {
    
        seed := bip39.NewSeed("blast about old claw current first paste risk involve victory edit current", "")
        fmt.Println("Seed: ", hex.EncodeToString(seed)) // Seed:  dd5ffa7088c0fa4c665085bca7096a61e42ba92e7243a8ad7fbc6975a4aeea1845c6b668ebacd024fd2ca215c6cd510be7a9815528016af3a5e6f47d1cca30dd
    
        master, ch := hd.ComputeMastersFromSeed(seed)
        path := "m/44'/118'/0'/0/0'"
        priv, err := hd.DerivePrivateKeyForPath(master, ch, path)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("Derivation Path: ", path)                 // Derivation Path:  m/44'/118'/0'/0/0'
        fmt.Println("Private Key: ", hex.EncodeToString(priv)) // Private Key:  69668f2378b43009b16b5c6eb5e405d9224ca2a326a65a17919e567105fa4e5a
    
        var privKey = secp256k1.PrivKey(priv)
        pubKey := privKey.PubKey()
        fmt.Println("Public Key: ", hex.EncodeToString(pubKey.Bytes())) // Public Key:  03de79435cbc8a799efc24cdce7d3b180fb014d5f19949fb8d61de3f21b9f6c1f8
    
        decodeString, err := hex.DecodeString(fmt.Sprintf("04%x", pubKey.Bytes()))
        if err != nil {
            log.Fatal(err)
        }
    
        // Convert test data to base32:
        conv, err := bech32.ConvertBits(decodeString, 8, 5, true)
        if err != nil {
            fmt.Println("Error:", err)
        }
        encoded, err := bech32.Encode("atom", conv)
        if err != nil {
            fmt.Println("Error:", err)
        }
    
        // Show the encoded data.
        fmt.Println("Wallet Address:", encoded) // Wallet Address: atom1qspau72rtj7g57v7lsjvmnna8vvqlvq56hcejj0m34sau0eph8mvr7qgl9avu
    }