[譯] NaiveCoin: 如何創(chuàng)建一種加密貨幣 #1

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é)點。

此章節(jié)的完整代碼實現(xiàn)

區(qū)塊數(shù)據結構

我們先從定義區(qū)塊結構開始。在這個階段,我們只給每個區(qū)塊定義最必須的屬性。

  • index: 區(qū)塊在區(qū)塊鏈中的序列號

  • data: 任何需要包括在此區(qū)塊中的數(shù)據

  • timestamp: 時間戳

  • hash: 根據 block 內容計算的 sha256 哈希值

  • previousHash: 上一個區(qū)塊的 hash 值,此屬性起到明確指定上一個區(qū)塊的作用

image

區(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ū)塊。

由以上對 hashpreviousHash 屬性的處理,可推導出區(qū)塊鏈重要的特性,即區(qū)塊的內容不能被修改,除非同時修改它后續(xù)的所有區(qū)塊內容。

以下的例子描述了這個特性。如果將第44區(qū)塊的數(shù)據從“DESERT”修改成“STREET”,所有后續(xù)區(qū)塊的哈希值也必須被修改。這是由于區(qū)塊的哈希值取決于其previousHash 的值(以及其它屬性)。

image

這個特性在工作量證明被引入時尤其重要。一個區(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, datatimestamp)。區(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ū)塊鏈中。

image

見代碼實現(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ù)據。
image

在此項目中我們使用 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ù)據通信。

image

小結

Naivecoin 只是一個模擬的通用區(qū)塊鏈實現(xiàn)。這部分章節(jié)為我們展示了如何簡單的實現(xiàn)區(qū)塊鏈的基礎功能。下一章節(jié)中我們將 在Naivecoin 實現(xiàn)中加入POW(工作量證明)。

此章節(jié)的完整代碼實現(xiàn)

#2

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容