4.5 高级密钥和地址

在以下部分中,我们将看到密钥和地址的高级形式,诸如加密私钥、脚本和多重签名地址,靓号地址,和纸钱包。

4.5.1 P2SH (Pay-to-Script Hash)支付脚本哈希和多重签名地址

正如我们所知,传统的比特币地址从数字1开头,来源于公钥,而公钥来源于私钥。虽然任何人都可以将比特币发送到 一个1开头的地址,但比特币只有提供相应的私钥签名和公钥哈希值后才能花费。

以数字3开头的比特币地址是P2SH地址,有时被错误的称谓多重签名或多重签名地址。它们指定比特币交易中受益人为脚本哈希,而不是公钥的所有者。这个特性在2012年1月由BIP-16(参见附录appdxbitcoinimpproposals)引进,正是因为BIP-16提供了向地址本身添加功能的机会而被广泛地采纳。不同于P2PKH(pay-to-public-key-hash,支付公钥哈希),交易发送资金到传统1开头的比特币地址,资金被发送到3开头的地址时,要求的不仅仅是公钥的哈希值和私钥签名来证明所有权。这些要求是在创建地址时在脚本中指定的,对这个地址的所有输入都将使用相同的要求。

P2SH地址从交易脚本中创建,后者定义谁能花费这个交易输出(参见【7.3 P2SH(Pay-to-Script-Hash】)。对P2SH地址编码使用的是与创建比特币地址同样的双重哈希函数,只是应用在脚本而不是公钥:

 script hash = RIPEMD160(SHA256(script))

得到的”脚本哈希”使用Base58Check编码,版本前缀为5,最后生成3开头的编码地址。一个P2SH地址例子是 3F6i6kwkevjR7AsAd4te2YB2zZyASEm1HM。可以使用比特币浏览器命令script encode、sha256、ripemd160和base58check encode(参见附录appdx_bx])导出,举例如下:

$ echo \
'DUP HASH160 [89abcdefabbaabbaabbaabbaabbaabbaabbaabba] EQUALVERIFY CHECKSIG' > script
$ bx script-encode < script | bx sha256 | bx ripemd160 \
| bx base58check-encode --version 5
3F6i6kwkevjR7AsAd4te2YB2zZyASEm1HM

提示:
P2SH不一定就是多重签名的交易。虽然P2SH地址通常都是代表多重签名,但也可能表示编码其他类型交易的脚本。

4.5.1.1 多重签名地址和P2SH

目前,P2SH最常见的实现是多重签名地址脚本。顾名思义,底层脚本需要多个签名来证明所有权,才能花费资金。比特币多重签名功能的设计要求是总共N个密钥中需要M个签名(也称为“阈值”),被称为M-N多重签名,其中M是等于或小于N。例如,第一章中提到的咖啡店主Bob使用的多重签名地址,需要1/2签名,一个是属于他的密钥,另一个是他妻子的密钥,其中任何一方都可以签名花费这个地址锁定的输出。这类似于传统的银行中的一个“联合账户”,其中夫妻任何一方都可以单独签单消费。或就像Bob雇佣的网页设计师Gopesh, 可能需要的是一个2/3的多签名地址,可以确保至少两个合伙人签名了交易才可以继续支付。

我们将会在第五章交易中探讨如何构造P2SH(和多重签名)地址花费资金的交易。

4.5.2 靓号地址

靓号地址也是有效的比特币地址,只不过增加了可读性。例如,1LoveBPzzD72PUXLzCkYAtGFYmK5vYNR33就是有效的地址,开头的4个字符Base-58包含了字母love。靓号地址需要生成和测试几十亿个候选私钥,直到一个符合模式要求的比特币地址。虽然有一些优化过的靓号生成算法,但是这些方法必然涉及到随机选择一个私钥,生成公钥,再生成比特币地址,并检查是否与所要的靓号模式相匹配,需要重复数十亿次,直到找到一个匹配。

一旦找到一个与所需模式匹配的靓号地址,那么这个靓号地址的私钥就和其他地址一样被所有者花费比特币。靓号地址不比其他地址具有更多或更少的安全性。它们依靠和其他地址相同的椭圆曲线加密算法(ECC)和SHA。找一个靓号开头的地址的私钥并不比找其他地址容易。

在第一章中,我们介绍了Eugenia,一位菲律宾的儿童慈善机构负责人。我们假设Eugenia组织了一场比特币募捐活动,并希望使用靓号比特币地址来宣传这个募捐活动。Eugenia将会创造一个以1Kids开头的靓号地址来推广儿童慈善募捐的活动。让我们看看这个靓号地址如何被创建,以及对Eugenia慈善募捐的安全性有什么意义。

4.5.2.1 生成靓号地址

应该意识到,比特币地址不过是由Base58字母表中的符号表示的一组数字。寻找像“1kids”开头的靓号就像从1Kids11111111111111111111111111111到1Kidszzzzzzzzzzzzzzzzzzzzzzzzzzzzz这个区间找一个地址。以“1kid”开头的地址区间大约有5829个地址(大约是1.4 * 1051))。表4-6显示了这些有“1kids”前缀的地址范围。

表4-6 “1Kids”靓号的范围

From 1Kids11111111111111111111111111111
1Kids11111111111111111111111111112
1Kids11111111111111111111111111113
To 1Kidszzzzzzzzzzzzzzzzzzzzzzzzzzzzz

我们把“1Kids”这个前缀当作数字,可以看看比特币地址中这个前缀出现的频率。一台普通台式电脑, 没有任何特殊的硬件,大约每秒搜索大约10万个密钥。

表4-7 靓号(1KidsCharity)的出现的频率以及生成所需时间

Length Pattern Frequency Average search time
1 1Ki 1 in 58 keys < 1 milliseconds
2 1Ki 1 in 3,364 50 milliseconds
3 1Kid 1 in 195,000 < 2 seconds
4 1Kids 1 in 11 million 1 minute
5 1KidsC 1 in 656 million 1 hour
6 1KidsCh 1 in 38 billion 2 days
7 1KidsCha 1 in 2.2 trillion 3–4 months
8 1KidsChar 1 in 128 trillion 13–18 years
9 1KidsChari 1 in 7 quadrillion 800 years
10 1KidsCharit 1 in 400 quadrillion 46,000 years
11 1KidsCharity 1 in 23 quintillion 2.5 million years

正如你所见,Eugenia并不能很快创造以“1KidsCharity”开头的靓号地址,即使她有数千台电脑同时进行运算。每增加一个字符就会增加58倍的计算难度。超过七个字符的靓号通常需要专用的硬件才能被找到,譬如用户定制的多个GPU的台式机。通常是使用无法继续比特币挖矿的矿机,被赋予了寻找靓号地址的任务。用GPU搜索靓号的速度比通用的CPU要快很多个量级。

另一种寻找靓号地址的方法是外包给一个靓号矿池,如Vanity Pool。矿池提供一种服务,把有GPU硬件的人组织起来,通过为他人寻找靓号地址来获得比特币。Eugenia可以将搜索7位字符模式的靓号地址的工作外包出去,只需要很少的金额(本书写作时大概是0.01比特币或者5美元),几个小时内就可以得到结果,而不用自己运行CPU搜索上几个月。

生成一个靓号地址是一项通暴力破解的过程:尝试一个随机密钥,检查生成的地址是否和所需的模式相匹配,重复这个过程直到成功找到为止。例4-9是个靓号挖矿的例子,用C++程序写的来寻找靓号地址的程序。这个例子运用到了我们在“其他替代客户端、资料库、工具包”一节介绍过的libbitcoin库。

例4-9 靓号挖矿程序

vanity-miner.cpp文件中的代码如下:

#include <random>
#include <bitcoin/bitcoin.hpp>

// The string we are searching for
const std::string search = "1kid";

// Generate a random secret key. A random 32 bytes.
bc::ec_secret random_secret(std::default_random_engine& engine);
// Extract the Bitcoin address from an EC secret.
std::string bitcoin_address(const bc::ec_secret& secret);
// Case insensitive comparison with the search string.
bool match_found(const std::string& address);

int main()
{
    // random_device on Linux uses "/dev/urandom"
    // CAUTION: Depending on implementation this RNG may not be secure enough!
    // Do not use vanity keys generated by this example in production
    std::random_device random;
    std::default_random_engine engine(random());

    // Loop continuously...
    while (true)
    {
        // Generate a random secret.
        bc::ec_secret secret = random_secret(engine);
        // Get the address.
        std::string address = bitcoin_address(secret);
        // Does it match our search string? (1kid)
        if (match_found(address))
        {
            // Success!
            std::cout << "Found vanity address! " << address << std::endl;
            std::cout << "Secret: " << bc::encode_base16(secret) << std::endl;
            return 0;
        }
    }
    // Should never reach here!
    return 0;
}

bc::ec_secret random_secret(std::default_random_engine& engine)
{
    // Create new secret...
    bc::ec_secret secret;
    // Iterate through every byte setting a random value...
    for (uint8_t& byte: secret)
        byte = engine() & 255;
    // Return result.
    return secret;
}

std::string bitcoin_address(const bc::ec_secret& secret)
{
    // Convert secret to payment address
    bc::wallet::ec_private private_key(secret);
    bc::wallet::payment_address payaddr(private_key);
    // Return encoded form.
    return payaddr.encoded();
}

bool match_found(const std::string& address)
{
    auto addr_it = address.begin();
    // Loop through the search string comparing it to the lower case
    // character of the supplied address.
    for (auto it = search.begin(); it != search.end(); ++it, ++addr_it)
        if (*it != std::tolower(*addr_it))
            return false;
    // Reached end of search string, so address matches.
    return true;
}

注释:
下面的例4-10 使用std :: random_device。 根据实施情况,可能会映射到底层操作系统提供的CSRNG。 在类Unix的操作系统(如Linux)中,它来自/ dev/urandom。 这里使用的随机数字生成器只用于演示,并不适用于生产级别的比特币密钥,因为它没有足够的安全性。

示例代码需要用C++编译器链接libbitcoin库(此库需要提前安装到系统)进行编译。运行这个例子可以不带参数直接执行vanity-miner的可执行文件 (参见例4-10),它就会尝试找到以“1kid”开头的靓号地址。

例4-10 编译并运行vanity-miner程序示例

$ # Compile the code with g++
$ g++ -o vanity-miner vanity-miner.cpp $(pkg-config --cflags --libs libbitcoin)
$ # Run the example
$ ./vanity-miner
Found vanity address! 1KiDzkG4MxmovZryZRj8tK81oQRhbZ46YT
Secret: 57cc268a05f83a23ac9d930bc8565bac4e277055f4794cbd1a39e5e71c038f3f
$ # Run it again for a different result
$ ./vanity-miner
Found vanity address! 1Kidxr3wsmMzzouwXibKfwTYs5Pau8TUFn
Secret: 7f65bbbbe6d8caae74a0c6a0d2d7b5c6663d71b60337299a1a2cf34c04b2a623
# 使用 "time"来看最终需要的时长
$ time ./vanity-miner
Found vanity address! 1KidPWhKgGRQWD5PP5TAnGfDyfWp5yceXM
Secret: 2a802e7a53d8aa237cd059377b616d2bfcfa4b0140bc85fa008f2d3d4b225349

real    0m8.868s
user    0m8.828s
sys    0m0.035s

正如我们运行Unix命令所测出的运行时间所示,示例代码要花几秒钟来找出匹配“kid”三个字符模式的结果。你可以尝试在源代码中改变search pattern,看一看四个字符或者五个字符需要花多久时间!

4.5.2.2 靓号地址安全性

靓号地址其实就是一把双刃剑,既可能增加安全,也可能削弱安全措施。用于改善安全性时,一个独特的地址使竞争对手难以替换掉你的地址,欺骗你的客户付款给他们。不幸的是,任何人都能创建一个类似的随机地址,甚至另一个靓号地址,欺骗你的客户。

Eugenia可以让捐款人捐款到她的一个随机生成地址(例如:1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy)。 她也可以生成一个独特的以“1Kids”开头的靓号地址。

针对这两种情况,使用单一固定地址(而不是每笔捐款都用一个独立的动态地址)的风险之一是骗子有可能会黑进你的网站,用他自己的地址取代你的地址,从而将捐赠转移给他自己。如果你在不同的地方公布了你的捐款地址,你的用户应该在付款之前认真检查以确保这个地址跟在你的网站、邮件和传单上看到的地址是同一个。如果是随机地址 1j7mdg5rbqyuhenydx39wvwk7fslpeoxzy,普通用户可能会只检查头几个字符“1j7mdg”,就认为地址匹配。使用靓号地址生成器,那些想替换类似地址的骗子可能很快生成与前几个字符相匹配的地址,如表4-8所示。

表4-8 生成匹配某随机地址的多个靓号

Original Random Address 1J7mdg5rbQyUHENYdx39WVWK7fsLpEoXZy
Vanity (4-character match) 1J7md1QqU4LpctBetHS2ZoyLV5d6dShhEy
Vanity (5-character match) 1J7mdgYqyNd4ya3UEcq31Q7sqRMXw2XZ6n
Vanity (6-character match) 1J7mdg5WxGENmwyJP9xuGhG5KRzu99BBCX

那靓号地址还能增加安全性吗?如果Eugenia生成1Kids33q44erFfpeXrmDSz7zEqG2FesZEN的靓号地址,用户可能看到靓号单词和后面的几个字符,例如注意到1Kids33。这样就会迫使攻击者至少生成6个字母相匹配的的靓号地址(比之前多2个字符),花费比Eugenia多3364倍(582的努力。本质上,Eugenia付出的努力(或者支付给靓号池付出的)迫使攻击者不得不生成更长的靓号模式。如果Eugenia花钱请矿池生成8个字符的靓号地址,攻击者将会被迫生成10个字符,这将是个人电脑,甚至订制的昂贵靓号挖矿机或靓号池也无法生成的。对Eugenia来说可能还能承担的起,但对攻击者来说却一定是承受不起,特别如果欺诈的潜在回报不足以抵消生成靓号地址所需的费用。

4.5.3 纸钱包

纸钱包是打印在纸张上的比特币私钥。有时纸钱包为了方便起见也包括对应的比特币地址,但这并不是必要的,因为地址可以从私钥中派生。纸钱包是一个非常有效的建立备份或者线下存储比特币(即冷存储)的方式。作为备份机制,纸钱包可以提供安全性,以防在电脑硬盘损坏、失窃或意外删除的情况下造成密钥的的丢失。作为冷存储机制,如果纸钱包密钥在线下生成并且从来没在电脑中存储过,那么在防范黑客攻击,键盘记录器,或其他在线电脑威胁方面会更加安全。

纸钱包有许多不同的形状,大小,和外观设计,但基本原则就是密钥和地址打印在纸上。表4-9展现了纸钱包最基本的形式。

表4-9 比特币纸钱包的私钥和公钥的打印形式

Public address Private key (WIF)
1424C2F4bC9JidNjjTUZCbUxv6Sa1Mt62x 5J3mBbAH58CpQ3Y5RNJpUKPE62SQ5tfcvU2JpbnkeyhfsYB1Jcn

使用工具,可以很容易地生成纸钱包,譬如使用bitaddress.org网站上的”client-side JavaScript generator”。这个页面包含所有生成密钥和纸钱包所必须的代码,甚至在完全断网的情况下,也可以生成密钥和纸钱包。方法是,把HTML页面保存在本地磁盘或外部U盘。然后断开互联网,用浏览器打开文件。更推荐,使用一个纯净版操作系统启动电脑,比如光盘启动的Linux系统。在脱机情况下使用这个工具所生成的密钥,都可以通过USB线(不能用无线)在本地打印机上打印出来,这样就创造了一个纸钱包,其中的密钥只存在纸张上从未存储在任何在线系统上。将这些纸钱包放置在防火保险柜内,然后发送比特币到这个比特币地址上,就实现了一个简单但非常有效的冷存储解决方案。图4-8展示了通过bitaddress.org 生成的纸钱包。

图4-8展示了通过bitaddress.org 生成的纸钱包

图4-8展示了通过bitaddress.org 生成的纸钱包

这个简单的纸钱包系统的不足之处是那些被打印下来的密钥容易被盗窃。一个能够接近这些纸张的小偷只需偷走它或者用相机拍照,就能控制被这些密钥锁定的比特币。一个更复杂的纸钱包存储系统是使用BIP-38加密私钥。所有者使用一个记在脑中的口令保护打印在纸钱包上的这些私钥。没有口令,这些被加密过的密钥毫无用处。纸钱包仍旧优于用密码保护的钱包,因为这些密钥从没有在线过,并且必须从保险箱或者其他物理安全存储设备中取走。图4-9展示了通过bitaddress.org 生成的加密纸钱包。

图4-9展示了通过bitaddress.org 生成的加密纸钱包,密码:test

图4-9 通过bitaddress.org 生成的加密纸钱包的例子,密码:test

警告 虽然你可以多次向纸钱包存款,但最好一次性提取里面所有的资金。因为在解锁和花费资金的过程中,如果你没有一次性全部提取,钱包会生成一个找零地址。如果签名交易所用的计算机存在安全隐患,就有可能泄露私钥,丢失找零地址中的资金。一次性提走所有资金可以减少私钥泄露的风险,如果你所需的金额比较少,可以把剩余的资金一次性发送到一个新的纸钱包里。

纸钱包有许多设计形式和大小尺寸,还有许多不同的特征。还可以作为礼物送给别人,有季节性的主题,像圣诞节和新年主题。另一些设计用于存放在银行金库或带有密码保护的保险箱中,要么用不透明的刮擦标签,要么用防篡改的胶纸折叠密封。图4-10至图4-12展示了几个不同安全和备份功能的纸钱包的例子。

图4-10 通过bitcoinpaperwallet.com生成的、私钥写在折叠袋上的纸钱包 图4-10 从bitcoinpaperwallet.com生成的、私钥写在折叠袋上的纸钱包

图4-11 通过bitcoinpaperwallet.com 生成的、私钥被密封住的纸钱包

图4-11 从bitcoinpaperwallet.com 生成的、私钥被密封住的纸钱包

其他设计有密钥和地址的额外副本,类似于票根形式的可以拆卸存根,可以存储多个副本以防火灾、洪水或其他自然灾害。

图4-12 在备份“存根”上有多个私钥副本的纸钱包

图4-12 在备份“存根”上有多个密钥副本的纸钱包例子