NBitcoin:最完整的比特币端口(第一部分:加密)






4.96/5 (55投票s)
面向.NET开发者的比特币入门
目录
- 比特币简介
- 创业者的视角
- 经济学家的视角
- 告密者的视角
- 黑暗的视角
- 直接看代码
- 入门
- 比特币地址
- 保护隐私
- 身份验证
- 幕后
- 共享所有权
- 结论
比特币简介
CodeProject的各位同仁,我很高兴发布这个系列的第一篇文章。[新:第二部分在此]
我最近将大部分 C++ 比特币源代码移植到了C#。我导入了几乎所有的单元测试。NBitcoin 大约有 70 个测试,您可以用来玩耍和学习。
这是一次很棒的学习经历,我将与您分享。但比特币的所有技术细节都太长了,无法在一篇文章中解释清楚。
我将从不同的角度开始介绍比特币,谈谈它的诞生原因,然后向您展示代码。第一部分我将只涵盖比特币的离线方面。换句话说,我将忽略所有协议细节,而是讨论高级加密部分。
创业者的视角
当我对PayPal随意阻止我的资金、官僚主义、高额费用、缺乏透明度和我们银行系统在进行货币转账时的隐私缺失感到厌倦时,比特币引起了我的注意。
作为一家初创公司,为网站集成支付提供商是一项巨大的负担和成本中心。
我尝试了很多方法,最终使用了PayPal,它现在每笔交易都要收取5%的费用,并且保留随意冻结我资金的权利。他们有律师,所以他们永远是对的。
为什么这么痛苦?为什么我不能轻松地在没有中介的情况下转账?只有我和客户,没有中间人。比特币使这一切成为可能。
这是我作为创业者的看法。
经济学家的视角
对于**凯恩斯主义**经济学家来说,比特币是一种央行(美联储或欧洲央行)无法控制其供应的货币,他们的“愚蠢大脑”会被激活,并让他们想起大萧条的恐惧。
那时,将美元转化为法定货币(不受黄金支撑,但受央行控制)被视为摆脱危机的最明智的决定。
从那天起,美联储(银行的银行,最后贷款人)发生了变化。在大萧条之前,美联储是银行的黄金储备。
之后,美联储变成了一个巨大的印钞机。
废除金本位似乎是个好主意,直到**米尔顿·弗里德曼**恰如其分地解释了大萧条的成因,并促使当时的美联储理事本·伯南克在2002年承认,美联储是大萧条的罪魁祸首。换句话说,问题不在于美元是否由黄金支撑,而在于美联储的成立导致银行冒险,因为它们相信自己会得到救助。这场由美联储造成的大萧条,讽刺的是,反而让美联储获得了更大的权力,因为它摆脱了黄金的束缚。(长篇故事在此)
米尔顿·弗里德曼认为,美联储“银行的银行”的成立是一个错误,这导致大银行冒险,因为它们知道无论如何都会得到救助。
然而,我们的学校并不教授这段历史。
经济学家的视角很重要。比特币是由**中本聪**创造的,正是因为他深信米尔顿·弗里德曼的理论,比特币诞生于2009年1月3日左右,第一个区块,即创世区块,用嵌入其中的消息“2009年1月3日,财政部长濒临第二次银行纾困”证明了这一点。
开发者的视角
比特币是神的恩赐,无论你心中信奉的是哪个神。它就像一个完整的货币体系,开源供所有人查看和学习。如果您想给我一些比特币,愿中本聪保佑您。这是我的地址:15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe。
是的,这就是您需要知道的发送给我的钱。作为开发者,我们不再需要做任何事情就能收到报酬。
问题是,我敢肯定你们很多人都不真正想看 C++ 代码,并在 Linux 上用一些奇怪的命令行魔法来编译。
您喜欢 Visual Studio 吗?您喜欢 C# 吗?您崇拜Anders吗?那么您来对地方了。
我如何获得我的比特币地址?我如何知道您是否给我发了钱?我如何付款给您?这将是本文的红线。(多亏了NBitcoin,这一切只需要4行代码)
对于更复杂的场景,例如自动月度支付或自动支付和发货处理,所有这些都是可能的,但将是下一篇文章(或两篇)的主题。(剧透:这仅仅是JSON调用的问题)
告密者的视角
您可以喜欢或鄙视维基解密或斯诺登所做的事情,但您不能忽视它们,正如苹果公司的一则长期广告所说。
但请想象一下,如果您是一名告密者。
您住在瑞典,揭露了一些腐败官员和政府官员的非法活动。现在,作为报复,这些官员将在没有任何审判的情况下,阻止您所有的支付方式。
没有PayPal,没有电汇,没有信用卡,没有Visa,什么都没有,他们会让你因你的“罪行”而饿死。没有任何审判。
但是,无论他们做什么,您都可以依靠比特币。比特币不会被任何实体所颠覆,甚至不会被其创造者所颠覆。
黑暗的视角
现在,我们承认,比特币并非只用于正义的事业。
但我会说,目前罪犯可以通过信件,或者简单地通过一些由黑手党控制的银行将钱汇往国外。
普通纸币和比特币一样,都可以用于洗钱。
在媒体上,当一篇文章写道:“纽约洗钱”时,没有人会关注。
如果一篇文章写道:“纽约使用比特币洗钱”,那肯定会引起关注。
比特币本身并不黑暗,也不由黑手党控制,它是这个星球上最开放、最民主的货币形式,您可以证明这一点,它是开源的。但我们必须面对现实:就像我们钟爱的美元一样,罪犯也会使用它。(我稍后会谈论比特币的民主方面,社区在“投票”比特币功能方面有有趣的方式)
然而,比特币比通过暗行银行进行的交易更可追溯。(参见:汇丰银行)
一家比特币银行已经倒闭,它叫MtGox,它的客户损失了钱。
Mt Gox声称是因为被黑客攻击,我认为是因为银行挤兑。没有人能证明Mt Gox是对的,或者说我是对的。无论如何,如果您处理比特币,请始终记住以下名言,并将其传播给所有人。
如果您不拥有私钥,您就不拥有比特币。
Mt Gox是一家银行,他们为您保管私钥,但没有交给您,并声称安全地保管您的资金。但如果您没有私钥,您就无法证明他们确实保管了您的资金,也不能在比特币网络上直接花费它们。
再说一遍。
如果您不拥有私钥,您就不拥有比特币。
纸上谈兵,不如看代码。
入门
首先,代码在GitHub上。
其次,二进制文件在Nuget上。
因此,要开始使用,请创建一个新的控制台项目,然后在**NBitcoin** Nuget包上添加引用。
至于依赖项,**BouncyCastle** 用于加密部分,比特币底层使用ECDSA非对称密钥,我不想自己实现。在 C++ 代码中,它使用的是 OpenSSL。
**Mono.NAT**和**SQLite**仅在您打算创建自己的比特币节点(下一篇文章)时使用。Mono.NAT将使用UPNP打开网关上的比特币端口以运行您的节点。
SQLite是您的节点服务器用于存储交易、区块和对等节点信息的嵌入式数据库。它同时支持x86和x64。如果您不打算与比特币网络通信,可以删除这些。
比特币地址
如我所述,如果您想给我一些比特币,请发送到 **15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe**。
但是,作为开发者,这个地址代表什么?正式规范在此,但让我们用NBitcoin来探索它。
这个字符串是一个base58编码的字节数组。
让我们看一下这个地址的十六进制表示。
static void Main(string[] args)
{
byte[] byteArray = Encoders.Base58.DecodeData("15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe");
string hex = Encoders.Hex.EncodeData(byteArray);
Console.WriteLine(hex);
}
这给了我们:
00356facdac5f5bcae995d13e667bb5864fd1e7d59fb69d021
00:标识比特币数据结构类型的**前缀**,在这种情况下,它是公钥哈希(稍后会介绍)
356facdac5f5bcae995d13e667bb5864fd1e7d59:实际的**公钥哈希**(20字节)
fb69d021:前面所有数据的**校验和**(用于检测输入错误)
在比特币中,一个地址属于一个网络,有两个网络:**主网络**和**测试网络**。您可以在测试网络上免费获取比特币进行测试。
然而,每个网络都有不同的前缀来标识公钥哈希。
在主网络上,**00** 是公钥哈希的前缀。但在测试网络上,它是**6f**。
这样做是为了防止您将资金发送到属于另一个网络上的地址。
您可以使用**BitcoinAddress**类型以多种方式执行转换。
static void Main(string[] args)
{
BitcoinAddress address = (BitcoinAddress)Network.CreateFromBase58Data("15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe");
//BitcoinAddress address = Network.Main.CreateBitcoinAddress("15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe");
//BitcoinAddress address = new BitcoinAddress("15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe", Network.Main);
Console.WriteLine(address.ID);
//Print 356facdac5f5bcae995d13e667bb5864fd1e7d59
}
那么,这个“**公钥哈希**”是从哪里来的?
当有人在比特币上给您汇款时,他们会将一个**交易**广播到网络。
所有交易都包含一个或多个**TxIn**(交易输入)和一个或多个**TxOut**(交易输出)。
任何包含您**公钥哈希**的**TxOut**都是您可以花费的。
当轮到您付款给某人时,您会向网络发送一个新的**交易**,但这次您将包含一个**TxIn**,其中包含您要花费的**TxOut**的引用。(我们将这种引用称为**OutPoint**)
然而,您将使用与您要花费的**TxOut**中的**公钥哈希**关联的**私钥**对交易进行签名。从而向网络证明您的所有权。
这是创建新密钥对的过程。
static void Main(string[] args)
{
Key privateKey = new Key(); //Create private key
BitcoinAddress address = privateKey.PubKey.GetAddress(Network.Main); //Get the public key, and derive the address on the Main network
Console.WriteLine(address); //printed in my case 15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe
}
显然,如果您想存储私钥,
Key privateKey = new Key(); //Create private key
BitcoinSecret secret = privateKey.GetBitcoinSecret(Network.Main);
Console.Write(secret); //Print base58 string
这是所有类的一个小图,您可以看到**BitcoinAddress**只不过是一个**公钥哈希**,并且无法推断出**PubKey**。
保护隐私
如果每笔交易都广播到网络,这意味着任何人都可以跟踪我收到了多少钱,这引起了一些隐私问题。这是真的,您可以看到有人给我发送了 0.0026 BTC。
您可以创建任意数量的私钥。如果您想保护私钥,可以将您的收款地址分散到多个地址。(一组私钥称为**钱包**)
如果您是一家企业,需要自动化您的付款,您还有另一个选择:
为每次业务交易创建一个不同的比特币地址。
然而,这有两个问题:
- 您需要维护您的业务交易与您使用的私钥之间的映射关系;
- 如果这样的数据库被攻破或私钥被盗,您就会损失金钱。
解决方案称为分层确定性钱包。
有了它,您可以授予您的支付服务器**生成公钥的权利,而无需提供私钥**。如果支付数据库被盗或被攻破,您不会损失任何东西。
在NBitcoin中,此功能通过两个类实现:**ExtKey**和**ExtPubKey**。
ExtKey将为相应的ID生成一个**Key**;
ExtPubKey将为相应的ID生成一个**PubKey**。
ExtKey privateKey = new ExtKey();
ExtPubKey pubKey = privateKey.Neuter();
//Now, give the pubkey to your payment server
//....
//The payment server receive an order, note the server does not need the private key to generate the address
uint orderID = 1001;
BitcoinAddress address = pubKey.Derive(orderID).PubKey.GetAddress(Network.Main);
Console.WriteLine(address);
//Now on the server that have access to the private key, you get the private key from the orderID
Key key = privateKey.Derive(orderID).Key;
BitcoinSecret secret = key.GetBitcoinSecret(Network.Main);
Console.WriteLine(secret); //Print a nice secret key string
在此示例中,我们仅使用orderId来生成Key/Pubkey,但实际上您可以使用更多数据,这就是为什么我们称之为**分层钱包**。
例如
pubKey.Derive(departmentID).Derive(orderID).PubKey
这样,您就可以为每个部门生成一个专用的私钥。(如果您拥有主**ExtKey**,还可以花费他们的资金)
使用比特币密钥对进行身份验证
亲爱的读者,我如何向您证明,我确实是**15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe**这个地址的私钥所有者?
这是证明:
消息:“我是Nicolas Dorier,这个地址15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe的所有者。”
签名:“IGkC4NKGXOXJ6CNT8T6Dx0egqaiSb8rAlBdsmanStOhbVfILmY+3p88Z/Fhb/jSkUhHFhsbcxFZydoPrh/2LNY0=”
我是如何生成这个签名的?
BitcoinSecret secret = new BitcoinSecret("base58 private key", Network.Main);
string signature = secret.Key.SignMessage("I am Nicolas Dorier, owner of this address 15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe");
Console.WriteLine(signature);
您这边如何验证?
string message = "I am Nicolas Dorier, owner of this address 15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe";
string signature = "IGkC4NKGXOXJ6CNT8T6Dx0egqaiSb8rAlBdsmanStOhbVfILmY+3p88Z/Fhb/jSkUhHFhsbcxFZydoPrh/2LNY0=";
BitcoinAddress address = new BitcoinAddress("15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe", Network.Main);
bool verified = address.VerifyMessage(message, signature);
Console.WriteLine(verified);
幕后
为了简化解释,我做了一些简化来说明比特币地址是什么。
我说,一个**TxOut**将资金发送到一个**比特币地址**,当您想花费它时,您会在交易中添加一个**TxIn**,其中引用了**TxOut**,并用您的私钥对其进行签名。
但比特币比这更灵活。
**TxOut**不一定包含您的地址。**TxOut**包含一个脚本,我们称之为**ScriptPubKey**。这个脚本就像一个算法,说明您需要做什么才能花费这个**TxOut**。
相应地,**TxIn**有一个称为**ScriptSig**的**脚本**,它执行**ScriptPubKey**想要花费**TxOut**的操作。这些脚本是一种无循环的堆栈语言。
当**SigPubKey**与**ScriptSig**连接时,算法就会执行,其结果会被推入堆栈。
到目前为止,如果您想付钱给我,**ScriptPubKey**看起来是这样的(可读形式):
BitcoinAddress to = new BitcoinAddress("15sYbVpRh6dyWycZMwPdxJWD4xbfxReeHe", Network.Main);
PayToPubkeyHashScriptTemplate template = new PayToPubkeyHashScriptTemplate();
Script script = template.GenerateOutputScript(to);
Console.WriteLine(script);
OP_DUP OP_HASH160 **356facdac5f5bcae995d13e667bb5864fd1e7d59(公钥哈希)** OP_EQUALVERIFY OP_CHECKSIG
另一方面,如果我想使用这些资金,我的脚本将是:
BitcoinSecret secret = new BitcoinSecret("base58 secret key", Network.Main);
byte[] transactionSignature = secret.Key.Sign(transactionHash);
PayToPubkeyHashScriptTemplate template = new PayToPubkeyHashScriptTemplate();
Script scriptSig = template.GenerateInputScript(new TransactionSignature(transactionSignature, SigHash.All), secret.Key.PubKey);
Console.WriteLine(scriptSig);
生成的脚本将在堆栈中推入两个值:交易的签名和公钥。
<交易签名> <公钥>
如果我们连接这两个脚本,我们将得到:
<交易签名> <公钥> OP_DUP OP_HASH160 <公钥哈希> OP_EQUALVERIFY OP_CHECKSIG
这意味着:
推入<交易签名>
推入<公钥(1)>
再次推入<公钥(2)>
对<公钥(2)>进行哈希处理,弹出<公钥(2)>,推入<公钥哈希(1)>
推入<公钥哈希(2)>
验证<公钥哈希(1)>是否等于<公钥哈希(2)>,弹出<公钥哈希(1)>,弹出<公钥哈希(2)>
使用推入的<公钥(1)>和<交易签名>检查签名,弹出<交易签名>,弹出<公钥(1)>,如果验证成功则推入true,否则推入false。
共享所有权
为什么脚本系统如此重要?
因为它允许新的所有权方式,有一个特殊的脚本称为“M of N钱包”,当有人将资金发送到M of N钱包时,这意味着支出者需要提供M个可能地址中的N个签名。
这样,您就可以强制要求3人中的2人同意支出,以强制执行共识。
如果您想与某人共享资金,可以使用2人中的1人。
这与共享私钥的区别在于不可否认性。当您使用“1 of 2钱包”时,您拥有支出者身份的证明。
这样的脚本可以这样生成:
PayToMultiSigScriptTemplate template = new PayToMultiSigScriptTemplate();
Script scriptPubKey = template.GenerateOutputScript(1, new PubKey[] { nicoPubKey, bobPubKey });
01 <Nico的公钥哈希> <Bob的公钥哈希> 02 OP_CHECKMULTISIG
还有其他标准的脚本,我肯定会在下一篇文章中讨论。这里是它们的类图。
结论
最后,这篇文章比我想象的要长……下次我将更深入地探讨比特币协议的内部机制,并讨论其他标准的脚本。请告诉我您是否喜欢这篇文章。
[新:第二部分在此]