一、外部账户
以太坊账户负责存储用户的以太坊余额。
对大多数普通用户来说,以太坊账户和银行账户非常类似,通常只需要一个账户即可。
确切地说,以太坊账户分为外部账户和合约账户两类:
外部账户:即普通用户用私钥控制的账户;
合约账户:一种拥有合约代码的账户,它不属于任何人,也没有私钥与之对应。
本节我们仅讨论普通用户使用的外部账户。
和比特币类似,一个以太坊账户就是一个公钥哈希后得到的地址,它是由一个私钥推导出对应的公钥,然后再计算出地址。其中,私钥与公钥算法与比特币完全相同,均为 secp256k1 椭圆曲线,但和比特币不同的是,以太坊采用非压缩公钥,然后直接对公钥做 keccak256 哈希,得到 32 字节的哈希值,取后20 字节加上 0x 前缀即为地址:

用代码实现如下:
const
randomBytes = require('randombytes'),
ethUtil = require('ethereumjs-util');
// 生成256bit的随机数作为私钥:
let priKey = randomBytes(32).toString('hex');
// 计算公钥(非压缩格式):
let pubKey = ethUtil.privateToPublic(new Buffer(priKey, 'hex')).toString('hex');
// 计算地址:
let addr = ethUtil.pubToAddress(new Buffer(pubKey, 'hex')).toString('hex');
console.log('Private key: 0x' + priKey);
console.log('Public key: 0x' + pubKey);
console.log('Address: 0x' + addr);
某一个执行结果
Private key: 0x57131c3114f3efa1fff70fa9d76ccd7ae7026fbb20c7788dbfe9221169282057
Public key: 0x2a08f01769a17214f759196e3fcd28afa636101bafd8de3ecd2ae92c49924442b6359a06d40246c492342688e54e0e4bf0f988ee34bea70792d0937dca9cb18c
Address: 0x4f831eaa7052c661d054995d4076c422cf9f3eb1
和比特币采用 Base58 或 Bech32 编码不同,以太坊对私钥和地址均采用十六进制编码,因此它没有任何校验,如果某一位写错了,仍然是一个有效的私钥或地址。
keccak256 哈希算法在以太坊中也被称为 SHA3 算法,但是要注意,keccak 算法原本是 SHA3 的候选算法,然而在 SHA3 最后的标准化时,对 keccak 做了改进,因此,标准的 SHA3 算法和 keccak 是不同的,只是以太坊在开发时就选择了尚未成为 SHA3 标准的 keccak 算法。后续我们在讨论以太坊的哈希算法时,一律使用 keccak256 而不是 SHA3-256。
二、带校验的地址
因为以太坊的地址就是原始哈希的后 20 字节,并且以十六进制表示,这种方法简单粗暴,但没有校验。地址中任何数字出错都仍是一个有效地址。为了防止抄错,以太坊通过 EIP-55 实现了一个带校验的地址格式,它的实现非常简单,即对地址做一个 keccak256 哈希,然后按位对齐,将哈希值 ≥8 的字母变成大写:
original addr = 0x29717bf51d8afca452459936d395668a576bce66
keccak hash = e72ecce2eb2ed0ffab5e05f043ee68fab3df759d...
checksum addr = 0x29717BF51D8AFcA452459936d395668A576Bce66
因此,以太坊地址就是依靠部分变成大写的字母进行校验,它的好处是带校验的地址和不带校验的地址对钱包软件都是一样的格式,缺点是有很小的概率无法校验全部小写的地址。
const ethUtil = require('ethereumjs-util');
console.log('is valid address: ' + ethUtil.isValidAddress('0x29717bf51d8afca452459936d395668a576bce66')); // true
console.log('is valid checksum address: ' + ethUtil.isValidChecksumAddress('0x29717BF51D8AFcA452459936d395668A576Bce66')); // true
console.log('is valid checksum address: ' + ethUtil.isValidChecksumAddress('0x29717BF51D8AFcA452459936d395668A576BcE66')); // false
执行结果
is valid address: true
is valid checksum address: true
is valid checksum address: false
下面这个程序可以自动搜索指定前缀地址的私钥:
const randomBytes = require('randombytes');
const ethUtil = require('ethereumjs-util');
// 搜索指定前缀为'0xAA...'的地址:
let prefix = '0xAA';
if (/^0x[a-fA-F0-9]{1,2}$/.test(prefix)) {
let
max = parseInt(Math.pow(32, prefix.length-2)),
qPrefix = prefix.toLowerCase().substring(2),
prettyPriKey = null,
prettyAddress = null,
priKey, pubKey, addr, cAddr, i;
for (i=0; i priKey = randomBytes(32).toString('hex');
pubKey = ethUtil.privateToPublic(new Buffer(priKey, 'hex')).toString('hex');
addr = ethUtil.pubToAddress(new Buffer(pubKey, 'hex')).toString('hex');
if (addr.startsWith(qPrefix)) {
cAddr = ethUtil.toChecksumAddress('0x' + addr);
if(cAddr.startsWith(prefix)) {
prettyPriKey = priKey;
prettyAddress = cAddr;
break;
}
}
}
if (prettyPriKey === null) {
console.error('Not found.');
} else {
console.log('Private key: 0x' + prettyPriKey);
console.log('Address: ' + prettyAddress);
}
} else {
console.error('Invalid prefix.');
}
某一个执行结果
Private key: 0x15f99cf1f14b6d81f3ac2e6f369207b35a9f422267e9f9610c7b216c769da52a
Address: 0xAAC102E93f42877F49616D2bDe1341DB737eaA62
原理是不断生成私钥和对应的地址,直到生成的地址前缀满足指定字符串。一个可能的输出如下:
Private key: 0x556ba88aea1249a1035bdd3ec2d97f8c60404e26ecfcd7757e0906885d40322e
Address: 0xAA6f2ea881B96F87152e029f69Bd443834D99f97
注意
如果你想用这种方式生成地址,请确保电脑无恶意软件,
并在断网环境下用 Node 执行而不是在浏览器中执行。
三、HD 钱包
因为以太坊和比特币的非对称加密算法是完全相同的,不同的仅仅是公钥和地址的表示格式,因此,比特币的 HD 钱包体系也完全适用于以太坊。用户通过一套助记词,既可以管理比特币钱包,也可以管理以太坊钱包。
以太坊钱包的派生路径是 m/44'/60'/0'/0/0,用代码实现如下:
const
bitcoin = require('bitcoinjs-lib'),
bip39 = require('bip39'),
ethUtil = require('ethereumjs-util');
// 助记词和口令:
let
words = 'bleak version runway tell hour unfold donkey defy digital abuse glide please omit much cement sea sweet tenant demise taste emerge inject cause link',
password = 'bitcoin';
// 计算seed:
let seedHex = bip39.mnemonicToSeedHex(words, password);
console.log('seed: ' + seedHex); // b59a8078...c9ebfaaa
// 生成root:
let root = bitcoin.HDNode.fromSeedHex(seedHex);
console.log('xprv: ' + root.toBase58()); // xprv9s21ZrQH...uLgyr9kF
console.log('xpub: ' + root.neutered().toBase58()); // xpub661MyMwA...oy32fcRG
// 生成派生key:
let child0 = root.derivePath("m/44'/60'/0'/0/0");
let prvKey = child0.keyPair.d.toString(16);
let pubKey = ethUtil.privateToPublic(new Buffer(prvKey, 'hex')).toString('hex');
let address = '0x' + ethUtil.pubToAddress(new Buffer(pubKey, 'hex')).toString('hex');
let checksumAddr = ethUtil.toChecksumAddress(address);
console.log(" prv m/44'/60'/0'/0/0: 0x" + prvKey); // 0x6c03e50ae20af44b9608109fc978bdc8f081e7b0aa3b9d0295297eb20d72c1c2
console.log(" pub m/44'/60'/0'/0/0: 0x" + pubKey); // 0xff10c2376a9ff0974b28d97bc70daa42cf85826ba83e985c91269e8c975f75f7d56b9f5071911fb106e48b2dbb2b30e0558faa2fc687a813113632c87c3b051c
console.log(" addr m/44'/60'/0'/0/0: " + address); // 0x9759be9e1f8994432820739d7217d889918f2f07
console.log("check-addr m/44'/60'/0'/0/0: " + checksumAddr); // 0x9759bE9e1f8994432820739D7217D889918f2f07
执行结果
seed: b59a8078d4ac5c05b0c92b775b96a466cd136664bfe14c1d49aff3ccc94d52dfb1d59ee628426192eff5535d6058cb64317ef2992c8b124d0f72af81c9ebfaaa
xprv: xprv9s21ZrQH143K4ATirFwVJe2ZvmmBACdKcK1cWXtDdMhLFViGUDBDWJnWHLYGz5624M8zFsYKGoGt8UVcPMwu9MR5gveP98ZoRq7uLgyr9kF
xpub: xpub661MyMwAqRbcGeYBxHUVfmyJUobfZfMAyXwDJvHqBhEK8J3R1kVU476z8dvaQYCvwWPTQFzVZUuNTAWWdhNQVY2TjTiFseh8j9goy32fcRG
prv m/44'/60'/0'/0/0: 0x6c03e50ae20af44b9608109fc978bdc8f081e7b0aa3b9d0295297eb20d72c1c2
pub m/44'/60'/0'/0/0: 0xff10c2376a9ff0974b28d97bc70daa42cf85826ba83e985c91269e8c975f75f7d56b9f5071911fb106e48b2dbb2b30e0558faa2fc687a813113632c87c3b051c
addr m/44'/60'/0'/0/0: 0x9759be9e1f8994432820739d7217d889918f2f07
check-addr m/44'/60'/0'/0/0: 0x9759bE9e1f8994432820739D7217D889918f2f07
因为以太坊采用账户余额模型,通常情况下一个以太坊地址已够用。
如果要生成多个地址,可继续派生 m/44'/60'/0'/0/1、m/44'/60'/0'/0/2 等。
四、小结
以太坊的私钥和公钥采用和比特币一样的 ECDSA 算法和 secp256k1 曲线,并且可以复用比特币的 HD 钱包助记词;
以太坊的地址采用对非压缩公钥的 keccak256 哈希后 20 字节,并使用十六进制编码,可以通过大小写字母实现地址的校验。
区块链 第10.3章 以太坊账户