#1: 一个小型可工作区块链
概述
区块链的基本概念很简单:一个分布式数据库,保持不断增长的命令列表记录。在这一章里,我们将实现这样的区块链精简具版本。最后一章我们将在区块链中加入以下基本功能:
-
定义一个区块,并构造区块链
-
实现一个能带任意数据并形成新区块的方法
-
区块链的节点之间的通信与同步
-
一个简单的HTTP API控制节点
本章中实现的完整代码,可以在这里找到。
区块的结构
我们需要定义块的基本结构,这里只定义了一些最基本的属性
-
index:区块的高度
-
data:任何块中包含的数据
-
timestamp:一个时间戳
-
hash:通过sha256加密块的内容得到的hash
-
previousHash:一个散列引用前面的块。 这个值显式地定义了前面的块

就像下面的代码一样:
class Block {
public index: number; //区块的索引
public hash: string; //区块的hash
public previousHash: string; //前一个区块的hash
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;
}
}
区块的哈希(hash)
区块的hash是重要的属性之一,hash是通过区块的所有数据计算得到。这就意味着任何hash改变,原来的hash都会失效。hash也可以认为是一个区块的唯一标识符。
我们通过下面的代码计算得到hash
const calculateHash = (index: number, previousHash: string, timestamp: number, data: string): string =>
CryptoJS.SHA256(index + previousHash + timestamp + data).toString();
应该注意,hash还不能被挖掘,因为还没有使用proof-of-work来解决。我们用hash来保存完整的块和显式地引用前面的块。
一个重要的一点,一个区块中==hash==和==previousHash==不能被改变,除非改变所有的区块。
在下面的例子中,如果第44区块的数据从==desert==变为==street==,所有的区块都应该全部改变。这是由于==hash==依赖于==previoushash==。(除此之外)

当介绍POW(工作量证明)时,这是一个重要的概念,区块的高度越高,就越难被修改,因为要修改所有连续的区块。
创世区块
创世区块是区块链的第一条区块,这是唯一一个没有previoushash的区块。我们将通过编码生成第一条区块。
const genesisBlock: Block = new Block(
0, '816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7', null, 1465154705, 'my genesis block!!'
);
生成一个区块
生成一个区块之前,我们先需要知道前一个区块的hash和当前区块的索引、hash、数据、时间戳。区块的数据将由最终的用户给定。代码如下。
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;
};
区块的存储
现在我们将通过JavaScript的数组来保存区块链,这就意味着当节点结束时,数据不会被保存。
const blockchain: Block[] = [genesisBlock];
验证区块的完整性
在必要时候,我们验证一个区块的完整性或者整个区块链的完整性,我们就要从其他节点接受区块,并选择接受与否。
一个区块的有效性必须满足一下条件
- 块的索引必须是一个数字且比前一个区块的索引大
- 区块的previousHash能匹配前一个块的hash
- 区块本身的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;
};
我们还必须验证区块的结构:
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';
};
到这里我们已经验证了一个单独的区块,现在我们验证整个区块链。首先我们要检验第一个区块==genesisBlock==,后面我们用同意的方法来验证后面所有连续的块。下面是给出的代码。
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;
};
选择最长的区块链
在一定时间内,只有一条链是合法的。在冲突的情况下(例如,有两个节点生成块编号72)我们应该选择最长的区块链。 在以下示例中,区块72:a350235b00不会区块链中,因为它将由长链覆盖。

以下是代码实现:
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');
}
};
节点间的通信
一个节点的基本功能是与其他节点共享并同最新的区块链,下面的规则是用于同步。
- 当一个节点生成一个新的块,将它广播到网络中
- 当一个节点连接到一个新的广播时同步最新的区块块
-
当一个节点遇到一块索引大于当前已知的块,将放弃当前的链并同步最新的链。
image
我们将使用websockets作为点对点通信。 积极为每个节点存储在套接字const sockets: WebSocket[]变量。 不使用自动对等的发现。 同行的位置(= Websocket url)必须手动添加。
控制节点
用户必须通过一些方式控制节点,这是设置一个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);
});
};
可以看到,用户可以通过节点实现以下功能
- 列出所有块
- 创建一个新的块内容
- 列表或添加平行节点
最直接的方式通过curl来控制节点
#get all blocks from the node
> curl http://localhost:3001/blocks
体系结构
应该注意的是,实际上暴露了两个web服务器的节点:一个用于用户控制的节点(HTTP服务器)和一个对等节点之间的通信。 (Websocket HTTP服务器)
[图片上传失败...(image-ddda7f-1523368323591)]
网友评论