MetaMask-账户生成过程

本节,来聊聊 MetaMask 钱包中,账户、密钥的生成过程,最后讨论下账户、密钥碰撞的概率。

MetaMask

私钥生成

私钥的本质是 32 位随机数。主要通过 Crypto.getRandomValues() 完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// https://github.com/MetaMask/eth-simple-keyring/blob/a1ce8d2c360e348d49e980915a4a94feb270e4e2/index.js#L16
function generateKey() {
const privateKey = randomBytes(32);
// I don't think this is possible, but this validation was here previously,
// so it has been preserved just in case.
// istanbul ignore next
if (!ethUtil.isValidPrivate(privateKey)) {
throw new Error(
'Private key does not satisfy the curve requirements (ie. it is invalid)',
);
}
return privateKey;
}

// generate random bytes.
// https://github.com/crypto-browserify/randombytes/blob/master/browser.jshttps://github.com/crypto-browserify/randombytes/blob/master/browser.js
function randomBytes (size, cb) {
// phantomjs needs to throw
if (size > MAX_UINT32) throw new RangeError('requested too many random bytes')

var bytes = Buffer.allocUnsafe(size)

if (size > 0) { // getRandomValues fails on IE if size == 0
if (size > MAX_BYTES) { // this is the max bytes crypto.getRandomValues
// can do at once see .
// https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues
for (var generated = 0; generated < size; generated += MAX_BYTES) {
// buffer.slice automatically checks if the end is past the end of
// the buffer so we don't have to here.
// generate random numbers.The Crypto.getRandomValues() method lets you get cryptographically strong random values.
crypto.getRandomValues(bytes.slice(generated, generated + MAX_BYTES))
}
} else {
crypto.getRandomValues(bytes)
}
}

if (typeof cb === 'function') {
return process.nextTick(function () {
cb(null, bytes)
})
}

return bytes
}

生成公钥

从私钥中,计算出公钥。
public_key = Point.BASE * private_key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// https://github.com/MetaMask/eth-simple-keyring/blob/a1ce8d2c360e348d49e980915a4a94feb270e4e2/index.js#L16
async addAccounts(n = 1) {
const newWallets = [];
for (let i = 0; i < n; i++) {
const privateKey = generateKey();
// get public key from private key.
const publicKey = ethUtil.privateToPublic(privateKey);
newWallets.push({ privateKey, publicKey });
}
this._wallets = this._wallets.concat(newWallets);
const hexWallets = newWallets.map(({ publicKey }) =>
ethUtil.bufferToHex(ethUtil.publicToAddress(publicKey)),
);
return hexWallets;
}

// https://github.com/ethereumjs/ethereumjs-util/blob/ebf40a0fba8b00ba9acae58405bca4415e383a0d/src/account.ts
export const privateToPublic = function(privateKey: Buffer): Buffer {
assertIsBuffer(privateKey)
// skip the type flag and use the X, Y points
return Buffer.from(publicKeyCreate(privateKey, false)).slice(1)
}


// https://github.com/ethereum/js-ethereum-cryptography/blob/master/src/secp256k1-compat.ts
export function publicKeyCreate(
privateKey: Uint8Array,
compressed = true,
out?: Output
): Uint8Array {
// private key must has 32 bytes.
assertBytes(privateKey, 32);
assertBool(compressed);
const res = secp.getPublicKey(privateKey, compressed);
return output(out, compressed ? 33 : 65, res);
}

// https://github.com/paulmillr/noble-secp256k1/blob/88c35c2b30ae6f803e57bb3c78ddebbb4f6f0eaa/index.ts#L918
export function getPublicKey(privateKey: PrivKey, isCompressed = false): PubKey {
const point = Point.fromPrivateKey(privateKey);
if (typeof privateKey === 'string') {
return point.toHex(isCompressed);
}
return point.toRawBytes(isCompressed);
}


// https://github.com/paulmillr/noble-secp256k1/blob/88c35c2b30ae6f803e57bb3c78ddebbb4f6f0eaa/index.ts#L325
// public_key = Point.BASE * private_key

// https://github.com/paulmillr/noble-secp256k1/blob/88c35c2b30ae6f803e57bb3c78ddebbb4f6f0eaa/index.ts#L386
static fromPrivateKey(privateKey: PrivKey) {
return Point.BASE.multiply(normalizePrivateKey(privateKey));
}

// transform the private key to a number(value).
function normalizePrivateKey(key: PrivKey): bigint {
let num: bigint;
if (typeof key === 'bigint') {
num = key;
} else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) {
num = BigInt(key);
} else if (typeof key === 'string') {
if (key.length !== 64) throw new Error('Expected 32 bytes of private key');
num = hexToNumber(key);
} else if (key instanceof Uint8Array) {
if (key.length !== 32) throw new Error('Expected 32 bytes of private key');
num = bytesToNumber(key);
} else {
throw new TypeError('Expected valid private key');
}
if (!isWithinCurveOrder(num)) throw new Error('Expected private key: 0 < key < n');
return num;
}

// https://github.com/paulmillr/noble-secp256k1/blob/88c35c2b30ae6f803e57bb3c78ddebbb4f6f0eaa/index.ts#L861
function isWithinCurveOrder(num: bigint): boolean {
return 0 < num && num < CURVE.n;
}

// https://github.com/paulmillr/noble-secp256k1/blob/88c35c2b30ae6f803e57bb3c78ddebbb4f6f0eaa/index.ts#L21
// CAUTION:
//this is the max private key value.
// 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
CURVE.n = POW_2_256 - BigInt('432420386565659656852420866394968145599')

椭圆曲线方程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// https://github.com/paulmillr/noble-secp256k1/blob/88c35c2b30ae6f803e57bb3c78ddebbb4f6f0eaa/index.ts#L13
const CURVE = {
// Params: a, b
a: _0n,
b: BigInt(7),
// Field over which we'll do calculations
P: POW_2_256 - _2n ** BigInt(32) - BigInt(977),
// Curve order. Specifically, it belongs to prime-order subgroup;
// but our curve is h=1, so other subgroups don't exist
n: POW_2_256 - BigInt('432420386565659656852420866394968145599'),
// Cofactor
h: _1n,
// Base point (x, y) aka generator point
Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'),
// For endomorphism, see below
beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'),
};

生成账户地址

账户地址,来自于公钥。具体来说,是一个 64 bytes 的 hash 的 后20 个 bytes。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// https://github.com/MetaMask/eth-simple-keyring/blob/a1ce8d2c360e348d49e980915a4a94feb270e4e2/index.js#L64
async getAccounts() {
return this._wallets.map(({ publicKey }) =>
ethUtil.bufferToHex(ethUtil.publicToAddress(publicKey)),
);
}

// https://github.com/ethereumjs/ethereumjs-util/blob/ebf40a0fba8b00ba9acae58405bca4415e383a0d/src/account.ts
export const pubToAddress = function(pubKey: Buffer, sanitize: boolean = false): Buffer {
assertIsBuffer(pubKey)
if (sanitize && pubKey.length !== 64) {
pubKey = Buffer.from(publicKeyConvert(pubKey, false).slice(1))
}
// public key length should 64
assert(pubKey.length === 64)
// Only take the lower 160bits of the hash
return keccak(pubKey).slice(-20)
}

// https://github.com/ethereum/js-ethereum-cryptography/blob/bce953bceb9b7bf125c276e10e69e4de3eb96eed/src/secp256k1-compat.ts
// guarantee the length should be 64,if public key length can not satisfied.
export function publicKeyConvert(
publicKey: Uint8Array,
compressed = true, // binary or hex.
out?: Output
): Uint8Array {
// So, public key length should in [32,64] .
assertBytes(publicKey, 33, 65);
assertBool(compressed);
const res = secp.Point.fromHex(publicKey).toRawBytes(compressed);
return output(out, compressed ? 33 : 65, res);
}

空间

接下来,有几个问题值得我们思考:

  • 私钥会不会重复;
  • 私钥和公钥是一一对应的吗?
  • 账户会不会重复。

根据上面的代码,其实我们容易发现:

  • 私钥的可使用空间为 2^256 ,在推出公钥的时候,我们发现私钥需要小于 CURVE.n,因此,私钥的实际使用空间大约为 2^128(16个字节);
  • 公钥的实际长度是 32 位(压缩后),因此其可使用空间为 2^256 ,但是实际范围,不会高于私钥,它是由私钥推导出来的;
  • 地址的可用空间为 2^160(将公钥 hash 后的后20个字节),但是实际使用范围,不会高于公钥,它是由公钥推导出来的。

那么,私钥会不会重复?依照现有的计算能力,基本上是不可能的。
账户会不会重复?账户的可用空间比私钥的可用空间大,但是实际使用空间应不大于私钥空间,然而,账户相同仍然是一个非常小的概率,小到不可能(这一点应该有数学证明)。
私钥和公钥的对应关系,目前从找到的资料表明,根据数学理论,应是一一对应的,这一点暂时不太理解这个数学过程。

关于黑洞账户(Black Hole)

黑洞(英语:black hole)是时空展现出极端强大的引力,以致于所有粒子、甚至光这样的电磁辐射都不能逃逸的区域。 —维基百科

因此,所谓 eth 上的黑洞账户,即,只是接收输入,而从不输出。输出是受私钥控制的,也因此,没有私钥,或者私钥被销毁,这种账户就是一种黑洞账户。比如我们常用的黑洞账户 0x0000000000000000000000000000000000000000

从上一节可知,账户(或者说账户地址),实际上是公钥经过 hash 过后,取后 20 个字节的结果。而公钥的实际使用空间,大概为 2^128
关于是否存在一个可以控制黑洞账户的私钥,我没有找到一个理论依据。但是,我认为我们无法确认这么大的空间中,公钥经过 hash 后,不会产生一个最后 20 位全部为 0 的地址,hash 过程是单向的,就现有技术而言,我们也无法实现全域的穷举,去证明这一点。

正如我们不需要去担心世界上会出现两个相同的私钥(私钥是随机生成的),我们也不必担心这个世界上有谁拥有者这个黑洞地址的私钥,因为其概率低到不可能(就现在的技术能力而言)。

参考链接

Is each Ethereum address shared by (theoretically) 2 ** 96 private keys?

Two same identical private keys: Is it possible at all?

比特币私钥总数有2的256次方 , 这个数有多大?

椭圆曲线加密与哈希函数是什么?非对称加密是什么?比特币中的数学原理

ECC椭圆曲线的特性

Named Curves - Example

Verifying G on y² = x³ + 7 mod p

Account uniqueness guaranteed?

© 2024 YueGS