《Nodejs开发加密货币》之二十一:交易

题外话:这篇文章,耗费了我大量精力,用UML表达javascript类及流程本来就不是什么容易的事情,用来描述加密货币交易这种验证逻辑非常多的代码更难,加之Nodejs的回调在这些代码里嵌套很深,所以如何把异步调用变成人类容易理解的顺序调用,也做了一番取舍,时间不知不觉就过了一星期。

所幸,赶在比特币减半的今天完成并发布这篇文章,也算在区块链火热的今天,《Nodejs开发加密货币》走到了一个关键节点:触及了加密货币的灵魂和腹地。动辄几千一枚的比特币等加密货币可能会消亡,但是背后的技术却蓬勃发展,玩技术的要善于把握先机,抢占技术高点,让自己时刻成为稀缺的资源,自身价值才能一路高升。

本书是市面上唯一一本讲解Nodejs开发加密货币的实践书籍,与那些纯粹为了举例而提供的代码示例不同,全书代码,哪怕是前端代码实例,都是来自正在运行的真实项目,所以无论您是学习Nodejs技术找寻实践项目,还是学习前端设计、web开发等,或者深入区块链研究,都值得参考。

书中项目亿书完全开源,本书完全开源,链接在文末,敬请关注或参与。

前言

我们在第一部分《了解加密货币》里说过,加密货币是“利益”转移的程序化,其核心目标是保证数字财富或价值安全、透明、快速的转移。因此,交易是加密货币系统中最重要的部分,加密货币的核心就是交易,加密解密、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

类图

《Nodejs开发加密货币》之二十一:交易_第1张图片

解读

1、交易的本质

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

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

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

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

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

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

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

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

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

2、交易生命周期

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

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

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

3、亿书交易类型

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

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

其中,

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

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

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

4、交易基本流程

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

(1)生成交易数据

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

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

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

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

详情看这里的流程图:

《Nodejs开发加密货币》之二十一:交易_第2张图片

(2)给合法交易签名

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

详情看这里的流程图:

《Nodejs开发加密货币》之二十一:交易_第3张图片

(3)验证交易合法性

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

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

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

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

《Nodejs开发加密货币》之二十一:交易_第4张图片

(4)广播到点对点网络

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

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

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

《Nodejs开发加密货币》之二十一:交易_第5张图片

5、转账交易分析

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

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

// 148行
router.map(shared, {
  "get /": "getTransactions",
  "get /get": "getTransaction",
  "get /unconfirmed/get": "getUnconfirmedTransaction",
  "get /unconfirmed": "getUnconfirmedTransactions",
  "put /": "addTransactions"
});

// 160行
library.network.app.use('/api/transactions', router);

解析一下,就是:

get /api/transactions/ -> shared.getTransactions
get /api/transactions/get -> shared.getTransaction
get /api/transactions/unconfirmed/get -> shared.getUnconfirmedTransaction
get /api/transactions/unconfirmed -> shared.getUnconfirmedTransactions
put /api/transactions/ -> shared.addTransactions

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

// 652行
shared.addTransactions = function (req, cb) {
    var body = req.body;
    library.scheme.validate(body, {
        type: "object",
        properties: {
            secret: {
                type: "string",
                minLength: 1,
                maxLength: 100
            },
            amount: {
                type: "integer",
                minimum: 1,
                maximum: constants.totalAmount
            },
            recipientId: {
                type: "string",
                minLength: 1
            },
            publicKey: {
                type: "string",
                format: "publicKey"
            },
            secondSecret: {
                type: "string",
                minLength: 1,
                maxLength: 100
            },
            multisigAccountPublicKey: {
                type: "string",
                format: "publicKey"
            }
        },
        //
        required: ["secret", "amount", "recipientId"]
    }, function (err) {
        // 验证数据格式
        if (err) {
            return cb(err[0].message);
        }

        // 验证密码信息
        var hash = crypto.createHash('sha256').update(body.secret, 'utf8').digest();
        var keypair = ed.MakeKeypair(hash);

        if (body.publicKey) {
            if (keypair.publicKey.toString('hex') != body.publicKey) {
                return cb("Invalid passphrase");
            }
        }

        var query = {};

        // 乙方(接收方)地址转换,保证可以用户名转账
        var isAddress = /^[0-9]+[L|l]$/g;
        if (isAddress.test(body.recipientId)) {
            query.address = body.recipientId;
        } else {
            query.username = body.recipientId;
        }

        library.balancesSequence.add(function (cb) {
            // 验证乙方用户合法性
            modules.accounts.getAccount(query, function (err, recipient) {
                if (err) {
                    return cb(err.toString());
                }
                if (!recipient && query.username) {
                    return cb("Recipient not found");
                }

                var recipientId = recipient ? recipient.address : body.recipientId;
                var recipientUsername = recipient ? recipient.username : null;

                // 验证甲方(发送方)用户合法性
                if (body.multisigAccountPublicKey && body.multisigAccountPublicKey != keypair.publicKey.toString('hex')) {
                    // 验证多重签名
                    modules.accounts.getAccount({publicKey: body.multisigAccountPublicKey}, function (err, account) {
                        if (err) {
                            return cb(err.toString());
                        }
                        // 多重签名帐号不存在
                        if (!account || !account.publicKey) {
                            return cb("Multisignature account not found");
                        }
                        // 多重签名帐号未激活
                        if (!account || !account.multisignatures) {
                            return cb("Account does not have multisignatures enabled");
                        }
                        // 帐号不属于该多重签名组
                        if (account.multisignatures.indexOf(keypair.publicKey.toString('hex')) < 0) {
                            return cb("Account does not belong to multisignature group");
                        }

                        // 接着验证甲方(发送方)用户合法性
                        modules.accounts.getAccount({publicKey: keypair.publicKey}, function (err, requester) {
                            if (err) {
                                return cb(err.toString());
                            }
                            // 甲方帐号不存在
                            if (!requester || !requester.publicKey) {
                                return cb("Invalid requester");
                            }

                            // 甲方支付密码(二次签名)不正确
                            if (requester.secondSignature && !body.secondSecret) {
                                return cb("Invalid second passphrase");
                            }

                            // 甲方帐号公钥与多重签名帐号公钥是不一样的(因为两个账户是不一样的)
                            if (requester.publicKey == account.publicKey) {
                                return cb("Invalid requester");
                            }

                            var secondKeypair = null;

                            if (requester.secondSignature) {
                                var secondHash = crypto.createHash('sha256').update(body.secondSecret, 'utf8').digest();
                                secondKeypair = ed.MakeKeypair(secondHash);
                            }

                            try {
                                // 763行 把上述数据整理成需要的交易数据结构,并给交易添加时间戳、签名、生成ID、计算交易费等
                                var transaction = library.logic.transaction.create({
                                    type: TransactionTypes.SEND,
                                    amount: body.amount,
                                    sender: account,
                                    recipientId: recipientId,
                                    recipientUsername: recipientUsername,
                                    keypair: keypair,
                                    requester: keypair,
                                    secondKeypair: secondKeypair
                                });
                            } catch (e) {
                                return cb(e.toString());
                            }

                            // 776行 处理交易
                            modules.transactions.receiveTransactions([transaction], cb);
                        });
                    });
                } else {
                    // 直接验证甲方(发送方)用户合法性,这里的请求者requester就是发出交易者sender                
                    ...
    });
}

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

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

// modules/transactions.js文件
// 337行
Transactions.prototype.processUnconfirmedTransaction = function (transaction, broadcast, cb) {
    modules.accounts.setAccountAndGet({publicKey: transaction.senderPublicKey}, function (err, sender) {
        // 这是个闭包,在下面的程序运行结束的时候才调用,因此是验证完毕,才写入区块链、广播到网络
        function done(err) {
            if (err) {
                return cb(err);
            }
            // 这里 加入区块链 操作
            private.addUnconfirmedTransaction(transaction, sender, function (err) {
                if (err) {
                    return cb(err);
                }
                // 触发事件,广播到网络
                library.bus.message('unconfirmedTransaction', transaction, broadcast);

                cb();
            });
        }

        if (err) {
            return done(err);
        }

        if (transaction.requesterPublicKey && sender && sender.multisignatures && sender.multisignatures.length) {
            modules.accounts.getAccount({publicKey: transaction.requesterPublicKey}, function (err, requester) {
                if (err) {
                    return done(err);
                }

                if (!requester) {
                    return cb("Invalid requester");
                }
                // 开始执行一系列验证,包括交易是不是已经存在
                library.logic.transaction.process(transaction, sender, requester, function (err, transaction) {
                    if (err) {
                        return done(err);
                    }

                    // 检查是否交易已经存在(包括双花交易)
                    if (private.unconfirmedTransactionsIdIndex[transaction.id] !== undefined || private.doubleSpendingTransactions[transaction.id]) {
                        return cb("Transaction already exists");
                    }
                    // 这里是 直接验证交易签名等信息,接着调用闭包 done(),把交易写入区块链并广播到网络
                    library.logic.transaction.verify(transaction, sender, done);
                });
            });
        } else {
            ...
}

总结

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

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

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

链接

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

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

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

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

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

参考

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

精通比特币(英文)

精通比特币(中文)

你可能感兴趣的:(Nodejs开发加密货币,区块链)