1: 搭建最小可用的區(qū)塊鏈
原文鏈接 #1: Minimal working blockchain
概要
區(qū)塊鏈的基礎概念其實是相當簡單的:一個分布式的數(shù)據庫,數(shù)據庫的每個運行節(jié)點都維護一個持續(xù)增長的按時間排序的記錄列表。在此章節(jié)中我們將實現(xiàn)一個最簡版的區(qū)塊鏈。此章節(jié)結束時,我們的區(qū)塊鏈將有以下功能:
嚴格定義的區(qū)塊和區(qū)塊鏈結構
提供將包含任意數(shù)據的新區(qū)塊寫入到區(qū)塊鏈的方法
可以與其他運行節(jié)點溝通和同步鏈數(shù)據的運行節(jié)點
提供簡易的 HTTP API來操作單個運行節(jié)點。
區(qū)塊數(shù)據結構
我們先從定義區(qū)塊結構開始。在這個階段,我們只給每個區(qū)塊定義最必須的屬性。
index: 區(qū)塊在區(qū)塊鏈中的序列號data: 任何需要包括在此區(qū)塊中的數(shù)據timestamp: 時間戳hash: 根據 block 內容計算的sha256哈希值previousHash: 上一個區(qū)塊的hash值,此屬性起到明確指定上一個區(qū)塊的作用
區(qū)塊結構對應的代碼如下:
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;
}
}
區(qū)塊哈希值
區(qū)塊哈希值是區(qū)塊中最重要的屬性之一。哈希值根據所有區(qū)塊中的數(shù)據計算而得,這意味著如果區(qū)塊中任何屬性發(fā)生變化,原有的哈希值就不再有效。區(qū)塊哈希值也能被看成區(qū)塊的唯一性標識。舉例來說,有可能出現(xiàn)兩個 index 一致的區(qū)塊,但是他們總會有不一樣的哈希值。
根據以下的代碼來計算哈希值:
const calculateHash = (index: number,
previousHash: string,
timestamp: number,
data: string) : string =>
CryptoJS.SHA256(index + previousHash + timestamp + data).toString();
需要注意的是,在這個階段,區(qū)塊的哈希值與挖礦沒有任何關系,因為還未有 POW(工作量證明) 問題需要解決。我們使用區(qū)塊哈希值來保證區(qū)塊的完整性,同時也使用它明確的引用上一個區(qū)塊。
由以上對 hash和 previousHash 屬性的處理,可推導出區(qū)塊鏈重要的特性,即區(qū)塊的內容不能被修改,除非同時修改它后續(xù)的所有區(qū)塊內容。
以下的例子描述了這個特性。如果將第44區(qū)塊的數(shù)據從“DESERT”修改成“STREET”,所有后續(xù)區(qū)塊的哈希值也必須被修改。這是由于區(qū)塊的哈希值取決于其previousHash 的值(以及其它屬性)。
這個特性在工作量證明被引入時尤其重要。一個區(qū)塊在區(qū)塊鏈中的位置越深(越靠前),要修改它的難度就越大,因為需要同時修改它本身以及它后續(xù)的所有區(qū)塊。
創(chuàng)世區(qū)塊
創(chuàng)世區(qū)塊是區(qū)塊鏈中的第一個區(qū)塊。它是唯一一個沒有 previousHash 的區(qū)塊,我們在代碼里將創(chuàng)世區(qū)塊硬編碼:
const genesisBlock: Block = new Block(
0, '816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d',
null, 1465154705, 'my genesis block!!');
創(chuàng)建區(qū)塊
創(chuàng)建一個新的區(qū)塊,需要獲得上一個區(qū)塊的哈希值,并創(chuàng)建其他必須的內容( index, hash, data 和 timestamp)。區(qū)塊的數(shù)據(data字段)由用戶提供,其他的參數(shù)使用以下代碼生成:
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ū)塊鏈
目前我們使用 JavaScript 的數(shù)組,將區(qū)塊鏈保存在程序的運行內存中。這意味著當一個運行節(jié)點停止時,該節(jié)點上的區(qū)塊鏈數(shù)據不會被持久化。
const blockchain: Block[] = [genesisBlock];
驗證區(qū)塊完整性
我們需要隨時可以對一個區(qū)塊,或者一條區(qū)塊鏈上的區(qū)塊是否有效(數(shù)據是否完好,哈希值是否對應內容)。當我們的節(jié)點從其他運行節(jié)點中接收新的區(qū)塊時,我們尤其需要驗證區(qū)塊的有效性,以便決定是否接受這些區(qū)塊。
驗證區(qū)塊的有效性,需要滿足以下所有條件:
- 區(qū)塊的
index需要比上一個區(qū)塊大1; - 區(qū)塊的
previousHash屬性需要與上一個區(qū)塊的hash屬性一致; - 區(qū)塊自身的 hash 值需要有效。
以下代碼描述了驗證過程:
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;
};
同時我們還必須驗證該區(qū)塊的結構正確,以避免其他節(jié)點發(fā)來的未正確格式的數(shù)據造成程序崩潰。
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)在我們可以驗證單個區(qū)塊是否有效,讓我們進一步對一條鏈上的區(qū)塊做驗證。首先驗證鏈中的第一個區(qū)塊為創(chuàng)世區(qū)塊。然后,我們使用以上的方式來依次校驗鏈中的下一個區(qū)塊,以下為實現(xiàn)代碼:
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ū)塊鏈中都應該只存在一組區(qū)塊。在沖突發(fā)生的情況下(如:兩個運行節(jié)點都生成了第72區(qū)塊),則從中選擇包含更長區(qū)塊的鏈。在以下的例子中,由于被更長的區(qū)塊鏈復寫,第72區(qū)塊: a350235b00 中的數(shù)據將不會被包括在區(qū)塊鏈中。
見代碼實現(xiàn):
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é)點間通信
與其他節(jié)點分享數(shù)據和同步區(qū)塊鏈數(shù)據,是每個運行節(jié)點的必要功能。以下規(guī)則來保證節(jié)點間網絡的同步:
- 當一個節(jié)點生成新的區(qū)塊時,它將此區(qū)塊廣播至網絡中;
- 當一個節(jié)點連接到另一個節(jié)點時,向此節(jié)點查詢最新的區(qū)塊信息;
- 當一個節(jié)點發(fā)現(xiàn)新的區(qū)塊,該區(qū)塊的 index 比節(jié)點中的區(qū)塊大,則將此區(qū)塊加到自身的區(qū)塊鏈中,或者查詢以獲得整條鏈的數(shù)據。
在此項目中我們使用 WebSocket 技術來實現(xiàn) peer-to-peer 的通信。各個節(jié)點活躍的 socket 列表保存在 const sockets: WebSocket[] 變量中。項目沒有實現(xiàn)節(jié)點發(fā)現(xiàn)機制,需要手動添加所有的節(jié)點地址(WebSocket URLs)。
操作節(jié)點
用戶需要能以某種方式來操作節(jié)點。在項目中我們?yōu)榇舜罱藢?HTTP 服務。
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);
});
};
根據以上代碼,用戶可以通過以下方式操作節(jié)點:
- 列舉所有區(qū)塊
- 創(chuàng)建一個由用戶指定內容的新區(qū)塊
- 列舉和新增節(jié)點
可以使用 Curl 操作節(jié)點:
#get all blocks from the node
> curl http://localhost:3001/blocks
架構
每個節(jié)點都對外暴露兩個web 服務: 一個 HTTP server 給用戶來操作節(jié)點,一個Websocket HTTP server 實現(xiàn)節(jié)點間的數(shù)據通信。
小結
Naivecoin 只是一個模擬的通用區(qū)塊鏈實現(xiàn)。這部分章節(jié)為我們展示了如何簡單的實現(xiàn)區(qū)塊鏈的基礎功能。下一章節(jié)中我們將 在Naivecoin 實現(xiàn)中加入POW(工作量證明)。