# 离线、安全的生成私钥/公钥/地址

By [Wei Zhong](https://paragraph.com/@wei-zhong) · 2023-01-01

---

**代码：**[**https://github.com/w-zhong/Generate-SK-PK-Address**](https://github.com/w-zhong/Generate-SK-PK-Address)

与比特币采取的UTXO模式不同，以太坊采取的是账户模式，以太坊地址可以看作一个个账户，它们都具有**唯一性**，**私密性**和**权限控制**的能力。

与现实中银行账户不同的是，以太坊地址背后不仅仅可以是个人和公司，同样也可以是协议，因为地址是可以包含代码的，这类地址被称作合约地址。

首先我们来看一下以太坊地址的定义和分类：

*   外部地址（Externally Owned Account，EOA）：由私钥生成并控制
    
*   合约地址（Smart Contract Account）：由EOA部署生成，由内部代码控制
    

![以太坊两种地址的定义及区别](https://storage.googleapis.com/papyrus_images/cc0e50fe58c57a9eeed0ab5404987d98299125640b44b102bab8c33d72c21afb.png)

以太坊两种地址的定义及区别

绝大多数web3用户都是通过钱包（例如MetaMask）的方式创建公私钥对和地址，今天我们会采取纯代码的方式带大家把全流程走通。值得一提的是，当环境搭建好之后，整个过程完全不需要接触互联网，因此这种方式可以更加安全的保护你的私钥。

### 概述

![私钥到外部地址的生成过程](https://storage.googleapis.com/papyrus_images/a11856bf6463252b10399b55282a54044f7c8035dba6269f8b09b4529df12b30.png)

私钥到外部地址的生成过程

从私钥可以单向生成地址，完全不需要任何其他信息，这也是为什么我们说有了私钥就有了地址。但是反之不可行，外部账户反推压缩公钥这一步是可行的，椭圆曲线加密算法和哈希算法这两步是不可逆的，这是**数学上的不可逆**，所以最终由外部账户/地址来反推私钥是不可行的。

这也是为什么我们说地址可以公开，但是私钥千万不要公开。

### 生成私钥

私钥本质上就是1-2\*\*256之间的随机数，所以生成私钥的过程中有可靠的随机源至关重要，理论上扔256次硬币的方式也可以生成私钥，而且非常随机，非常可靠。

我们在下面生成私钥的例子里面采用的是node自带的`crypto`库，它符合业界最广泛采用的OpenSSL标准，随机源来自底层操作系统，可以放心使用。

    // 01_generateSK.js 
    const crypto = require("crypto"); 
    const SK = crypto.randomBytes(32).toString("hex"); 
    
    console.log("Private Key: ", SK); 
    // 私钥 = 75c7e7c3a022276216f7cbed396787bf25931bcf6924bffd246bc7772cbba086
    

在终端输入`node 01_generateSK.js`运行后，私钥就生成了，它的格式为32字节的十六进制字符串。

### 用私钥生成公钥

32字节的私钥通过ECDSA椭圆曲线加密算法（具体为secp256k1曲线）生成64字节的公钥，我们需要安装对应的npm库：`npm i secp256k1`

    // 02_generatePKfromSK.js 
    const { publicKeyCreate } = require("secp256k1"); 
    const SK = "75c7e7c3a022276216f7cbed396787bf25931bcf6924bffd246bc7772cbba086";
    const SKBuffer = Buffer.from(SK, "hex"); 
    const PK = Buffer.from(publicKeyCreate(SKBuffer, false)).toString("hex"); 
    
    console.log("Public Key: ", PK); 
    // 公钥 = 210e93d406a66998ba69d3c9f26fffcccc432f3152e204aef49402eddaaa6cf155ea52d17bb40fc9f3ee3b13a0d8824acf49ff7af2ca5397770fc2475b160cb6
    

公钥返回值为130位16进制字符串，去掉固定前缀04即可得到64字节/128位公钥。

### 压缩公钥得到地址

将公钥通过哈希算法Keccak-256获得32字节的压缩公钥，需要安装对应的npm库：`npm i keccak256`。

    // 03_generateAddressfromPK.js const keccak256 = require("keccak256"); 
    const PK = "210e93d406a66998ba69d3c9f26fffcccc432f3152e204aef49402eddaaa6cf155ea52d17bb40fc9f3ee3b13a0d8824acf49ff7af2ca5397770fc2475b160cb6"; 
    const PKBuffer = Buffer.from(PK, "hex"); 
    const Address = keccak256(PKBuffer) .toString("hex") .slice(64 - 40); 
    
    console.log("Ethereum Address: ", "0x" + Address); 
    // 地址 = 0xf3f3037f2a393234998dbc589a057fbe65810760
    

最后，取压缩公钥最后40位/20字节，并且加入0x前缀即可得到42位的最终地址。

### 一键生成私钥/公钥/地址

我们可以用 `ethereumjs-wallet` 库将上面三步集成到一起，一键生成公私钥对和地址，市面上主流的钱包应用都是用这个方法生成地址账户。

安装：`npm i ethereumjs-wallet`

    // 04_generateWallet.js const Wallet = require("ethereumjs-wallet"); 
    const eip55 = require("eip55"); 
    const wallet = Wallet.default.generate(); 
    
    console.log("SK: ", wallet.getPrivateKeyString()); 
    console.log("PK: ", wallet.getPublicKeyString()); 
    console.log("Address before checksum: ", wallet.getAddressString()); 
    console.log( "Address after checksum :", eip55.encode(wallet.getAddressString()) ); 
    // 私钥 = 0x3fa8b14e683907bfefdebe44f91d81225197136f59cba6e8bf5a8e3b9fa40569 
    // 公钥 = 0xc77d41e166d05a112683879095007c8572c32f4a129a0a6264a26bd3d4c746a125595468aecf833b468b93edb33d4b48ab77d1795ba4dcb32b589689ba31dddf 
    // 未经EIP-55校验的地址 = 0x73989e1bdd4bd5849932cbc24c8d4f97ef79d544 
    // 经过EIP-55校验的地址 = 0x73989e1bdd4bD5849932CBC24C8D4f97eF79D544
    

在这个例子里面，你可能注意到我们额外引入了`eip55`，未经eip55校验的地址为默认值，全部小写，经过eip55校验的地址，部分字母由小写变成了大写。我们都知道16进制的字符串对于字母的大小写是完全不敏感的，也就是说上述两个地址在EVM/Solidity看来是完全等价的，那么为什么我们要多此一举呢？

### EIP-55

\*\*[EIP-55](https://eips.ethereum.org/EIPS/eip-55) \*\*是V神在2016年提出的以太坊改善提案，它的原理如下：

*   将原始地址（字母全部为小写）取哈希
    
*   如果地址第i位是字母，那么就去查一下哈希的第4i位
    
*   第4i位如果是1，第i位改为大写，如果是0，第i位不变，仍为小写
    
*   如果我们输入地址时有误操作，哪怕只有一位的错误，那么地址的哈希值就会变化，最终导致其他位数上的字母大小写状态无法匹配
    

以太坊地址去掉0x前缀后有40位，按平均有15个字母来计算，经过15次校验后，地址有输入错误但无法被识别的概率只有0.0247%。说到这里你肯定恍然大悟，EIP-55是人为设定的规则，它可以大大的降低由于地址输入错误而造成的转账损失，简单而高效，市面上主流Dapp都已经支持该提案。

### 最后

好了，这就是生成私钥、公钥、外部地址的全流程，它完全依赖于加密算法、哈希等数学规律，其实在比特币/以太坊出现以前就已经存在并且被大规模采用了。

而且整个过程完全可以在断网的情况下操作，真正做到了私钥不触网，可以推荐给喜欢冷钱包，对安全性有极高要求的朋友们。

恭喜你，如果你坚持读到这里，那么你已经比90%的人都要了解私钥，公钥和外部地址了，下篇文章我们会介绍以太坊第二个地址类型-合约地址，到时见！

---

*Originally published on [Wei Zhong](https://paragraph.com/@wei-zhong/dlRUN9KeDpLMbMiJCAzQ)*
