《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

類圖

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

總結

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

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

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

連結

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

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

電子書閱讀: http://bitcoin-on-nodejs.ebookchain.org

億書官網: http://ebookchain.org

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

參考

億書白皮書: http://ebookchain.org/ebookchain.pdf

精通比特幣(英文)

精通比特幣(中文)