交易

前言

我们在第一部分《了解加密货币》里说过,加密货币是“利益”转移的程序化,其核心目标是保证数字财富或价值安全、透明、快速的转移。因此,交易是加密货币系统中最重要的部分,是加密货币的核心功能,加密解密、P2P网络、区块链等一系列技术都是围绕交易展开的。

这一篇,我们就来研究亿书提供的交易类型及代码实现,集中总结交易的生命周期及实现过程,把我们在《地址》和《签名和多重签名》里故意漏掉的判断逻辑补充完整。

源码

transaction-types.js https://github.com/Ebookcoin/ebookcoin/blob/v0.1.3/helpers/transaction-types.js

transaction.js https://github.com/Ebookcoin/ebookcoin/blob/v0.1.3/logic/transaction.js

transactions.js https://github.com/Ebookcoin/ebookcoin/blob/v0.1.3/modules/transactions.js

类图

transactions-clase.png

解读

交易的本质

从经济学角度来说,交易就是一种价值交换。在《精通比特币》(见参考)一书里,作者是这样定义比特币交易的:简单地说,交易是指把比特币从一个地址转到另一个地址。更准确地说,一笔“交易”就是一个经过签名运算的,表达价值转移的数据结构。每一笔“交易”都经过比特币网络传输,由矿工节点收集并封包至区块中,永久保存在区块链某处。

交易,在汉语词典里,既可以是名词,代表交易内容的数据信息(技术上叫做数据结构),又可以是动词,代表一个操作过程。把这些重要信息汇总到一起,既让用户容易理解,又要体现加密货币特点,可以这样定义一个交易操作:

加密货币交易是指人们通过加密货币网络,把加密货币进行有效转移,
并把交易数据保存到区块链的过程。

这个定义与我们的直观感受比较接近。通常,大家喜欢把加密货币交易,比做纸质支票,支票本身就是记录一笔交易的数据结构,从签署支票到兑付完成的过程就是一个交易操作行为。一笔加密货币交易就是一个有着货币转移目的的电子支票,只有在交易被执行时才会在金融体系中体现,而且交易发起人并不一定是签署该笔交易的人。

交易可以被任何人在线上或线下创建,即便创建这笔交易的人不是这个账户的授权签字人。这一点非常好理解,假如有一张空的纸质支票,我们可以自己填写,也可以找人填写,最后只要有支付权限的领导签名,支票就能生效,就可以兑付。加密货币也是如此,无论谁创建的加密货币交易,只要被资金所有者(们)数字签名,交易就能实现。

交易只是一些经过加密处理的字节码,不含任何机密信息、私钥或密码,可被包括wifi、无线电在内的任何网络公开传播,甚至可以被处理成二维码、表情符号、短信等形式发送。只要这笔交易能进入加密货币网络,那么发送者并不需要信任用来传播该笔交易的任何一个网络节点。同时,这些节点也不需要信任发送者,不用记录发送者的任何身份信息。相反,电子商务网站的交易,不仅包含敏感信息,而且依赖加密网络连接完成信息传输。

因此,从本质上讲,加密货币交易是价值所有权的变更,价值转移仅仅是这种行为的结果。加密货币总量就是那些,从始至终都不会变化,人为丢失的是人类流通使用的私钥权限,总量仍在网络上不会丢失。记录加密货币总量的区块链就那一条,这个链条可以越来越长,越来越大,但是增加的仅仅是交易信息,即价值所有权变更信息。用个不慎确切的比喻,加密货币就像一列永不停息的火车,上下的是人次,固定的是座位,您只有在自己的人生旅途中才拥有某个座位的所有权(使用权)。

从设计原理上说,加密货币淡化了交易者帐号,简化为输入输出,所谓的账户也只是存在于客户端钱包这类具体的应用层的软件里,就像那列火车总要有火车站吧,而某一段旅程的火车票是有具体所属的,是要与现实人的帐号或身份对应的,所以火车站是要记录用户信息,要有检票、验票的过程。

亿书的原理也是如此,只不过亿书通过进一步扩展交易类型,强化了用户帐号的存在,使得更加适合处理各类资产所有权,从而为数字版权保护奠定良好架构基础。

交易生命周期

加密货币的整个系统,都是为了确保正确地生成交易、快速地传播和验证交易,并最终写入全球交易总账簿——区块链而设计。因此,从开发设计角度考虑,一笔交易必须包括下列过程:

  1. 生成一笔交易。这里是指一条包含交易双方加密货币地址、数量、时间戳和有效签名等信息,而且不含任何私密信息的合法交易数据;
  2. 广播到网络。几乎每个节点都会获得这笔交易数据。
  3. 验证交易合法性。生成交易的节点和其他节点都要验证,没有得到验证的交易,是不能进入加密货币网络的。
  4. 写入区块链。

下面,我们来详细阅读分析亿书的交易是如何实现的。

亿书交易类型

目前,亿书已经完成或正在开发的交易类型,包括14种(后续会有更多),分别是:

  1. // helpers/transaction-types.js
  2. module.exports = {
  3. SEND : 0,
  4. SIGNATURE : 1,
  5. DELEGATE : 2,
  6. VOTE : 3,
  7. USERNAME : 4,
  8. FOLLOW : 5,
  9. MULTI: 6,
  10. DAPP: 7,
  11. IN_TRANSFER: 8,
  12. OUT_TRANSFER: 9,
  13. ARTICALE : 10,
  14. EBOOK: 11
  15. BUY: 12
  16. READ: 13
  17. }

其中,

SEND是最基本的转账交易,SIGNATURE是上一篇提到的“签名”交易,DELEGATE是注册为受托人,VOTE是投票,USERNAME是注册用户别名地址,FOLLOW是添加联系人,MULTI是注册多重签名帐号,DAPP是侧链应用,IN_TRANSFER是转入Dapp资金,OUT_TRANSFER转出Dapp资金,这些是现有版本已经完成的功能。

ARTICALE是发布文章,EBOOK是发布电子书,BUY是购买(电子书或其他商品),READ是付费阅读(电子书等),这些功能会逐步添加。

这些交易,除了SEND转账交易外,其他的交易类型,我们暂且称它们为功能性交易(在比特币的圈子里,有人称为伪交易)。

交易基本流程

亿书交易类型尽管多样,但是交易的基本逻辑是一样的。整个加密货币都是交易逻辑的有效组成部分,要比传统电子商务网站复杂的多,但与交易直接相关的代码,却又非常简单清晰。从开发角度说,实现一笔交易,亿书需要这样几个步骤:

(1)生成交易数据

交易是人类行为,涉及到甲乙双方(货币发送者和接收者,我们用甲乙方来代替,下文同)和交易数额,这在很多交易,特别是版权交易方面更加重要。甲方是主动发起交易的有效用户,是亿书币的支付方,是交易的支付来源。乙方比较灵活,可以是另一个有合法地址的用户,也可以是亿书系统本身(功能性交易),是亿书币的接收方。

简单的一句话就是:谁与谁交易了多少钱。用下面转账交易部分的代码举例,请看modules/transactions.js文件里的763和800行,一笔交易必须包含如下字段:

  • 交易类型。代码里表示为 type: TransactionTypes.SEND;
  • 支付帐号。代码里指的是 sender: account;
  • 接受帐号。代码里指的是 recipientId: recipientId, 如果用的是别名地址,就是 recipientUsername: recipientUsername,如果是功能性交易,这里就不需要了;
  • 交易数量。代码里指的是 amount: body.amount。

这些数据有的要求用户输入,比如用户密钥,交易数量等,这些数据是否正确,也是非常关键的事情。这是软件程序验证逻辑的一个重要部分,不可或缺。这个很好理解,如果一个人胡乱填写密钥和接受地址,也能把币发送出去,那就笑话了。但具体校验过程较为繁琐,这里主要涉及到:发起交易的用户是否存在、密钥是否正确、是否多重签名帐号、是否有支付密码,以及接受方用户地址是否合法等,都要逐个检验。

详情看这里的流程图:

addTransaction-activity.png

(2)给合法交易签名

基本信息正确之后,一笔合法交易,还要使用甲乙方的公钥签名,确保交易所属。同时,还要准确记录它的交易时间戳,方便追溯。还要生成交易ID,每个交易ID都包含了丰富的加密信息,需要复杂的生成过程,绝不像传统的网站系统,让数据库自动生成索引就可以充当ID了。

详情看这里的流程图:

signTransaction-activity.png

(3)验证交易合法性

通常,一笔交易经过6-10个区块之后,这笔交易被认为是无法更改的,即已确认,因为这时候拒绝、变更的难度已经非常大,理论上已经不可能。这里的交易合法性,除了基本信息正确之外,主要是指保证交易是未确认的交易,也不是用户重复提交的交易,即双花交易。双花交易是加密货币特有的现象,通俗的说,就是用户在交易确认之前(有一段时间,比特币时间更长),又一次提交了相同交易信息,导致一笔钱花两次,这种情况是必须要避免的。

每笔交易在广播到网络之前必须验证合法性,不合法的交易没有机会广播到网络。节点收到新的交易信息时,要重新验证。如此一来,任何对网络的攻击,都只会影响一个节点,安全性大大提高。

验证合法的交易就可以直接加入区块链了,因此从上面的第一步到现在,亿书都是在一个节点上完成的。这也为下面的广播处理打下基础,一旦交易被广播到网络,在其他节点,这里的验证和处理过程就会重复执行一次。

验证的过程,看这里的流程图:

verifyTransaction-activity.png

(4)广播到点对点网络

没有中心服务器,必须借助点对点网络,把交易数据写入分布式公共账本——区块链,保证交易数据永远无法篡改,而且可以轻松查询追溯。这在中心化的服务器上,为了应对个别交易摩擦,保证交易记录可追溯,要采取更多的技术手段,记录更多的数据字段,意味着要保持大量数据冗余,付出更多资金成本。

因为交易数据不含私密信息,对网络没有苛刻要求,因此加密货币的网络可以覆盖很广,对网络的编程也变得灵活很多。理论上,只要能保证联通的便捷和快速,具体设计中不需要考虑更多复杂的因素。当然,就亿书这款产品而言,独有的用户协作和分享功能,对网络编程的性能有自身的要求,就另当别论,这方面将在下一个版本中体现出来。

这里,仅仅是加密货币基础网络功能,交易广播到网络的流程如下:

broadcastTransaction-activity.png

转账交易分析

前面几篇,我们接触到几种交易类型,比如:注册别名地址和多重签名地址,不过并没有研究具体的交易过程,下面通过分析转账交易来学习整个交易、验证的过程。

代码实现在modules/transactions.js文件里,主要Api如下:

  1. // 148行
  2. router.map(shared, {
  3. "get /": "getTransactions",
  4. "get /get": "getTransaction",
  5. "get /unconfirmed/get": "getUnconfirmedTransaction",
  6. "get /unconfirmed": "getUnconfirmedTransactions",
  7. "put /": "addTransactions"
  8. });
  9. // 160行
  10. library.network.app.use('/api/transactions', router);

解析一下,就是:

  1. get /api/transactions/ -> shared.getTransactions
  2. get /api/transactions/get -> shared.getTransaction
  3. get /api/transactions/unconfirmed/get -> shared.getUnconfirmedTransaction
  4. get /api/transactions/unconfirmed -> shared.getUnconfirmedTransactions
  5. put /api/transactions/ -> shared.addTransactions

我们仍然把读取数据的Api放一放,因为他们很简单,重点掌握写数据的操作,put /api/transactions/,对应方法shared.addTransactions,代码如下:

  1. // 652行
  2. shared.addTransactions = function (req, cb) {
  3. var body = req.body;
  4. library.scheme.validate(body, {
  5. type: "object",
  6. properties: {
  7. secret: {
  8. type: "string",
  9. minLength: 1,
  10. maxLength: 100
  11. },
  12. amount: {
  13. type: "integer",
  14. minimum: 1,
  15. maximum: constants.totalAmount
  16. },
  17. recipientId: {
  18. type: "string",
  19. minLength: 1
  20. },
  21. publicKey: {
  22. type: "string",
  23. format: "publicKey"
  24. },
  25. secondSecret: {
  26. type: "string",
  27. minLength: 1,
  28. maxLength: 100
  29. },
  30. multisigAccountPublicKey: {
  31. type: "string",
  32. format: "publicKey"
  33. }
  34. },
  35. //
  36. required: ["secret", "amount", "recipientId"]
  37. }, function (err) {
  38. // 验证数据格式
  39. if (err) {
  40. return cb(err[0].message);
  41. }
  42. // 验证密码信息
  43. var hash = crypto.createHash('sha256').update(body.secret, 'utf8').digest();
  44. var keypair = ed.MakeKeypair(hash);
  45. if (body.publicKey) {
  46. if (keypair.publicKey.toString('hex') != body.publicKey) {
  47. return cb("Invalid passphrase");
  48. }
  49. }
  50. var query = {};
  51. // 乙方(接收方)地址转换,保证可以用户名转账
  52. var isAddress = /^[0-9]+[L|l]$/g;
  53. if (isAddress.test(body.recipientId)) {
  54. query.address = body.recipientId;
  55. } else {
  56. query.username = body.recipientId;
  57. }
  58. library.balancesSequence.add(function (cb) {
  59. // 验证乙方用户合法性
  60. modules.accounts.getAccount(query, function (err, recipient) {
  61. if (err) {
  62. return cb(err.toString());
  63. }
  64. if (!recipient && query.username) {
  65. return cb("Recipient not found");
  66. }
  67. var recipientId = recipient ? recipient.address : body.recipientId;
  68. var recipientUsername = recipient ? recipient.username : null;
  69. // 验证甲方(发送方)用户合法性
  70. if (body.multisigAccountPublicKey && body.multisigAccountPublicKey != keypair.publicKey.toString('hex')) {
  71. // 验证多重签名
  72. modules.accounts.getAccount({publicKey: body.multisigAccountPublicKey}, function (err, account) {
  73. if (err) {
  74. return cb(err.toString());
  75. }
  76. // 多重签名帐号不存在
  77. if (!account || !account.publicKey) {
  78. return cb("Multisignature account not found");
  79. }
  80. // 多重签名帐号未激活
  81. if (!account || !account.multisignatures) {
  82. return cb("Account does not have multisignatures enabled");
  83. }
  84. // 帐号不属于该多重签名组
  85. if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) {
  86. return cb("Account does not belong to multisignature group");
  87. }
  88. // 接着验证甲方(发送方)用户合法性
  89. modules.accounts.getAccount({publicKey: keypair.publicKey}, function (err, requester) {
  90. if (err) {
  91. return cb(err.toString());
  92. }
  93. // 甲方帐号不存在
  94. if (!requester || !requester.publicKey) {
  95. return cb("Invalid requester");
  96. }
  97. // 甲方支付密码(二次签名)不正确
  98. if (requester.secondSignature && !body.secondSecret) {
  99. return cb("Invalid second passphrase");
  100. }
  101. // 甲方帐号公钥与多重签名帐号公钥是不一样的(因为两个账户是不一样的)
  102. if (requester.publicKey == account.publicKey) {
  103. return cb("Invalid requester");
  104. }
  105. var secondKeypair = null;
  106. if (requester.secondSignature) {
  107. var secondHash = crypto.createHash('sha256').update(body.secondSecret, 'utf8').digest();
  108. secondKeypair = ed.MakeKeypair(secondHash);
  109. }
  110. try {
  111. // 763行 把上述数据整理成需要的交易数据结构,并给交易添加时间戳、签名、生成ID、计算交易费等
  112. var transaction = library.logic.transaction.create({
  113. type: TransactionTypes.SEND,
  114. amount: body.amount,
  115. sender: account,
  116. recipientId: recipientId,
  117. recipientUsername: recipientUsername,
  118. keypair: keypair,
  119. requester: keypair,
  120. secondKeypair: secondKeypair
  121. });
  122. } catch (e) {
  123. return cb(e.toString());
  124. }
  125. // 776行 处理交易
  126. modules.transactions.receiveTransactions([transaction], cb);
  127. });
  128. });
  129. } else {
  130. // 直接验证甲方(发送方)用户合法性,这里的请求者requester就是发出交易者sender
  131. ...
  132. });
  133. }

上面这段代码涉及到的就是生成交易数据,这与之前的《地址》、《签名和多重签名》里提到的功能性交易差不多,这里把该方法代码完整粘贴出来,具体逻辑请看代码里的注释和前面的流程图。

接下来,776行,通过receiveTransactions方法处理交易,该方法最终调用的是下面的方法。关键部分,已经添加了注释,请结合上面的流程图阅读,不再详述。

  1. // modules/transactions.js文件
  2. // 337行
  3. Transactions.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) {
  4. modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) {
  5. // 这是个闭包,在下面的程序运行结束的时候才调用,因此是验证完毕,才写入区块链、广播到网络
  6. function done(err) {
  7. if (err) {
  8. return cb(err);
  9. }
  10. // 这里 加入区块链 操作
  11. privated.addUnconfirmedTransaction(transaction, sender, function (err) {
  12. if (err) {
  13. return cb(err);
  14. }
  15. // 触发事件,广播到网络
  16. library.bus.message('unconfirmedTransaction', transaction, broadcast);
  17. cb();
  18. });
  19. }
  20. if (err) {
  21. return done(err);
  22. }
  23. if (transaction.requesterPublicKey && sender && sender.multisignatures && sender.multisignatures.length) {
  24. modules.accounts.getAccount({publicKey: transaction.requesterPublicKey}, function (err, requester) {
  25. if (err) {
  26. return done(err);
  27. }
  28. if (!requester) {
  29. return cb("Invalid requester");
  30. }
  31. // 开始执行一系列验证,包括交易是不是已经存在
  32. library.logic.transaction.process(transaction, sender, requester, function (err, transaction) {
  33. if (err) {
  34. return done(err);
  35. }
  36. // 检查是否交易已经存在(包括双花交易)
  37. if (privated.unconfirmedTransactionsIdIndex[transaction.id] !== undefined || privated.doubleSpendingTransactions[transaction.id]) {
  38. return cb("Transaction already exists");
  39. }
  40. // 这里是 直接验证交易签名等信息,接着调用闭包 done(),把交易写入区块链并广播到网络
  41. library.logic.transaction.verify(transaction, sender, done);
  42. });
  43. });
  44. } else {
  45. ...
  46. }

总结

这里的编码逻辑非常清晰,但作为非常核心的部分,使用了大量编程技巧,需要比较熟练的开发技能。代码中涉及到大量的回调和验证,有的回调嵌套很深,需要对异步较为深入的理解,掌握熟练的回调处理方法,不然理解和编码都会有很多困扰。因此,好好熟悉基本编码技巧,从小处着手打好基础很重要。

本文涉及的流程图相对比较复杂,为了印刷方便,我把完整的流程图拆分成为四张,处理过程花费了大量时间,但是很多细节仍然无法照顾到,也无法保证没有错误和疏漏,请看到问题的小伙伴及时反馈给我。

交易是怎么写入区块链的,上面仅仅点到为止,不够详细和深入。为了进一步阐述区块链的原理,需要专门拿出一篇来,详细讲述。而且,作为目前加密货币的“网红”,区块链也值得我们好好研究。请看下一篇:《神秘的区块链》

链接

本系列文章即时更新,若要掌握最新内容,请关注下面的链接

本源文地址: https://github.com/imfly/bitcoin-on-nodejs

电子书阅读: http://bitcoin-on-nodejs.ebookchain.org

亿书白皮书: http://ebookchain.org/ebookchain.pdf

亿书官网: http://ebookchain.org

亿书官方QQ群:185046161(亿书完全开源开放,欢迎各界小伙伴参与)

参考

精通比特币(英文)

精通比特币(中文)