Source code is hosted in Github: wenweih/ethereum-cold-wallet

As cryptocurrency becomes more and more popular between people. Importantly, with hight profits return, someone has increased the proportion of digital asset in their asset allocation. For those with strong faith about that blockchain technology is the first time for the human to protect their private property cann't be infringed and transfer the value without any restriction by third party, are attracted by Blockchain technology. At the same time, the safety of cryptocurrency is more importanter then ever before.

Ethereum become the world's second most valuable cryptocurrency. for those who hold huge amounts of ETH, need a safety wallet to store private key. This post introduces how to make a hightest level of safety Ethereum HD cold wallet, including generate mnemonic, keystore and passphrse, construct raw transaction and sign it offline offchain, finally send signed transaction to Ethereum network.

mnemonic and keystore

From mnemonic and keystore with passphrse, we can recovery our account quickly and safely.

ethereum-hdwallet.png

If you don't know what is HD wallet, pls refer to my preview post 数字货币钱包 - 助记词 及 HD 钱包密钥原理

  • go-bip39 A golang implementation of the BIP0039 spec for mnemonic seeds
  • hdkeychain Package hdkeychain provides an API for bitcoin hierarchical deterministic extended keys

BIP44 Multi-Account Hierarchy for Deterministic Wallets,Jaxx, Metamask and imToken for Ethereum default path is m/44'/60'/0'/0/0, so we do.

Firstly generate mnemonic, entropy with a length of 128 to 256 bits (multiple of 8), we can derive sequence of 12 to 24 words

// Generate a mnemonic for memorization or user-friendly seeds
mnemonic, err := mnemonicFun()
if err != nil {
  return nil, err
}

...
func mnemonicFun() (*string, error) {
    // Generate a mnemonic for memorization or user-friendly seeds
    entropy, err := bip39.NewEntropy(128)
    if err != nil {
        return nil, err
    }
    mnemonic, err := bip39.NewMnemonic(entropy)
    if err != nil {
        return nil, err
    }
    return &mnemonic, nil
}

Secondly, pass mnemonic string to hdWallet(mnemonic string) (*ecdsa.PrivateKey, *string, error), you can custom your specify path also.

privateKey, path, err := hdWallet(*mnemonic)
if err != nil {
  return nil, err
}
...

func hdWallet(mnemonic string) (*ecdsa.PrivateKey, *string, error) {
    // Generate a Bip32 HD wallet for the mnemonic and a user supplied password
    seed := bip39.NewSeed(mnemonic, "")

    // Generate a new master node using the seed.
    masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
    if err != nil {
        return nil, nil, err
    }

    // This gives the path: m/44H
    acc44H, err := masterKey.Child(hdkeychain.HardenedKeyStart + 44)
    if err != nil {
        return nil, nil, err
    }

    // This gives the path: m/44H/60H
    acc44H60H, err := acc44H.Child(hdkeychain.HardenedKeyStart + 60)
    if err != nil {
        return nil, nil, err
    }

    // This gives the path: m/44H/60H/0H
    acc44H60H0H, err := acc44H60H.Child(hdkeychain.HardenedKeyStart + 0)
    if err != nil {
        return nil, nil, err
    }

    // This gives the path: m/44H/60H/0H/0
    acc44H60H0H0, err := acc44H60H0H.Child(0)
    if err != nil {
        return nil, nil, err
    }

    // This gives the path: m/44H/60H/0H/0/0
    acc44H60H0H00, err := acc44H60H0H0.Child(0)
    if err != nil {
        return nil, nil, err
    }

    btcecPrivKey, err := acc44H60H0H00.ECPrivKey()
    if err != nil {
        return nil, nil, err
    }

    privateKey := btcecPrivKey.ToECDSA()

    path := "m/44H/60H/0H/0/0"

    return privateKey, &path, nil
}

nice, we have implement Ethereum Account generation. As we know, from mnemonic with path, a specify account can be recovery; with keystore and passphrse, we can recovery account to, safe them seperate!

// save mnemonic
saveMnemonic(address, *mnemonic, *path)

// save keystore to configure path
saveKetstore(privateKey, fixedPwd, randomPwd)
// save random pwd with address to configure path
saveRandomPwd(address, randomPwd)
// save fixed pwd with address to configure path
saveFixedPwd(address, fixedPwd)

construct raw transaction

We own cold wallet now, if we want to our cold wallet ETH, how can we transfer safely?

The answer is: construct raw transaction online, sign raw transaction, and copy the signed tx and then broadcast to Ethereum network

func (db ormBbAlias) constructTxField(address string) (*string, *big.Int, *uint64, *big.Int, error) {
subAddress, err := db.getSubAddress(address)
if err != nil {
    return nil, nil, nil, nil, err
}

switch node {
case "geth":
    balance, nonce, gasPrice, err := nodeConstructTxField("geth", *subAddress)
    if err != nil {
        return nil, nil, nil, nil, err
    }
    return subAddress, balance, nonce, gasPrice, nil
case "parity":
    balance, nonce, gasPrice, err := nodeConstructTxField("parity", *subAddress)
    if err != nil {
        return nil, nil, nil, nil, err
    }
    return subAddress, balance, nonce, gasPrice, nil
case "etherscan":
    balance, nonce, gasPrice, err := etherscan.etherscanConstructTxField(*subAddress)
    if err != nil {
        return nil, nil, nil, nil, err
    }
    return subAddress, balance, nonce, gasPrice, nil
default:
    return nil, nil, nil, nil, errors.New("Only support geth, parity, etherscan")
}
}

construct raw transaction need three key value: amount, sendtransactionCount(Nonce in our code, you can consider is as send transaction index for the address) and gasPrice. we can get those from geth, parity by RPC or etherscan open API. next step is pass these values to new Transaction function.

func constructTx(nonce uint64, balance, gasPrice *big.Int, hexAddressFrom, hexAddressTo string) (*string, *string, *string, *string, *big.Int, error) {
gasLimit := uint64(21000) // in units

if !common.IsHexAddress(hexAddressTo) {
    return nil, nil, nil, nil, nil, errors.New(strings.Join([]string{hexAddressTo, "invalidate"}, " "))
}

var (
    txFee = new(big.Int)
    value = new(big.Int)
)

txFee = txFee.Mul(gasPrice, big.NewInt(int64(gasLimit)))
value = value.Sub(balance, txFee)

tx := types.NewTransaction(nonce, common.HexToAddress(hexAddressTo), value, gasLimit, gasPrice, nil)
rawTxHex, err := encodeTx(tx)
if err != nil {
    return nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"encode raw tx error", err.Error()}, " "))
}
txHashHex := tx.Hash().Hex()
return &hexAddressFrom, &hexAddressTo, rawTxHex, &txHashHex, value, nil
}

sign raw transaction offline

There is an air gap computer for signing raw transaction, which is not connect to the network. Copy the raw transaction hex data to the computer by USB flash drive, and pass it as string to signTx function

func signTx(simpletx *Tx) (*string, *string, *string, *string, *big.Int, *uint64, error) {
txHex := simpletx.TxHex
fromAddressHex := simpletx.From
tx, err := decodeTx(txHex)
if err != nil {
    return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"decode tx error", err.Error()}, " "))
}

if strings.Compare(strings.ToLower(tx.To().Hex()), strings.ToLower(config.To)) != 0 {
    return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"unsign tx to field:", tx.To().Hex(), "can't match configure to:", config.To}, " "))
}

promptSign(tx.To().Hex())

key, err := decodeKS2Key(fromAddressHex)
if err != nil {
    return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"decode keystore to key error", err.Error()}, " "))
}

// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md
// chain id
// 1 Ethereum mainnet
// 61 Ethereum Classic mainnet
// 62 Ethereum Classic testnet
// 1337 Geth private chains (default)
var chainID *big.Int
switch config.NetMode {
case "privatenet":
    chainID = big.NewInt(1337)
case "mainnet":
    chainID = big.NewInt(1)
default:
    return nil, nil, nil, nil, nil, nil, errors.New("you must set net_mode in configure")
}
signtx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey)
if err != nil {
    return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"sign tx error", err.Error()}, " "))
}
msg, err := signtx.AsMessage(types.NewEIP155Signer(chainID))
if err != nil {
    return nil, nil, nil, nil, nil, nil, errors.New(strings.Join([]string{"tx to msg error", err.Error()}, " "))
}

from := msg.From().Hex()
to := msg.To().Hex()
value := msg.Value()
nonce := msg.Nonce()
signTxHex, err := encodeTx(signtx)
hash := signtx.Hash().Hex()
return &from, &to, signTxHex, &hash, value, &nonce, nil
}

broadcast signed transaction

after signed the rawTxHex, we would exported signed data to the json file and then copy the file for broadcasting.

func sendTxCmd(nodeClient *ethclient.Client) {
files, err := ioutil.ReadDir(strings.Join([]string{HomeDir(), config.SignedTx}, "/"))
if err != nil {
    log.Fatalln("read raw tx error", err.Error())
}

for _, file := range files {
    fileName := file.Name()
    tx, err := readTxHex(&fileName, true)
    if err != nil {
        log.Errorln(err.Error())
    }

    signedTxHex := tx.TxHex
    to := config.To
    hash, err := sendTx(signedTxHex, to, nodeClient)
    if err != nil {
        log.Errorln("send tx: ", fileName, "fail", err.Error())
    } else {
        log.Infoln("send tx: ", *hash, "success")
    }
}
}

Demo

▶ ethereum-service genaccount -n 1
INFO[0000]                                               Time:="Wed Jun 20 17:06:19 2018" Using Configure file=/Users/hww/ethereum-service.yml
WARN[0000]                                               Note="all operate is recorded" Time:="Wed Jun 20 17:06:19 2018"
✔ Password: ***
✔ Password: ***
INFO[0005]                                               Generate Ethereum account=0xb51bcc5bCd0f58317C71680E2CaA37f58beBF093 Time:="Wed Jun 20 17:06:24 2018"
WARN[0005]                                               Time:="Wed Jun 20 17:06:24 2018" export address to file=/Users/hww/eth_address.csv
▶ ethereum-service construct -n geth
INFO[0000]                                               Time:="Thu Jun 21 03:10:42 2018" Using Configure file=/Users/hww/ethereum-service.yml
WARN[0000]                                               Note="all operate is recorded" Time:="Thu Jun 21 03:10:42 2018"
INFO[0000] csv2db done
WARN[0000] Ignore: 0xF6C135F6743eE7a17a54aA7a15B4Ff9C15926615 balance not great than the configure amount
WARN[0000] Ignore: 0x33600EEbCC950AA16bE862aD94c8a8c4D7741b60 balance not great than the configure amount
INFO[0000] Exported HexTx to /Users/hww/tx/unsign/unsign_from.0xb51bcc5bCd0f58317C71680E2CaA37f58beBF093.json
▶ ethereum-service sign
INFO[0000]                                               Time:="Thu Jun 21 03:10:49 2018" Using Configure file=/Users/hww/ethereum-service.yml
WARN[0000]                                               Note="all operate is recorded" Time:="Thu Jun 21 03:10:49 2018"
✔ 确认转出地址是否为配置地址: 0x0cEabC861BeEBE8e57a19C26586C14c6f5E7B174: y
INFO[0002] Exported HexTx to /Users/hww/tx/signed/signed_from.0xb51bcc5bCd0f58317C71680E2CaA37f58beBF093.json
▶ ethereum-service send
INFO[0000]                                               Time:="Thu Jun 21 03:11:06 2018" Using Configure file=/Users/hww/ethereum-service.yml
WARN[0000]                                               Note="all operate is recorded" Time:="Thu Jun 21 03:11:06 2018"
INFO[0000] send tx:  0x2bf7504fb597a9ae8c17910e305815e0ebca14851c433e8390889f54a8048d2f success

Finally, if you are insteresting for the code, pls contact me,feel free to donate ETH for me, thx:

0x0cF8eF947DfFf47DD2818f83ac7Da817973790e6