如何實(shí)現(xiàn)自己的區(qū)塊鏈? 人人能寫區(qū)塊鏈?能做到嗎?寫一個(gè)自己的區(qū)塊鏈,不是讓程序員憑空想象,而是使用已經(jīng)的開(kāi)源平臺(tái),通過(guò)學(xué)習(xí)理念、工具、通過(guò)編寫簡(jiǎn)答的函數(shù)即可以實(shí)現(xiàn)自己的區(qū)塊鏈。 人人能寫區(qū)塊鏈嗎? 目前比較著名的項(xiàng)目Ethereum,你可以在Ethereum這個(gè)平臺(tái)上實(shí)現(xiàn)自己的區(qū)塊鏈。 關(guān)于Ethereum項(xiàng)目,所有的資料、文檔、代碼、開(kāi)發(fā)技巧都是開(kāi)放的,參考網(wǎng)站: https://github.com/ethereum,只要你懂一門編程語(yǔ)言,就可以實(shí)踐自己的區(qū)塊鏈,只要具備C++,go,JavaScript,Python 任何一門語(yǔ)言即可,因?yàn)榫幊陶Z(yǔ)言的原理是相通的。區(qū)塊鏈?zhǔn)鞘裁矗?/strong> 區(qū)塊鏈英文名Blockchain,本質(zhì)是保存持續(xù)增長(zhǎng)的記錄的分布式數(shù)據(jù)庫(kù)。最奇妙的是區(qū)塊鏈把分布式數(shù)據(jù)在技術(shù)上只保留了Insert 、Query 兩個(gè)操作。 如何才能實(shí)現(xiàn)這樣一個(gè)系統(tǒng)呢?那么我們需要回答幾個(gè)核心問(wèn)題。 如何保存數(shù)據(jù)?保存數(shù)據(jù)的困難有哪些?
解決方案 :
Class DataBlock { getData(); setData(); getPre(); getNext();} 數(shù)據(jù)能否被修改呢?因?yàn)槿绻С中薷?,可能引發(fā)很多的同步問(wèn)題。那么能否不支持修改呢?實(shí)際上,GFS的整體架構(gòu)就是構(gòu)建在不可修改的數(shù)據(jù)結(jié)構(gòu)之上。 于是,我們推論出構(gòu)建不可修改的串聯(lián)的數(shù)據(jù)塊。但是我們要注意的是雖然底層數(shù)據(jù)不能修改,但是上層的數(shù)據(jù)視圖是可以修改的。 如何讓很多人都對(duì)數(shù)據(jù)有信心,相信是真實(shí)的,不能被篡改呢?每個(gè)人都保存一份同樣的數(shù)據(jù)。 如何添加數(shù)據(jù)呢 我們添加的每一份數(shù)據(jù)都需要按照同樣的方式添加到每個(gè)人保存的副本中。 我們能否具有計(jì)算能力呢?為什么需要計(jì)算能力? 我們從此可以得到一個(gè)通用的計(jì)算機(jī),也把數(shù)據(jù)的修改模型更加地簡(jiǎn)化和通用化。 我們?nèi)绾味x計(jì)算能力呢? 要回答這個(gè)問(wèn)題,我們首先要想的是這個(gè)分布式的計(jì)算機(jī)的各個(gè)部分是如何構(gòu)成的。 誰(shuí)來(lái)構(gòu)成整個(gè)存儲(chǔ)空間?每一個(gè)具體的地址。每一個(gè)地址保存了什么?數(shù)據(jù)。如何才能對(duì)地址計(jì)算呢?我們可以把對(duì)數(shù)據(jù)的處理邏輯也放入這個(gè)地址。那么一個(gè)地址到底需要什么呢?地址信息、財(cái)富信息、數(shù)據(jù)信息、代碼。 于是,所謂的狀態(tài)就是指系統(tǒng)中每一個(gè)地址和地址對(duì)應(yīng)的狀態(tài)的集合。我們通過(guò)一個(gè)一個(gè)的交易來(lái)進(jìn)入新的狀態(tài)。 接著, 我們可以把狀態(tài)轉(zhuǎn)移的過(guò)程也記錄下來(lái),這個(gè)就是記錄transaction的block。這些block連接在一起,形成blockchain。 如何應(yīng)對(duì)同時(shí)寫入的混亂 如何防止很多人一起寫造成的混亂呢?大家同時(shí)解決一個(gè)難題,誰(shuí)先解出來(lái),誰(shuí)就能夠?qū)懭搿?/p> 如何防止有人同時(shí)解出來(lái)?這個(gè)有可能,但是連續(xù)多次都是同時(shí)有人解出來(lái)的概率較低,于是選擇鏈最長(zhǎng)的那一個(gè)。 具備以上基本概念,讓我們?cè)谶@里通過(guò)實(shí)際的代碼進(jìn)一步實(shí)戰(zhàn),實(shí)現(xiàn)一個(gè)支持Token的BlockChain。參考:Naivecoin: a tutorial for building a cryptocurrency 官方網(wǎng)站:https://lhartikk. ( 具備基本的編程基礎(chǔ)) 如何保存數(shù)據(jù)區(qū)塊鏈的基本概念是:保存持續(xù)增長(zhǎng)的有序數(shù)據(jù)的分布式數(shù)據(jù)庫(kù)。要實(shí)現(xiàn)自己的區(qū)塊鏈,需要滿足以下功能:
區(qū)塊的結(jié)構(gòu)因?yàn)閰^(qū)塊鏈中的數(shù)據(jù)是相互連接的數(shù)據(jù)塊,因此我們需要?jiǎng)?chuàng)建LinkedList來(lái)實(shí)現(xiàn)這樣的場(chǎng)景。 如上圖所示,我們可以看到以下核心元素:
我們可以得到對(duì)應(yīng)的代碼: class Block { public index: number; public hash: string; public previousHash: string; public timestamp: number; public data: string; constructor(index: number, hash: string, previousHash: string, timestamp: number, data: string) { this.index = index; this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.hash = hash; } } 如何保證數(shù)據(jù)不被篡改在計(jì)算機(jī)的世界中,一切都是用數(shù)學(xué)來(lái)解釋,用數(shù)學(xué)來(lái)證明。對(duì)于一個(gè)數(shù)據(jù)塊,我們計(jì)算出它的摘要。只要保證數(shù)據(jù)有變化,必然會(huì)引發(fā)摘要變化即可。在這里我們使用的是SHA256的Hash算法。從這個(gè)角度來(lái)說(shuō),Hash也可以被看成這塊數(shù)據(jù)的DNA。 具體的Hash的過(guò)程如下, const calculateHash = (index: number, previousHash: string, timestamp: number, data: string): string => CryptoJS.SHA256(index + previousHash + timestamp + data).toString(); 聰明的讀者可能會(huì)發(fā)現(xiàn),這個(gè)簡(jiǎn)單的方法并不能防范更加復(fù)雜的黑客攻擊。因此,我們會(huì)不斷的改進(jìn)我們的代碼,以抵擋這個(gè)世界的某些惡意。 但是現(xiàn)在的我們至少能夠通過(guò)Hash來(lái)唯一的驗(yàn)證一個(gè)區(qū)塊鏈的結(jié)構(gòu)了。 為什么? 因?yàn)槲覀冊(cè)谧鯤ash的時(shí)候,也把這個(gè)區(qū)塊對(duì)應(yīng)的上一個(gè)區(qū)塊的Hash放了進(jìn)來(lái),因此如果有人想要篡改整個(gè)區(qū)塊鏈上的任何一個(gè)區(qū)塊,都會(huì)產(chǎn)生蝴蝶效應(yīng),后續(xù)的區(qū)塊都會(huì)為止失效。 如上圖所示,如果我們把區(qū)塊44的數(shù)據(jù)從TREE改為STREET,那么它自身的Hash結(jié)果會(huì)改變,接著區(qū)塊45中的previousHash也會(huì)發(fā)生改變,于是區(qū)塊45的Hash也會(huì)改變,以此類推。因此,越早的區(qū)塊發(fā)生異常,那么帶來(lái)的影響就會(huì)越大。 如何創(chuàng)建第一個(gè)區(qū)塊 第一個(gè)數(shù)據(jù)塊的難點(diǎn)在哪里?它沒(méi)有previousHash!因此,我們直接硬編碼即可。 const genesisBlock: Block = new Block( 0, '816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7', null, 1465154705, 'my genesis block!!'); 如何創(chuàng)建新的區(qū)塊創(chuàng)建區(qū)塊如同炒菜,需要備好所有的原料。如同以下的代碼所示,我們需要找到最后一個(gè)有效的區(qū)塊,推理出下一個(gè)區(qū)塊的index,得到當(dāng)前的時(shí)間戳,再結(jié)合上一個(gè)區(qū)塊的hash和當(dāng)前的數(shù)據(jù),也就能知道當(dāng)前區(qū)塊的hash,從而創(chuàng)建出新的區(qū)塊。 const generateNextBlock = (blockData: string) => { const previousBlock: Block = getLatestBlock(); const nextIndex: number = previousBlock.index + 1; const nextTimestamp: number = new Date().getTime() / 1000; const nextHash: string = calculateHash(nextIndex, previousBlock.hash, nextTimestamp, blockData); const newBlock: Block = new Block(nextIndex, nextHash, previousBlock.hash, nextTimestamp, blockData); return newBlock;}; 區(qū)塊保存在哪里 在當(dāng)前的版本中,我們只是保存在JavaScript所使用的內(nèi)存中,因此很容易丟失,但是我們可以逐漸完善,讓數(shù)據(jù)的保存越來(lái)越持久。 const blockchain: Block[] = [genesisBlock]; 如何驗(yàn)證數(shù)據(jù)的有效性在任何一個(gè)時(shí)刻,如果其他人給了我們一個(gè)新的區(qū)塊,我們?nèi)绾悟?yàn)證這個(gè)區(qū)塊是正確的呢?這需要符合以下的基本要求:
于是,我們實(shí)現(xiàn)的代碼如下: const isValidNewBlock = (newBlock: Block, previousBlock: Block) => { if (previousBlock.index + 1 !== newBlock.index) { console.log('invalid index'); return false; } else if (previousBlock.hash !== newBlock.previousHash) { console.log('invalid previoushash'); return false; } else if (calculateHashForBlock(newBlock) !== newBlock.hash) { console.log(typeof (newBlock.hash) + ' ' + typeof calculateHashForBlock(newBlock)); console.log('invalid hash: ' + calculateHashForBlock(newBlock) + ' ' + newBlock.hash); return false; } return true;}; 這里還有一個(gè)細(xì)節(jié),如果區(qū)塊內(nèi)數(shù)據(jù)的結(jié)構(gòu)不正確,也可能是一個(gè)問(wèn)題。因此我們還要額外進(jìn)行判斷。 const isValidBlockStructure = (block: Block): boolean => { return typeof block.index === 'number' && typeof block.hash === 'string' && typeof block.previousHash === 'string' && typeof block.timestamp === 'number' && typeof block.data === 'string';}; 我們現(xiàn)在可以完整的驗(yàn)證一條區(qū)塊鏈了嗎?可以。我們首先要單獨(dú)處理第一個(gè)區(qū)塊,然后依次驗(yàn)證之后的每一個(gè)區(qū)塊。 const isValidChain = (blockchainToValidate: Block[]): boolean => { const isValidGenesis = (block: Block): boolean => { return JSON.stringify(block) === JSON.stringify(genesisBlock); }; if (!isValidGenesis(blockchainToValidate[0])) { return false; } for (let i = 1; i blockchainToValidate.length; i++) { if (!isValidNewBlock(blockchainToValidate[i], blockchainToValidate[i - 1])) { return false; } } return true;}; 區(qū)塊鏈分叉了怎么辦在一個(gè)分布式的環(huán)境中,有可能不同的人在同一個(gè)區(qū)塊后添加了新的不同的區(qū)塊,那我們要聽(tīng)誰(shuí)的呢?聽(tīng)大多數(shù)人的話(盡管現(xiàn)實(shí)中大多數(shù)人的話也許……)!那誰(shuí)才能代表大多數(shù)的人民呢?實(shí)力更強(qiáng)大,更長(zhǎng)的那一條鏈。 因此在遇到多條鏈的時(shí)候,我們可以直接選擇更長(zhǎng)的一條。具體代碼如下, const replaceChain = (newBlocks: Block[]) => { if (isValidChain(newBlocks) && newBlocks.length > getBlockchain().length) { console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); blockchain = newBlocks; broadcastLatest(); } else { console.log('Received blockchain invalid'); }}; 節(jié)點(diǎn)之間要如何通訊因?yàn)樵谡麄€(gè)網(wǎng)絡(luò)中有很多節(jié)點(diǎn),大家都有可能去創(chuàng)建區(qū)塊,這就需要大家通過(guò)協(xié)商通訊的方式達(dá)成共識(shí),這需要以下三個(gè)基本能力:
上圖給出了節(jié)點(diǎn)通訊的具體流程。需要注意的是,在我們的代碼中,所有的連接都被爆存在了 如何控制節(jié)點(diǎn) 我們需要一個(gè)對(duì)外的接口來(lái)控制一個(gè)節(jié)點(diǎn),從而能夠查看節(jié)點(diǎn)的區(qū)塊、添加區(qū)塊、查看連通的節(jié)點(diǎn)、添加節(jié)點(diǎn)。于是我們通過(guò)以下代碼實(shí)現(xiàn)了HTTP對(duì)外的服務(wù)接口。 const initHttpServer = ( myHttpPort: number ) => { const app = express(); app.use(bodyParser.json()); app.get('/blocks', (req, res) => { res.send(getBlockchain()); }); app.post('/mineBlock', (req, res) => { const newBlock: Block = generateNextBlock(req.body.data); res.send(newBlock); }); app.get('/peers', (req, res) => { res.send(getSockets().map(( s: any ) => s._socket.remoteAddress + ':' + s._socket.remotePort)); }); app.post('/addPeer', (req, res) => { connectToPeers(req.body.peer); res.send(); }); app.listen(myHttpPort, () => { console.log('Listening http on port: ' + myHttpPort); });}; 于是,我們可以直接訪問(wèn)接口進(jìn)行控制。例如,獲得全部區(qū)塊的列表。 #get all blocks from the node> curl http://localhost:3001/blocks 系統(tǒng)的架構(gòu)是什么如上圖所示,我們每個(gè)節(jié)點(diǎn)都會(huì)向外提供兩個(gè)服務(wù):
小結(jié):如何保存數(shù)據(jù)綜上所述,我們已經(jīng)構(gòu)建了能夠保存區(qū)塊的區(qū)塊鏈的服務(wù)結(jié)構(gòu),實(shí)現(xiàn)了創(chuàng)建區(qū)塊和控制節(jié)點(diǎn)的基本能力。讓我們繼續(xù)添加更多的功能吧。 如何應(yīng)對(duì)攻擊在我們已經(jīng)實(shí)現(xiàn)的版本中,每個(gè)人都能在其中添加區(qū)塊,這樣不僅可能造成混亂,而且如果有人拼命的添加區(qū)塊也會(huì)阻塞整個(gè)網(wǎng)絡(luò)。 如何應(yīng)對(duì)呢? 那我們就限制每個(gè)人添加區(qū)塊的能力吧。如何限制呢?記得你在網(wǎng)站每次注冊(cè)新賬號(hào)的時(shí)候都會(huì)出現(xiàn)的驗(yàn)證碼嗎?我們只要讓大家在每次添加區(qū)塊的時(shí)候都要做一道“難題”即可。這就是Proof-of-Work的基本原理,而這個(gè)解題過(guò)程就被稱之為挖礦。 因此,這個(gè)難題的設(shè)置會(huì)影響到節(jié)點(diǎn)添加區(qū)塊的難度。越難的題會(huì)讓我們?cè)诫y添加區(qū)塊,相對(duì)來(lái)說(shuō)安全性會(huì)上升,但是延遲很可能增加。 如何設(shè)置不同難度的題目一個(gè)好的題目要讓計(jì)算機(jī)便于理解,運(yùn)算規(guī)則相對(duì)簡(jiǎn)單,運(yùn)算方式相對(duì)公平。于是結(jié)合Hash算法的題目被設(shè)計(jì)了出來(lái):找到一個(gè)特定區(qū)塊,這個(gè)區(qū)塊的Hash需要有特殊的前綴。 這個(gè)前綴越特殊,難度就越大。于是我們可以定義出題目的難度difficulty為你所定義的特殊的前綴是由幾個(gè)0組成。例如,如果你只要求找到的區(qū)塊的Hash有一個(gè)0(difficulty=0),那么可能相對(duì)簡(jiǎn)單;但是如果你要求你找到的區(qū)塊的Hash的前綴有10個(gè)0(difficulty=10),那么就有點(diǎn)難了。下圖給出了更細(xì)節(jié)的展示。 我們可以相應(yīng)的實(shí)現(xiàn)檢查Hash是否滿足difficulty的代碼。 const hashMatchesDifficulty = (hash: string, difficulty: number): boolean => { const hashInBinary: string = hexToBinary(hash); const requiredPrefix: string = '0'.repeat(difficulty); return hashInBinary.startsWith(requiredPrefix);}; 為了找到滿足difficulty條件的Hash,我們需要對(duì)同一個(gè)區(qū)塊計(jì)算出不同的Hash。但是這個(gè)Hash算法的一致性相矛盾??墒俏覀兛梢酝ㄟ^(guò)在區(qū)塊中加入新的參數(shù)來(lái)實(shí)現(xiàn)Hash結(jié)果的變化,因?yàn)镾HA256會(huì)因?yàn)閿?shù)據(jù)的任何微小變化為完全變化。于是我們添加了一個(gè)叫做Nonce的參數(shù),并且不斷的改變這個(gè)參數(shù)直到挖到我們想要的Hash結(jié)果。于是一個(gè)區(qū)塊的數(shù)據(jù)結(jié)構(gòu)更新如下: class Block { public index: number; public hash: string; public previousHash: string; public timestamp: number; public data: string; public difficulty: number; public nonce: number; constructor(index: number, hash: string, previousHash: string, timestamp: number, data: string, difficulty: number, nonce: number) { this.index = index; this.previousHash = previousHash; this.timestamp = timestamp; this.data = data; this.hash = hash; this.difficulty = difficulty; this.nonce = nonce; }} 如何解一個(gè)難題基于以上的分析,我們不斷的增加Nonce的值,直到找到一個(gè)有效的Hash,具體代碼如下: const findBlock = (index: number, previousHash: string, timestamp: number, data: string, difficulty: number): Block => { let nonce = 0; while (true) { const hash: string = calculateHash(index, previousHash, timestamp, data, difficulty, nonce); if (hashMatchesDifficulty(hash, difficulty)) { return new Block(index, hash, previousHash, timestamp, data, difficulty, nonce); } nonce++; }}; 當(dāng)我們找到了一個(gè)有效的Hash,就把這個(gè)區(qū)塊廣播給整個(gè)網(wǎng)絡(luò)。 如何確定難度 雖然我們能夠指定問(wèn)題的難度,但是我們要如何設(shè)置難度呢?而且如何才能讓網(wǎng)絡(luò)的節(jié)點(diǎn)都認(rèn)同這個(gè)難度呢? 讓我們回歸到區(qū)塊鏈的本身。區(qū)塊鏈無(wú)非是一個(gè)區(qū)塊的鏈表,并且每隔一段時(shí)間會(huì)加入一個(gè)新的區(qū)塊。而我們的題目就是在控制加入?yún)^(qū)塊的難度,也就是加入的時(shí)間間隔,于是我們引入一個(gè)全局參數(shù):
但是隨著環(huán)境的變化,例如有更多的節(jié)點(diǎn)加入網(wǎng)絡(luò),我們并不能一致維持這個(gè)時(shí)間,因此我們每隔一段時(shí)間需要調(diào)整一下難度,于是我們引入第二個(gè)全局參數(shù):
在我們的代碼中,我們會(huì)設(shè)置間隔為10秒。 // in seconds const BLOCK_GENERATION_INTERVAL: number = 10; // in blocks const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10; 于是在我們的區(qū)塊鏈中每產(chǎn)生10個(gè)區(qū)塊就會(huì)查看區(qū)塊的生成頻率是否滿足我們的預(yù)期
const getDifficulty = (aBlockchain: Block[]): number => { const latestBlock: Block = aBlockchain[blockchain.length - 1]; if (latestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0 && latestBlock.index !== 0) { return getAdjustedDifficulty(latestBlock, aBlockchain); } else { return latestBlock.difficulty; }};const getAdjustedDifficulty = (latestBlock: Block, aBlockchain: Block[]) => { const prevAdjustmentBlock: Block = aBlockchain[blockchain.length - DIFFICULTY_ADJUSTMENT_INTERVAL]; const timeExpected: number = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL; const timeTaken: number = latestBlock.timestamp - prevAdjustmentBlock.timestamp; if (timeTaken timeExpected / 2) { return prevAdjustmentBlock.difficulty + 1; } else if (timeTaken > timeExpected * 2) { return prevAdjustmentBlock.difficulty - 1; } else { return prevAdjustmentBlock.difficulty; }}; 時(shí)間戳被篡改了怎么辦我們題目的難度的調(diào)整需要用到區(qū)塊中保存的時(shí)間戳,但是這個(gè)時(shí)間戳可以由一個(gè)節(jié)點(diǎn)寫入任何值,我們?nèi)绾螒?yīng)對(duì)這樣的攻擊呢?這里的難點(diǎn)還在于不同節(jié)點(diǎn)上的時(shí)間本來(lái)就會(huì)有一定的差異。但是我們知道如果區(qū)塊的時(shí)間戳和我們自己的時(shí)間相差越遠(yuǎn)則越可能有問(wèn)題,因此我們把這個(gè)差異限制上一個(gè)區(qū)塊的創(chuàng)建時(shí)間到當(dāng)前時(shí)間范圍的1分鐘以內(nèi)。 const isValidTimestamp = (newBlock: Block, previousBlock: Block): boolean => { return ( previousBlock.timestamp - 60 newBlock.timestamp ) && newBlock.timestamp - 60 getCurrentTimestamp();}; 有人通過(guò)低難度產(chǎn)生超長(zhǎng)鏈怎么辦?我們?cè)谏弦还?jié)討論過(guò)當(dāng)遇到分叉的時(shí)候,選擇更長(zhǎng)的一個(gè)。但是一個(gè)惡意節(jié)點(diǎn)可以產(chǎn)生一個(gè)很長(zhǎng)的但是每個(gè)區(qū)塊的難度都是最簡(jiǎn)單的分叉。這樣怎么辦?那就把選擇標(biāo)準(zhǔn)從最長(zhǎng)調(diào)整為最難的。也就是說(shuō),我們會(huì)選擇累計(jì)解題難度最大的分叉,因?yàn)檫@背后所代表的人民的力量更加強(qiáng)大。 如何計(jì)算累計(jì)的難度呢?因?yàn)槊考右粋€(gè)0,我們的計(jì)算難度的期望會(huì)乘以2,所以我們計(jì)算每個(gè)區(qū)塊的 如上圖所示,再A和B兩個(gè)鏈條中,雖然B更短,但是因?yàn)锽的難度更大,所以我們會(huì)選擇B。 有一個(gè)需要注意的是,這里的關(guān)鍵是難度,而并非Hash的前置的0,因?yàn)槲覀冇锌赡芘銮傻玫揭粋€(gè)更多的0的情況。這個(gè)思路被稱之為“中本聰共識(shí)”,是中本聰發(fā)明Bitcoin時(shí)的一個(gè)重要貢獻(xiàn)。因?yàn)槊總€(gè)節(jié)點(diǎn)只有相對(duì)較小的Hash計(jì)算能力,因此他們會(huì)傾向于選擇累計(jì)難度更長(zhǎng)的鏈條來(lái)貢獻(xiàn)自己的力量,從而讓自己的貢獻(xiàn)得到認(rèn)可。 小結(jié):如何應(yīng)對(duì)攻擊 Proof-of-work的特點(diǎn)在于難于計(jì)算,易于驗(yàn)證。因此尋找特定前綴的SHA256成為一個(gè)很好的難題。 我們已經(jīng)在代碼中加入了難度,并且節(jié)點(diǎn)可以通過(guò)挖礦來(lái)把區(qū)塊添加到區(qū)塊鏈中。 如何交易我們?cè)谇皟晒?jié)實(shí)現(xiàn)的區(qū)塊鏈只是對(duì)數(shù)據(jù)的基本保存,如何能夠在這個(gè)基礎(chǔ)上構(gòu)建金融體系?但是一個(gè)金融體系的基本需求是什么呢?
但是我們的區(qū)塊鏈?zhǔn)且粋€(gè)沒(méi)有“信任”的分布式的網(wǎng)絡(luò),如何才能構(gòu)建出“確定性”呢?這需要我們找到一個(gè)不可抵賴的證明體系。 如何證明你是你其實(shí)證明自己往往是最難的,這需要我們落地到一個(gè)我們可以相信的事情。想一想古代碎玉為半,之后團(tuán)圓相認(rèn)的場(chǎng)景。在計(jì)算機(jī)的世界也是一樣,我們把一塊美玉的一半告訴全世界,然后把另一半藏在自己身上,這樣之后你自己能夠拼接處這塊美玉。 但這背后的難點(diǎn)在于,別人有了你公布出來(lái)的一半的玉,是可以偽造另一半的。但是在計(jì)算機(jī)的世界里,公鑰加密體系卻沒(méi)有這個(gè)缺陷。 你有兩個(gè)鑰匙:公鑰和私鑰。公鑰是由私鑰推演出來(lái)的,并且會(huì)公布給所有人。對(duì)于你自己發(fā)出去的信息,你都可以用你的私鑰簽名。其他人會(huì)收到你的信息和你的簽名,然后他會(huì)用你的公鑰來(lái)驗(yàn)證這個(gè)信息是否是通過(guò)你的私鑰進(jìn)行的簽名。具體流程如下圖所示。 具體來(lái)說(shuō),我們會(huì)選擇橢圓曲線加密算法(ECDSA)。到目前為止,我們引入了密碼學(xué)中的兩個(gè)核心工具:
公鑰和私鑰長(zhǎng)什么樣一個(gè)有效的私鑰是一個(gè)32字節(jié)的字符串,示例如下: 19f128debc1b9122da0635954488b208b829879cf13b3d6cac5d1260c0fd967c 一個(gè)有效的公鑰是由‘04’開(kāi)頭,緊接著64個(gè)字節(jié)的自負(fù)換,示例如下: 04bfcab8722991ae774db48f934ca79cfb7dd991229153b9f732ba5334aafcd8e7266e47076996b55a14bf9913ee3145ce0cfc1372ada8ada74bd287450313534a 公鑰是由私鑰演繹得到的,我們可以直接把它做為區(qū)塊鏈中一個(gè)用戶的賬號(hào)地址。 如何記錄一次交易我們已經(jīng)能夠讓用戶證明自己是誰(shuí)了,現(xiàn)在就要記錄他們之間的交易了。我們需要三個(gè)信息
即便如此,我們依然有個(gè)疑問(wèn)?發(fā)送者如何證明自己有這個(gè)token呢?那么他就需要提供之前他獲得這個(gè)token的證據(jù)。于是我們還需要第四個(gè)信息:指向自己的證據(jù)的指針。一個(gè)例子如下圖所示。 于是我們需要兩個(gè)結(jié)構(gòu)分別表示交易的發(fā)起者和交易的接收者。 接收者長(zhǎng)什么樣對(duì)于接受者,我們需要知道他的地址和交易的數(shù)量。如上一節(jié)所述,地址是ECDSA 的公鑰。這意味著,我們還需要保證只有對(duì)應(yīng)私鑰的擁有者才能進(jìn)一步操作這些token。這個(gè)結(jié)構(gòu)體的代碼如下: class TxOut { public address: string; public amount: number; constructor(address: string, amount: number) { this.address = address; this.amount = amount; }} 發(fā)起者長(zhǎng)什么樣交易的發(fā)起者需要提供自己token來(lái)源的證據(jù),也就是指向之前的交易。但是他要證明自己對(duì)這個(gè)交易的擁有權(quán),因此需要提供通過(guò)自己私鑰加密的簽名。這個(gè)結(jié)構(gòu)體的代碼如下: class TxIn { public txOutId: string; public txOutIndex: number; public signature: string;} 需要注意的是這里保存的只是通過(guò)私鑰進(jìn)行的簽名,而不是私鑰本身。在區(qū)塊鏈的整個(gè)系統(tǒng)中,僅僅存在他的公鑰和簽名,而不會(huì)出現(xiàn)他的私鑰。 如上圖所示,整個(gè)過(guò)程就是發(fā)起者解鎖了txIns中的tokens,然后把它們轉(zhuǎn)給了TxOut中的接收者。 完整的交易長(zhǎng)什么樣結(jié)合之前的討論,我們可以構(gòu)建出最終的交易: class Transaction { public id: string; public txIns: TxIn[]; public txOuts: TxOut[];} 如何唯一表示一次交易我們依然可以使用SHA256來(lái)進(jìn)行Hash,并且使用這個(gè)Hash來(lái)做為交易的id。這里要注意的是我們并沒(méi)有包含發(fā)起者的簽名,這個(gè)會(huì)在之后添加。 const getTransactionId = (transaction: Transaction): string => { const txInContent: string = transaction.txIns .map((txIn: TxIn) => txIn.txOutId + txIn.txOutIndex) .reduce((a, b) => a + b, ''); const txOutContent: string = transaction.txOuts .map((txOut: TxOut) => txOut.address + txOut.amount) .reduce((a, b) => a + b, ''); return CryptoJS.SHA256(txInContent + txOutContent).toString();}; 如何對(duì)交易進(jìn)行簽名因?yàn)樵趨^(qū)塊鏈中所有的交易都是公開(kāi)的,因此要保證沒(méi)有人能夠利用這些交易進(jìn)行攻擊。于是我們需要對(duì)所有敏感的信息都進(jìn)行簽名。具體代碼如下: const signTxIn = (transaction: Transaction, txInIndex: number, privateKey: string, aUnspentTxOuts: UnspentTxOut[]): string => { const txIn: TxIn = transaction.txIns[txInIndex]; const dataToSign = transaction.id; const referencedUnspentTxOut: UnspentTxOut = findUnspentTxOut(txIn.txOutId, txIn.txOutIndex, aUnspentTxOuts); const referencedAddress = referencedUnspentTxOut.address; const key = ec.keyFromPrivate(privateKey, 'hex'); const signature: string = toHexString(key.sign(dataToSign).toDER()); return signature;}; 但是我們會(huì)發(fā)現(xiàn)這里只是對(duì)交易id進(jìn)行了簽名,這樣足夠了嗎? 一種潛在的攻擊方式如下:當(dāng)攻擊者CCC收到一個(gè)交易:從地址AAA向地址BBB發(fā)送10個(gè)token,交易id為0x555...。他會(huì)嘗試把接受者修改為自己,然后把這個(gè)交易發(fā)送到網(wǎng)絡(luò)中。于是這個(gè)消息變成了:從地址AAA向地址CCC發(fā)送10個(gè)token。但是,當(dāng)另一個(gè)節(jié)點(diǎn)DDD接收到這個(gè)交易信息之后,會(huì)進(jìn)行驗(yàn)證,他首先計(jì)算交易id。但是這時(shí)候因?yàn)榻邮苷弑桓淖兞?,因此交易id也會(huì)改變,例如成為了0x567...。于是發(fā)現(xiàn)問(wèn)題。 及時(shí)攻擊者也修改了id為0x567...,但是AAA只是對(duì)0x555...進(jìn)行了簽名,因此簽名的數(shù)據(jù)會(huì)不匹配。因此,攻擊者也會(huì)被識(shí)破。 到目前為止,整個(gè)協(xié)議看似是安全的。 如何找到用戶擁有的token在一起交易中,發(fā)起者需要提供自己所擁有的沒(méi)有使用的token。因此,我們需要從當(dāng)前的區(qū)塊鏈中找到這些信息,于是我們需要維持整個(gè)系統(tǒng)中沒(méi)有花費(fèi)掉token的情況。這樣的數(shù)據(jù)結(jié)構(gòu)如以下代碼所示: class UnspentTxOut { public readonly txOutId: string; public readonly txOutIndex: number; public readonly address: string; public readonly amount: number; constructor(txOutId: string, txOutIndex: number, address: string, amount: number) { this.txOutId = txOutId; this.txOutIndex = txOutIndex; this.address = address; this.amount = amount; }} 我們進(jìn)一步可以把系統(tǒng)中所有未花費(fèi)的token記錄在一個(gè)數(shù)組中: let unspentTxOuts: UnspentTxOut[] = []; 如何更新未花費(fèi)的數(shù)據(jù)信息我們什么時(shí)候更新呢?當(dāng)新的區(qū)塊產(chǎn)生的時(shí)候。因?yàn)檫@個(gè)區(qū)塊里會(huì)包含新的交易信息。因此,我們需要從新的區(qū)塊中找到所有未花費(fèi)的token的信息,并且記錄在newUnspentTxOuts之中,代碼如下: const newUnspentTxOuts: UnspentTxOut[] = newTransactions .map((t) => { return t.txOuts.map((txOut, index) => new UnspentTxOut(t.id, index, txOut.address, txOut.amount)); }) .reduce((a, b) => a.concat(b), []); 我們同時(shí)也要知道哪些未被花費(fèi)的token被花費(fèi)掉了,這個(gè)被記錄在consumedTxOuts,代碼如下: const consumedTxOuts: UnspentTxOut[] = newTransactions .map((t) => t.txIns) .reduce((a, b) => a.concat(b), []) .map((txIn) => new UnspentTxOut(txIn.txOutId, txIn.txOutIndex, '', 0)); 最終我們通過(guò)刪除已經(jīng)花費(fèi)的并且加上新的未話費(fèi)的,從而產(chǎn)生了新的未話費(fèi)?數(shù)組resultingUnspentTxOuts,具體代碼如下: const resultingUnspentTxOuts = aUnspentTxOuts .filter(((uTxO) => !findUnspentTxOut(uTxO.txOutId, uTxO.txOutIndex, consumedTxOuts))) .concat(newUnspentTxOuts); 以上邏輯通過(guò)updateUnspentTxOuts的方法來(lái)實(shí)現(xiàn)。需要注意的是這個(gè)方法要在驗(yàn)證了區(qū)塊正確性的基礎(chǔ)上再來(lái)執(zhí)行,否則會(huì)產(chǎn)生各種風(fēng)險(xiǎn)。 如何驗(yàn)證交易的有效性剛才提到了,我們需要驗(yàn)證交易的有效性,要如何做呢?這背后要思考的是有什么情況會(huì)產(chǎn)生異常。 交易結(jié)構(gòu)異常怎么辦?我們需要判斷交易的結(jié)構(gòu)如何符合我們的標(biāo)準(zhǔn)。 const isValidTransactionStructure = (transaction: Transaction) => { if (typeof transaction.id !== 'string') { console.log('transactionId missing'); return false; } ... //check also the other members of class } 交易id異常怎么辦?我們需要進(jìn)行判斷。 if (getTransactionId(transaction) !== transaction.id) { console.log('invalid tx id: ' + transaction.id); return false; } 發(fā)起者信息異常怎么辦?我們可以對(duì)簽名進(jìn)行判斷,同時(shí)也要判斷token尚未被花費(fèi)。 const validateTxIn = (txIn: TxIn, transaction: Transaction, aUnspentTxOuts: UnspentTxOut[]): boolean => { const referencedUTxOut: UnspentTxOut = aUnspentTxOuts.find((uTxO) => uTxO.txOutId === txIn.txOutId && uTxO.txOutId === txIn.txOutId); if (referencedUTxOut == null) { console.log('referenced txOut not found: ' + JSON.stringify(txIn)); return false; } const address = referencedUTxOut.address; const key = ec.keyFromPublic(address, 'hex'); return key.verify(transaction.id, txIn.signature);}; 交易數(shù)量異常怎么辦?我們需要對(duì)發(fā)起者標(biāo)注的未花費(fèi)的數(shù)量和交易的實(shí)際數(shù)量進(jìn)行對(duì)比,查看兩者是否相等。 const totalTxInValues: number = transaction.txIns .map((txIn) => getTxInAmount(txIn, aUnspentTxOuts)) .reduce((a, b) => (a + b), 0); const totalTxOutValues: number = transaction.txOuts .map((txOut) => txOut.amount) .reduce((a, b) => (a + b), 0); if (totalTxOutValues !== totalTxInValues) { console.log('totalTxOutValues !== totalTxInValues in tx: ' + transaction.id); return false; } 區(qū)塊鏈的token最初從哪里來(lái)我們可以不斷的回溯每一個(gè)交易,但是最初的交易的token從哪里來(lái)呢?這需要我們定義無(wú)中生有的基礎(chǔ)交易。 在基礎(chǔ)交易中,它只有接收者,而沒(méi)有發(fā)起者。這如同國(guó)家銀行印刷了新的鈔票。在我們的區(qū)塊鏈中,將其定義為50。 const COINBASE_AMOUNT: number = 50; 這個(gè)沒(méi)有起點(diǎn)的交易從哪里來(lái)呢?來(lái)自于我們對(duì)支撐系統(tǒng)的“礦工”的獎(jiǎng)勵(lì)。每當(dāng)你挖出一個(gè)區(qū)塊,系統(tǒng)會(huì)獎(jiǎng)勵(lì)你50個(gè)token。 我們要如何保存這些初始的獎(jiǎng)勵(lì)呢?可以添加一個(gè)額外的標(biāo)志符。因?yàn)檫@個(gè)獎(jiǎng)勵(lì)是連同區(qū)塊一起產(chǎn)生的,所以我們可以使用區(qū)塊的id。 但是我們需要一些特殊的方法來(lái)驗(yàn)證這類初始獎(jiǎng)勵(lì)的有效性: const validateCoinbaseTx = (transaction: Transaction, blockIndex: number): boolean => { if (getTransactionId(transaction) !== transaction.id) { console.log('invalid coinbase tx id: ' + transaction.id); return false; } if (transaction.txIns.length !== 1) { console.log('one txIn must be specified in the coinbase transaction'); return; } if (transaction.txIns[0].txOutIndex !== blockIndex) { console.log('the txIn index in coinbase tx must be the block height'); return false; } if (transaction.txOuts.length !== 1) { console.log('invalid number of txOuts in coinbase transaction'); return false; } if (transaction.txOuts[0].amount != COINBASE_AMOUNT) { console.log('invalid coinbase amount in coinbase transaction'); return false; } return true;}; 小結(jié):如何交易我們?cè)诒竟?jié)討論了如何在區(qū)塊鏈中支持交易。核心概念是每個(gè)交易把一些未花費(fèi)的token轉(zhuǎn)換了新的主人。我們是通過(guò)一個(gè)人的私鑰來(lái)定義歸屬權(quán)的。 但是我們依然需要手動(dòng)的創(chuàng)建交易,因此我們會(huì)在下一章介紹如何實(shí)現(xiàn)錢包。 如何實(shí)現(xiàn)錢包我們已經(jīng)有了token,如何讓用戶更容易的管理自己的token并進(jìn)行交易呢?我們需要支持什么樣的核心功能?
在Bitcoin中你可以通過(guò)錢包管理自己的coin,在Ethereum中你也可以用錢包管理自己的各類token。 如何創(chuàng)建錢包錢包的基礎(chǔ)是什么?公鑰和私鑰。因此我們需要首先創(chuàng)建用戶的這兩把鑰匙。首先是私鑰,并且要保存在本地: const privateKeyLocation = 'node/wallet/private_key';const generatePrivatekey = (): string => { const keyPair = EC.genKeyPair(); const privateKey = keyPair.getPrivate(); return privateKey.toString(16);};const initWallet = () => { //let's not override existing private keys if (existsSync(privateKeyLocation)) { return; } const newPrivateKey = generatePrivatekey(); writeFileSync(privateKeyLocation, newPrivateKey); console.log('new wallet with private key created');}; 在這個(gè)基礎(chǔ)上,我們可以通過(guò)私鑰創(chuàng)建公鑰。 const getPublicFromWallet = (): string => { const privateKey = getPrivateFromWallet(); const key = EC.keyFromPrivate(privateKey, 'hex'); return key.getPublic().encode('hex');}; 需要注意的是把私鑰保存在本地是一件很不安全的事情。雖然我們這里只是一個(gè)簡(jiǎn)化的版本,但是也有很多更保險(xiǎn)的方法。因此,請(qǐng)善待你的私鑰吧。 如何顯示余額所謂的余額,不過(guò)是一些未花費(fèi)的交易的接收者的記錄。那么要如何定位這些記錄呢?用戶的公鑰。因此當(dāng)你定位到之后只需要對(duì)記錄求和即可。 const getBalance = (address: string, unspentTxOuts: UnspentTxOut[]): number => { return _(unspentTxOuts) .filter((uTxO: UnspentTxOut) => uTxO.address === address) .map((uTxO: UnspentTxOut) => uTxO.amount) .sum();}; 上述代碼首先基于公鑰定位到了記錄,然后進(jìn)行了求和。 如何進(jìn)行交易如何能夠屏蔽底層的發(fā)起者和接收者等復(fù)雜概念來(lái)簡(jiǎn)單的使用呢?而且我們的底層支持的是把發(fā)起者包括的所有token都給予接收者。如果發(fā)起者有50個(gè)token,但是指向轉(zhuǎn)移10個(gè)呢?這時(shí)候需要我們把剩余的40個(gè)token還給發(fā)起者。具體場(chǎng)景如下圖所示: 這個(gè)過(guò)程甚至能夠更加負(fù)責(zé),例如:
這個(gè)場(chǎng)景如上圖所示,我們要如何做呢?具體來(lái)說(shuō)我們需要把這三次交易的總token拆成兩份,其中的55個(gè)給D,另外的5個(gè)還給C。 如何實(shí)現(xiàn)這個(gè)代碼呢?我們首先在C所有未花費(fèi)的交易中不斷的累積token,直到總和達(dá)到或者超過(guò)目標(biāo)值。 const findTxOutsForAmount = (amount: number, myUnspentTxOuts: UnspentTxOut[]) => { let currentAmount = 0; const includedUnspentTxOuts = []; for (const myUnspentTxOut of myUnspentTxOuts) { includedUnspentTxOuts.push(myUnspentTxOut); currentAmount = currentAmount + myUnspentTxOut.amount; if (currentAmount >= amount) { const leftOverAmount = currentAmount - amount; return {includedUnspentTxOuts, leftOverAmount} } } throw Error('not enough coins to send transaction');}; 如代碼所示,我們還記錄了額外多出來(lái)的數(shù)量,我們之后會(huì)把它還給C。 因?yàn)槲覀冇辛诵枰褂玫奈椿ㄙM(fèi)的交易,于是我們能夠創(chuàng)建發(fā)起者的數(shù)據(jù)了。 const toUnsignedTxIn = (unspentTxOut: UnspentTxOut) => { const txIn: TxIn = new TxIn(); txIn.txOutId = unspentTxOut.txOutId; txIn.txOutIndex = unspentTxOut.txOutIndex; return txIn;};const {includedUnspentTxOuts, leftOverAmount} = findTxOutsForAmount(amount, myUnspentTxouts);const unsignedTxIns: TxIn[] = includedUnspentTxOuts.map(toUnsignedTxIn); 然后我們可以把對(duì)應(yīng)的token分別給予D和C,也就是一個(gè)是我們的接受者,一個(gè)是還給發(fā)起者。當(dāng)然,如果token恰好不多不少,我們就不需要?dú)w還了。 const createTxOuts = (receiverAddress:string, myAddress:string, amount, leftOverAmount: number) => { const txOut1: TxOut = new TxOut(receiverAddress, amount); if (leftOverAmount === 0) { return [txOut1] } else { const leftOverTx = new TxOut(myAddress, leftOverAmount); return [txOut1, leftOverTx]; }}; 我們現(xiàn)在可以構(gòu)建交易并且簽名了。 const tx: Transaction = new Transaction(); tx.txIns = unsignedTxIns; tx.txOuts = createTxOuts(receiverAddress, myAddress, amount, leftOverAmount); tx.id = getTransactionId(tx); tx.txIns = tx.txIns.map((txIn: TxIn, index: number) => { txIn.signature = signTxIn(tx, index, privateKey, unspentTxOuts); return txIn; }); 如何使用錢包我們現(xiàn)在構(gòu)建使用錢包的一個(gè)外部接口。 app.post('/mineTransaction', (req, res) => { const address = req.body.address; const amount = req.body.amount; const resp = generatenextBlockWithTransaction(address, amount); res.send(resp); }); 用戶只需要提供接收者地址和交易數(shù)量就可以使用錢包了。 小結(jié):如何實(shí)現(xiàn)錢包我們實(shí)現(xiàn)了支持交易的錢包。雖然在使用中最多包括兩個(gè)接收者,但實(shí)際上我們底層的接口支持更多復(fù)雜的場(chǎng)景。例如,把50個(gè)token分給三個(gè)不同的人。 但是現(xiàn)在你只能通過(guò)自己挖礦來(lái)添加新的區(qū)塊,我們要如何才能更方便的使用呢?這是下一節(jié)的內(nèi)容。 如何找他人幫忙如果每次添加交易都需要用戶自己挖礦,那么效率會(huì)極為低下。我們?nèi)绾尾拍芾盟藖?lái)幫忙呢?這需要我們把未確認(rèn)的交易提交到這個(gè)網(wǎng)絡(luò)中,并且期待有人能夠幫助我們把這次交易寫入?yún)^(qū)塊鏈中。 因此節(jié)點(diǎn)直接除了同步區(qū)塊的信息之外還需要交流未確認(rèn)的交易信息。 如何保存未確認(rèn)的交易我們需要構(gòu)建一個(gè)新的結(jié)構(gòu)“交易池”來(lái)保存未確認(rèn)的交易(Bitcoin中稱之為mempool)。 我們可以通過(guò)數(shù)據(jù)來(lái)實(shí)現(xiàn): let transactionPool: Transaction[] = []; 如何使用這個(gè)新的提交交易的功能呢?我們可以在創(chuàng)建一個(gè)對(duì)外的接口 app.post('/sendTransaction', (req, res) => { ... }) 這時(shí)候我們就不再需要挖礦,而只是把交易記錄下來(lái)。 const sendTransaction = (address: string, amount: number): Transaction => { const tx: Transaction = createTransaction(address, amount, getPrivateFromWallet(), getUnspentTxOuts(), getTransactionPool()); addToTransactionPool(tx, getUnspentTxOuts()); return tx;}; 如何通知他人交易信息當(dāng)我們添加一個(gè)未確認(rèn)的交易后,我們需要把這個(gè)交易告訴整個(gè)網(wǎng)絡(luò),并且期待有人會(huì)把這個(gè)交易放入?yún)^(qū)塊鏈中。我們要如何廣播呢?
因此我們需要構(gòu)建兩個(gè)新的消息: enum MessageType { QUERY_LATEST = 0, QUERY_ALL = 1, RESPONSE_BLOCKCHAIN = 2, QUERY_TRANSACTION_POOL = 3, RESPONSE_TRANSACTION_POOL = 4} 交易池信息的消息構(gòu)建如下: const responseTransactionPoolMsg = (): Message => ({ 'type': MessageType.RESPONSE_TRANSACTION_POOL, 'data': JSON.stringify(getTransactionPool())});const queryTransactionPoolMsg = (): Message => ({ 'type': MessageType.QUERY_TRANSACTION_POOL, 'data': null}); 為了實(shí)現(xiàn)整個(gè)廣播的邏輯,我們需要添加處理 case MessageType.RESPONSE_TRANSACTION_POOL: const receivedTransactions: Transaction[] = JSONToObjectTransaction[]>(message.data); receivedTransactions.forEach((transaction: Transaction) => { try { handleReceivedTransaction(transaction); //if no error is thrown, transaction was indeed added to the pool //let's broadcast transaction pool broadCastTransactionPool(); } catch (e) { //unconfirmed transaction not valid (we probably already have it in our pool) } }); 如何防止重放攻擊每個(gè)節(jié)點(diǎn)都能發(fā)送交易信息,因此我們需要驗(yàn)證是否有風(fēng)險(xiǎn)和錯(cuò)誤。如同之前的判斷標(biāo)準(zhǔn)一樣,我們需要判斷交易的格式是否正確,交易的發(fā)起者、接收者、簽名是否匹配。 但是在整個(gè)網(wǎng)絡(luò)中,一個(gè)攻擊者可以通過(guò)把同一個(gè)未花費(fèi)的交易用在不同的交易中來(lái)進(jìn)行攻擊。因此我們需要添加一條驗(yàn)證規(guī)則:一個(gè)未確認(rèn)交易中的任何一個(gè)未花費(fèi)交易不能出現(xiàn)在已有的未確認(rèn)交易中。具體的代碼實(shí)現(xiàn)如下, const isValidTxForPool = (tx: Transaction, aTtransactionPool: Transaction[]): boolean => { const txPoolIns: TxIn[] = getTxPoolIns(aTtransactionPool); const containsTxIn = (txIns: TxIn[], txIn: TxIn) => { return _.find(txPoolIns, (txPoolIn => { return txIn.txOutIndex === txPoolIn.txOutIndex && txIn.txOutId === txPoolIn.txOutId; })) }; for (const txIn of tx.txIns) { if (containsTxIn(txPoolIns, txIn)) { console.log('txIn already found in the txPool'); return false; } } return true;}; 我們要如何從交易池中移除一個(gè)交易呢?每當(dāng)一個(gè)新的區(qū)塊產(chǎn)生后,我們會(huì)更新交易池。 如何把未確認(rèn)交易放入?yún)^(qū)塊當(dāng)一個(gè)節(jié)點(diǎn)開(kāi)始挖礦的時(shí)候,他會(huì)把自己的交易池作為整個(gè)區(qū)塊的數(shù)據(jù)。具體代碼如下: const generateNextBlock = () => { const coinbaseTx: Transaction = getCoinbaseTransaction(getPublicFromWallet(), getLatestBlock().index + 1); const blockData: Transaction[] = [coinbaseTx].concat(getTransactionPool()); return generateRawNextBlock(blockData);};As the transactions are already validated, before they are added to the pool, we are not doing any further validations at this points. 如何更新交易池當(dāng)一個(gè)新的區(qū)塊產(chǎn)生時(shí),它會(huì)讓我們交易池中的很多區(qū)塊無(wú)效,因此我們需要重新驗(yàn)證交易池。場(chǎng)景如下,
因此,我們實(shí)現(xiàn)如下代碼: const updateTransactionPool = (unspentTxOuts: UnspentTxOut[]) => { const invalidTxs = []; for (const tx of transactionPool) { for (const txIn of tx.txIns) { if (!hasTxIn(txIn, unspentTxOuts)) { invalidTxs.push(tx); break; } } } if (invalidTxs.length > 0) { console.log('removing the following transactions from txPool: %s', JSON.stringify(invalidTxs)); transactionPool = _.without(transactionPool, ...invalidTxs) }}; 從以上代碼可以看出,我們只需要知道當(dāng)前尚未花費(fèi)掉交易即可進(jìn)行判斷。 小結(jié):如何找他人幫忙我們現(xiàn)在可以通過(guò)他人來(lái)幫忙把交易加入?yún)^(qū)塊中。但是一個(gè)節(jié)點(diǎn)為什么要加入其他人的交易信息呢?我們需要給他們支付一定的費(fèi)用。這個(gè)就留給你來(lái)實(shí)現(xiàn)吧。 我們?cè)谙乱还?jié)會(huì)實(shí)現(xiàn)能夠?qū)^(qū)塊鏈進(jìn)行操作的用戶界面。 如何實(shí)現(xiàn)用戶界面只是通過(guò)接口進(jìn)行操作還不夠直觀,讓我們?cè)诒竟?jié)中實(shí)現(xiàn)用戶界面。因?yàn)槲覀兊墓?jié)點(diǎn)已經(jīng)實(shí)現(xiàn)了HTTP的接口,因此我們只需要?jiǎng)?chuàng)建一個(gè)網(wǎng)頁(yè)進(jìn)行可視化就行了。 那么我們還需要什么接口來(lái)支持呢?
如何實(shí)現(xiàn)查詢接口以下是查詢區(qū)塊詳細(xì)信息的接口: app.get('/block/:hash', (req, res) => { const block = _.find(getBlockchain(), {'hash' : req.params.hash}); res.send(block); }); 以下是查詢交易詳細(xì)信息的接口: app.get('/transaction/:id', (req, res) => { const tx = _(getBlockchain()) .map((blocks) => blocks.data) .flatten() .find({'id': req.params.id}); res.send(tx); }); 以下是查詢具體地址詳細(xì)信息的接口,你將會(huì)看到一個(gè)地址的未花費(fèi)交易,從而能夠得到他的余額。 app.get('/address/:address', (req, res) => { const unspentTxOuts: UnspentTxOut[] = _.filter(getUnspentTxOuts(), (uTxO) => uTxO.address === req.params.address) res.send({'unspentTxOuts': unspentTxOuts}); }); 當(dāng)然,我們也可以添加他已經(jīng)花費(fèi)掉的交易的信息,從而得到更加全景的信息。 我們?nèi)绾螌?shí)現(xiàn)前端界面我們使用 如何查看區(qū)塊鏈的信息我們可以實(shí)現(xiàn)“區(qū)塊鏈查看器”的網(wǎng)站來(lái)對(duì)區(qū)塊鏈的信息進(jìn)行可視化,基于之前的接口,我們可以得到每個(gè)地址的余額等信息。因?yàn)檫@個(gè)功能只是進(jìn)行查詢,所以我們只需要對(duì)結(jié)果進(jìn)行可視化就行了。一個(gè)界面的截圖如下: 如何查看錢包的信息錢包的界面需要更多的交易能力,一個(gè)界面的截圖如下: 小結(jié):如何實(shí)現(xiàn)用戶界面我們?yōu)槲覀兊膮^(qū)塊鏈實(shí)現(xiàn)了查看器和錢包的界面,這會(huì)成為你進(jìn)一步前進(jìn)的基礎(chǔ)。 總結(jié)我們?cè)谶@一章實(shí)現(xiàn)了帶有token功能的區(qū)塊鏈,你能想到還有哪些重要的功能需要我們?nèi)?shí)現(xiàn)嗎? EthereumEthereum是一個(gè)圖靈機(jī),通過(guò)編寫協(xié)議來(lái)驅(qū)動(dòng)狀態(tài)的改變。 什么是圖靈機(jī)?狀態(tài)和狀態(tài)轉(zhuǎn)移。 核心功能?所有權(quán)、交易、狀態(tài)轉(zhuǎn)換。 賬號(hào)是什么?一個(gè)160位的二進(jìn)制。 都是人能夠控制的嗎?人能夠直接控制的是外部賬號(hào);通過(guò)代碼控制的是內(nèi)部賬號(hào)。 如何防止有人超長(zhǎng)時(shí)間占用節(jié)點(diǎn)?需要付費(fèi)。 系統(tǒng)架構(gòu)Go-ethereumEthereum最流行的Go版本的實(shí)現(xiàn)。 Solidity能夠編譯成供EVM執(zhí)行的ByteCode的高級(jí)語(yǔ)言。 MistEthereum的應(yīng)用查看器。 |
|
來(lái)自: 昵稱30836886 > 《區(qū)塊鏈數(shù)字幣》