比特币钱包
通常,比特币用户依赖名为 钱包 的客户端来创建交易并与点对点网络进行交互。即使比特币核心本身也是一个钱包,除了是官方的挖矿软件。其他知名的钱包有 Electrum、Hive 等。在这里,我将尝试描述钱包的组件。
这些是钱包在内部维护的典型数据结构。其中大部分完成了钱包的核心业务,即构建交易并将其广播到比特币网络。像 找零地址 或加密这样的功能是方便的特性,但对于一个可工作的实现来说并非必需的。
在关于比特币网络的最后一段中,你学习了如何创建一个基本的钱包。给定一个 ECDSA 密钥对,一个基本的比特币钱包由以下组成:
实际的钱包实际上会创建许多密钥对,但出于简单起见,我们将只使用一个。在分层上下文中,密钥对是我们数据模型的基础,并将“持有”我们的代币。
区块链组件决定了一个钱包是 轻量级 还是 重量级。像 Bitcoin Core 这样的重量级钱包由完整的区块链支持,而像 Electrum 和 Hive 这样的轻量级钱包只需要部分或根本不需要区块链,因此适用于网络连接缓慢或设备性能有限(如智能手机)的情况。
“重量级”真的很重。在撰写本文时(2015 年),比特币核心钱包需要大约 40GB 的磁盘空间来本地分配完整的区块链,其中包括自比特币诞生以来的所有广播比特币交易。而且还在增长。方便起见,一个轻客户端在一个正常用户不关心比特币历史上的每笔交易的假设下要快得多。相反,它只会下载 相关的 交易,即用户作为发送方或接收方出现的交易。
再次回到关注密钥对。如果我们的钱包只处理标准的 P2PKH 交易 – 大多数钱包都是如此 – 我们可以安全地假设:
让我们看看为什么。考虑典型的 P2PKH 输出脚本:
OP_DUP
OP_HASH160
[hash160(公钥)]
OP_EQUALVERIFY
OP_CHECKSIG
典型的 P2PKH 输入脚本:
[signature]
[public_key]
以及伪代码中的相关性扫描:
outpoint = struct { txid, index };
relevant_txs = {}; /* txid -> tx */
utxos = {}; /* outpoint */
balance = 0;
for (tx in blockchain.txs) {
/* 1 */
for (txout in tx.outputs) {
if (!is_p2pkh_output(txout.script)) {
continue;
}
if (txout.script contains hash160(keypair.public_key)) {
relevant_txs.add(tx);
outpoint = outpoint(tx.id, txout.index);
utxos.add(outpoint);
balance += txout.value;
}
}
/* 2 */
for (txin in tx.inputs) {
if (!is_p2pkh_input(txin.script)) {
continue;
}
if (txin.script contains keypair.public_key) {
relevant_txs.add(tx);
outpoint = txin.outpoint;
previous_tx = relevant_txs[outpoint.txid];
prev_txout = previous_tx.outputs[outpoint.index];
utxos.remove(outpoint);
balance -= prev_txout.value;
}
}
}
(1) 如果任何 P2PKH 交易输出包含我们公钥的 hash160 – 我们的比特币地址 – 那么该交易对我们的钱包是相关的。这样的输出将更多的币与我们的钱包密钥对关联,并贡献于钱包的 output value 。在花费之前,该输出是钱包 UTXO 集合的一个元素,因此增加了余额。
(2) 如果任何 P2PKH 交易输入包含我们的公钥,那么该交易对我们的钱包是相关的。这样的交易输入花费了与我们的钱包密钥对关联的比特币,具体来说,它花费了输入 outpoint 引用的输出。由于输出总是完全花费的,因此在花费后,余额会减少。
顺便提一下,你可能会注意到相关交易形成了钱包的 历史。
因此,给定一个密钥对,这两个标准显著减少了在区块链中搜索相关交易的时间,无论是本地(重量级钱包)还是远程(轻量级钱包)。然而,要构建新交易,我们必须跟踪 UTXO 集合,这似乎是扫描过程的一个额外结果。所有交易输出最初都会被添加到 UTXOs 中,但如果在另一笔交易中作为输入 outpoints 被重复使用,它们将被删除。最终的集合为我们提供了用于构建新交易的可用 outpoints。
我们还可以从 UTXOs 计算钱包余额:
balance = 0;
for (outpoint in utxos) {
unspent_tx = relevant_txs[outpoint.txid];
unspent_txout = unspent_tx.outputs[outpoint.index];
balance += unspent_txout.value;
}
从架构的角度看,一个钱包软件可以分为 3 个独立的模块:
大多数钱包本质上是一体的,其他一些是混合式的,因为它们在一个单独的模块中签署交易。TREZOR 是一个著名的混合钱包的例子,其中签名模块甚至被推送到外部设备中。
这个模块是唯一持有敏感数据的模块:私钥。它接收一个未签名的交易并返回一个已签名的交易,准备发布到比特币网络。由于签名任务仅涉及 ECDSA,因此该模块通常会方便地在硬件中实现。这种安排允许进行强大的 2 因素身份验证。
想想一次性密码(OTP)设备,那些设计成可以放在你钥匙链上的便捷密码生成器。OTP 经常用于私人银行业务,用于生成仅在短时间内有效的登录令牌。令牌是你在输入凭据后进入银行账户的第二步,特别敏感操作可以请求额外的令牌。你的大脑(凭据)和 OTP 设备(令牌)共同保护账户。如果你遗漏其中任何一个,你将无法登录。
现在看看 TREZOR。TREZOR 签名设备也是一个 2 因素身份验证方案的一部分,因为创建交易的能力取决于两个物理上分离的模块:TREZOR 本身(用于 ECDSA 密钥)和运行在桌面或移动设备上的网络/区块链软件(用于 UTXOs)。该设备接收一个未签名的交易,并在手动确认后对其进行签名。然后,签名的交易被发送回网络连接的软件,最终被广播。同样,该设备本身无法构建交易,因为它对区块链一无所知。同样,网络化软件无法签署交易,因为它无法访问私钥。
私钥和公钥密切相关,但它们可以存在于完全不同的上下文中。实际上,它们的关系是松散耦合的。这就是为什么钱包可能会选择公共地址分发模块。然而,在我们的单密钥对场景中,这样一个模块会显得有些多余,因为我们只会分发一个公共地址。
只有在了解了确定性钱包之后,公钥分发才有意义,而这不是本系列讨论的范围。此外,大多数钱包将这个模块方便地合并到了网络组件中,因为公共地址需要不断监视以获取入账交易。
网络模块位于其他两个模块的中间,也是控制模块。它负责几个有时复杂的任务:
特别是任务 1 和 2 可能会很 PITA,看看有关区块链下载的模糊协议描述。许多钱包制造商 - 比如 Electrum 和Mycelium - 选择建立自己的中心化同步网络并不奇怪。轻钱包受到区块链同步复杂性的严重影响。
任务 3 在上文关于区块链模型的段落中有描述,需要我们密钥对的公钥知识。有了公钥,我们就能监视/恢复入账和出账的 P2PKH 交易。最重要的是,相关交易历史决定了钱包的 UTXO 集,这是我们构建新交易所需的。
任务 4 肯定是最容易的,除非一个钱包足够先进,能够根据币选择启发式算法选择最佳的 UTXO。在收集了 UTXO 并组成未签名交易后,它会被传输到签名模块。未签名交易被签名后返回给网络模块,然后由网络模块宣布给比特币网络。最后,它等待交易在未来的区块中被挖掘。
就是这样。从现在开始,你应该对比特币的内部工作更加了解。当然,还有很多内容等待探索。
记住,所有的代码都在我的 GitHub 存储库上。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!