1. 程式人生 > >《Nodejs開發加密貨幣》之二十一:交易

《Nodejs開發加密貨幣》之二十一:交易

題外話:這篇文章,耗費了我大量精力,用UML表達javascript類及流程本來就不是什麼容易的事情,用來描述加密貨幣交易這種驗證邏輯非常多的程式碼更難,加之Nodejs的回撥在這些程式碼裡巢狀很深,所以如何把非同步呼叫變成人類容易理解的順序呼叫,也做了一番取捨,時間不知不覺就過了一星期。

所幸,趕在比特幣減半的今天完成併發布這篇文章,也算在區塊鏈火熱的今天,《Nodejs開發加密貨幣》走到了一個關鍵節點:觸及了加密貨幣的靈魂和腹地。動輒幾千一枚的比特幣等加密貨幣可能會消亡,但是背後的技術卻蓬勃發展,玩技術的要善於把握先機,搶佔技術高點,讓自己時刻成為稀缺的資源,自身價值才能一路高升。

本書是市面上唯一一本講解Nodejs開發加密貨幣的實踐書籍,與那些純粹為了舉例而提供的程式碼示例不同,全書程式碼,哪怕是前端程式碼例項,都是來自正在執行的真實專案,所以無論您是學習Nodejs技術找尋實踐專案,還是學習前端設計、web開發等,或者深入區塊鏈研究,都值得參考。

書中專案億書完全開源,本書完全開源,連結在文末,敬請關注或參與。

前言

我們在第一部分《瞭解加密貨幣》裡說過,加密貨幣是“利益”轉移的程式化,其核心目標是保證數字財富或價值安全、透明、快速的轉移。因此,交易是加密貨幣系統中最重要的部分,加密貨幣的核心就是交易,加密解密、P2P網路、區塊鏈等一系列技術都是圍繞交易展開的。

這一篇,我們就來研究億書提供的交易型別及程式碼實現,集中總結交易的生命週期及實現過程,把在《地址》和《簽名和多重簽名》裡故意漏掉的判斷邏輯補充完整。

原始碼

類圖

transactions-clase.png

解讀

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。

這些資料有的要求使用者輸入,比如使用者金鑰,交易數量等,這些資料是否正確,也是非常關鍵的事情。這是軟體程式驗證邏輯的一個重要部分,不可或缺。這個很好理解,如果一個人胡亂填寫金鑰和接受地址,也能把幣傳送出去,那就笑話了。但具體校驗過程較為繁瑣,這裡主要涉及到:發起交易的使用者是否存在、金鑰是否正確、是否多重簽名帳號、是否有支付密碼,以及接受方使用者地址是否合法等,都要逐個檢驗。

詳情看這裡的流程圖:

addTransaction-activity.png

(2)給合法交易簽名

基本資訊正確之後,一筆合法交易,還要使用甲乙方的公鑰簽名,確保交易所屬。同時,還要準確記錄它的交易時間戳,方便追溯。還要生成交易ID,每個交易ID都包含了豐富的加密資訊,需要複雜的生成過程,絕不像傳統的網站系統,讓資料庫自動生成索引就可以充當ID了。

詳情看這裡的流程圖:

signTransaction-activity.png

(3)驗證交易合法性

通常,一筆交易經過6-10個區塊之後,這筆交易被認為是無法更改的,即已確認,因為這時候拒絕、變更的難度已經非常大,理論上已經不可能。這裡的交易合法性,除了基本資訊正確之外,主要是指保證交易是未確認的交易,也不是使用者重複提交的交易,即雙花交易。雙花交易是加密貨幣特有的現象,通俗的說,就是使用者在交易確認之前(有一段時間,比特幣時間更長),又一次提交了相同交易資訊,導致一筆錢花兩次,這種情況是必須要避免的。

每筆交易在廣播到網路之前必須驗證合法性,不合法的交易沒有機會廣播到網路。節點收到新的交易資訊時,要重新驗證。如此一來,任何對網路的攻擊,都只會影響一個節點,安全性大大提高。

驗證合法的交易就可以直接加入區塊鏈了,因此從上面的第一步到現在,億書都是在一個節點上完成的。這也為下面的廣播處理打下基礎,一旦交易被廣播到網路,在其他節點,這裡的驗證和處理過程就會重複執行一次。

驗證的過程,看這裡的流程圖:

verifyTransaction-activity.png

(4)廣播到點對點網路

沒有中心伺服器,必須藉助點對點網路,把交易資料寫入分散式公共賬本——區塊鏈,保證交易資料永遠無法篡改,而且可以輕鬆查詢追溯。這在中心化的伺服器上,為了應對個別交易摩擦,保證交易記錄可追溯,要採取更多的技術手段,記錄更多的資料欄位,意味著要保持大量資料冗餘,付出更多資金成本。

因為交易資料不含私密資訊,對網路沒有苛刻要求,因此加密貨幣的網路可以覆蓋很廣,對網路的程式設計也變得靈活很多。理論上,只要能保證聯通的便捷和快速,具體設計中不需要考慮更多複雜的因素。當然,就億書這款產品而言,獨有的使用者協作和分享功能,對網路程式設計的效能有自身的要求,就另當別論,這方面將在下一個版本中體現出來。

這裡,僅僅是加密貨幣基礎網路功能,交易廣播到網路的流程如下:

broadcastTransaction-activity.png

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 {
            ...
}

總結

這裡的編碼邏輯非常清晰,但作為非常核心的部分,使用了大量程式設計技巧,需要比較熟練的開發技能。程式碼中涉及到大量的回撥和驗證,有的回撥巢狀很深,需要對非同步較為深入的理解,掌握熟練的回撥處理方法,不然理解和編碼都會有很多困擾。因此,好好熟悉基本編碼技巧,從小處著手打好基礎很重要。

本文涉及的流程圖相對比較複雜,為了印刷方便,我把完整的流程圖拆分成為四張,處理過程花費了大量時間,但是很多細節仍然無法照顧到,也無法保證沒有錯誤和疏漏,請看到問題的小夥伴及時反饋給我。

交易是怎麼寫入區塊鏈的,上面僅僅點到為止,不夠詳細和深入。為了進一步闡述區塊鏈的原理,需要專門拿出一篇來,詳細講述。而且,作為目前加密貨幣的“網紅”,區塊鏈也值得我們好好研究。請看下一篇:《神祕的區塊鏈》

連結

本系列文章即時更新,若要掌握最新內容,請關注下面的連結

億書官方QQ群:185046161(億書完全開源開放,歡迎各界小夥伴參與)

參考