BruceFan's Blog

Stay hungry, stay foolish

0%

用Python实现简单的区块链(二)

继续上一篇文章的内容,我们已经有一个可以证实的区块链了,但是现在链中只保存着一些无用的信息,这篇文章中我们将会实现简单的钱包(wallet)交易(transaction),用交易来替换这些数据,创建一个非常简单的加密货币:”NoobCoin”。

创建Wallet

在加密货币中,币的所有权在区块链上通过交易转移,参与者有一个地址来发送和接收币。基本形式的钱包只能保存地址,但大多数钱包是能够在区块链上创建新交易的软件。

上图显示了钱包、交易和区块链的关系,钱包可以创建交易,交易被存储在区块中,一个区块中可以有多个交易。具体的内容下面会详细介绍。
首先创建一个钱包类来保存私钥和公钥,UTXOs的作用后面再介绍:
代码清单 wallet.py

1
2
3
4
5
6
7
8
9
10
import random
from ecdsa.util import PRNG
from ecdsa import SigningKey

class Wallet:
def __init__(self):
rng = PRNG(str(random.random()))
self.privateKey = SigningKey.generate(entropy=rng)
self.publicKey = self.privateKey.get_verifying_key()
self.UTXOs = {}

在noobcoin中,公钥实际上是作为地址使用的,被共享给其他人来接收付款。私钥是用来签名交易的,这样除了私钥的所有者,没有人能花我们的币。公钥也和交易一起发送,用来验证签名是合法的,数据没有被篡改。

这里用的椭圆曲线加密来产生公私钥对,用到了一个第三方库python-ecdsa

交易和签名

每个交易都会包含以下数据:

  • 资金发送者的公钥(地址)
  • 资金接收者的公钥(地址)
  • 转移资金的数目
  • 输入(inputs),引用先前的交易,证明发送者有资金可以发送
  • 输出(outputs),显示了接收资金的所有地址,这些输出在新交易中会被当做输入
  • 加密签名,证明地址的所有者是发送这个交易的人,并且数据没有被篡改

下面创建交易类:
代码清单 transaction.py

1
2
3
4
5
6
7
8
9
10
11
12
13
class Transaction:
def __init__(self, pubkey_from, pubkey_to, value, inputs):
self.transactionId = ""
self.sender = pubkey_from
self.reciepient = pubkey_to
self.value = value
self.inputs = inputs
self.outputs = []
self.sequence = 0

def calculateHash(self):
self.sequence += 1
return StringUtils.sha256(self.sender.to_string() + self.reciepient.to_string() + str(self.value) + str(self.sequence))

在创建产生签名的方法之前,我们需要先在StringUtils类中加一些辅助函数:
代码清单 strutils.py

1
2
3
4
5
6
7
@staticmethod
def applyECDSASig(prikey, inputs):
return prikey.sign(inputs)

@staticmethod
def verifyECDSASig(pubkey, data, signature):
return pubkey.verify(signature, data)

现在我们把签名方法用到Transaction类中,添加一个generateSignature()和verifySignature()方法:
代码清单 transaction.py

1
2
3
4
5
6
7
def generateSignature(self, prikey):
data = self.sender.to_string() + self.reciepient.to_string() + str(self.value)
self.signature = StringUtils.applyECDSASig(prikey, data)

def verifySignature(self):
data = self.sender.to_string() + self.reciepient.to_string() + str(self.value)
return StringUtils.verifyECDSASig(self.sender, data, self.signature)

实际中会签名更多的信息,例如outputs/inputs和timestamp等,但现在我们只签名最少的。一个新的交易添加到一个区块时,矿工将会验证签名。

测试钱包和签名

测试一下上面的代码:
代码清单 noobchain.py

1
2
3
4
5
6
7
8
9
10
11
12
13
from wallet import Wallet
from transaction import Transaction
walletA = Wallet()
walletB = Wallet()

print "Private and public keys: "
print walletA.privateKey.to_pem()
print walletA.publicKey.to_pem()

transaction = Transaction(walletA.publicKey, walletB.publicKey, 5, None);
transaction.generateSignature(walletA.privateKey);
print "Is signature verified"
print transaction.verifySignature()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ python noobchain.py
Private and public keys:
-----BEGIN EC PRIVATE KEY-----
MF8CAQEEGIAnlP0QuZpVjYN8cF1g0VK+QIDhpySdUqAKBggqhkjOPQMBAaE0AzIA
BF5wfHb88KlyjGkVeSSnqge/4Q4vxDIlHkwu5BvrszPLaEHs00DVwFEFdmjp5wkv
vw==
-----END EC PRIVATE KEY-----

-----BEGIN PUBLIC KEY-----
MEkwEwYHKoZIzj0CAQYIKoZIzj0DAQEDMgAEXnB8dvzwqXKMaRV5JKeqB7/hDi/E
MiUeTC7kG+uzM8toQezTQNXAUQV2aOnnCS+/
-----END PUBLIC KEY-----

Is signature verified
True

上面的代码创建了两个钱包,打印了walletA的公私钥。创建了一个交易,用walletA的私钥给它签名。
接下来介绍outputs和inputs,并把交易保存到区块链。

outputs & inputs

加密货币是如何被拥有的

你要拥有一个比特币需要先接收一个比特币,分布式账本并不会真的添加一个比特币给你,减少发送者的一个比特币,发送者会引用以前的交易证明他有一个比特币并作为input,然后创建一个交易output来证明发送了一个比特币到你的地址。(交易的input就是引用先前的交易output作为证明)
你钱包的余额就是所有未使用的地址是你的交易output的总和。
这里我们遵循比特币的惯例,称未使用的交易输出为:UTXO.
下面来创建TransactionInput和TransactionOutput类:
代码清单 transaction.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TransactionInput:
def __init__(self, transactionOutputId):
self.transactionOutputId = transactionOutputId
self.UTXO = None # 包含未使用的交易output

class TransactionOutput:
def __init__(self, pubkey_to, value, parentTransactionId):
self.reciepient = pubkey_to
self.value = value
self.parentTransactionId = parentTransactionId
self.id = StringUtils.sha256(pubkey_to.to_string()+str(value)+parentTransactionId)

def isMine(self, publicKey):
return publicKey == self.reciepient

大致的工作流程:初始钱包手动创建一个交易,给A钱包100个币,这个交易没有input,只有output。output显示接收者为A钱包,金额是100,output会被添加到全局UTXOs,这个交易会被添加到第0个区块,不会被验证。钱包A要给钱包B发送40个币,交易的input就是引用上一笔交易的output,证明自己有足够的币可以发送,交易会有两个output,一个是接收者为B钱包,金额是40,另一个是接收者为A钱包,金额是60(找零)。这两个output会被添加到全局UTXOs,并将交易的input引用的output从全局UTXOs中删除,这笔交易会被添加到区块上。

处理交易

链上的区块可能会接收到很多交易,区块链可能会很长,处理一个新的交易会花费大量的时间,因为我们必须要验证它的inputs。为了解决这个问题,就需要保存一个额外的集合:所有可用来做input的未花费output,因此我们新建一个config.py文件:

1
2
3
4
5
blockchain = []
# 未使用的output
UTXOs = {}
minimumTransaction = 0.1
difficulty = 3

在Transaction类中添加一个processTransaction()方法:
代码清单 transaction.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def processTransaction(self):
if self.verifySignature() == False:
print "#Transaction Signature failed to verify"
return False
# 收集交易input(确保他们是未花费的)
for i in self.inputs:
i.UTXO = config.UTXOs[i.transactionOutputId]
# 输入的和不能小于最小交易额
if self.getInputsValue() < config.minimumTransaction:
print "#Transaction Inputs too small: " + self.getInputsValue()
return False
# 产生交易输出
leftOver = self.getInputsValue() - self.value
self.transactionId = self.calculateHash()
# 发送value给reciptient
self.outputs.append(TransactionOutput(self.reciepient, self.value, self.transactionId))
# 发送找零给sender
self.outputs.append(TransactionOutput(self.sender, leftOver, self.transactionId))
for o in self.outputs:
config.UTXOs[o.id] = o
# 从UTXO删除交易inputs当做花费
for i in self.inputs:
if i.UTXO == None:
continue
del config.UTXOs[i.UTXO.id]
return True

# 返回输入值的和
def getInputsValue(self):
total = 0
for i in self.inputs:
if i.UTXO == None:
continue
total += i.UTXO.value
return total

# 返回输出值的和
def getOutputsValue(self):
total = 0
for o in self.outputs:
total += o.value
return total

我们用这个方法来做一些检查,确保交易是合法的。最后,我们从UTXOs中删除了input引用到的output,意味着一个交易的output只能被用做一次input。
最后再来更新钱包:

  • 收集我们的余额(通过遍历UTXOs列表来检查交易的output是否是自己的)
  • 为我们创建交易

代码清单 wallet.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import config
from transaction import Transaction, TransactionInput

class Wallet:
def __init__(self):
...
# 计算自己的余额
def getBalance(self):
total = 0
for item in config.UTXOs.items():
UTXO = item[1]
if UTXO.isMine(self.publicKey): # 将全局UTXOs中是自己的output添加到自己钱包的UTXOs
self.UTXOs[UTXO.id] = UTXO
total += UTXO.value
return total
# 发送金额给其他人
def sendFunds(self, _recipient, value):
if self.getBalance() < value:
print "#Not Enough funds to send transaction. Transaction Discarded."
return None
inputs = []
total = 0
# 需要引用到的output
for item in self.UTXOs.items():
UTXO = item[1]
total += UTXO.value
inputs.append(TransactionInput(UTXO.id))
if total > value:
break
# 创建交易
newTransaction = Transaction(self.publicKey, _recipient, value, inputs)
newTransaction.generateSignature(self.privateKey)
# 删除引用过的output
for i in inputs:
del self.UTXOs[i.transactionOutputId]
return newTransaction

添加交易到区块上

现在交易系统已经可以运行,我们需要把它实现到区块链上。把区块链上无用的数据替换为一个交易的列表。一个区块上可能会有上千个交易,对于计算hash来说需要包含的太多。因此,我们使用交易的merkle root。在StringUtils类中添加一个产生merkleroot的辅助方法:
代码清单 strutils.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@staticmethod
def getMerkleRoot(transactions):
count = len(transactions)
previousTreeLayer = []
for transaction in transactions:
previousTreeLayer.append(transaction.transactionId)
treeLayer = previousTreeLayer
while count > 1:
treeLayer = []
for i in range(1, len(previous)):
treeLayer.append(sha256(previousTreeLayer[i-1]+previousTreeLayer[i]))
count = len(treeLayer)
previousTreeLayer = treeLayer
merkleRoot = treeLayer[0] if len(treeLayer) == 1 else ""
return merkleRoot

接下来修改Block类:
代码清单 block.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Block:
def __init__(self, previousHash):
self.previousHash = previousHash
self.timeStamp = time.time()
self.transactions = []
self.nonce = 0
self.merkleRoot = ""
self.hash = self.calculateHash()

def calculateHash(self):
return StringUtils.sha256(self.previousHash+str(self.timeStamp)+str(self.nonce)+self.merkleRoot)

def mineBlock(self, difficulty):
self.merkleRoot = StringUtils.getMerkleRoot(self.transactions)
target = '0'*difficulty
while self.hash[0:difficulty] != target:
self.nonce += 1
self.hash = self.calculateHash()
print "Block Mined!!! : " + self.hash
# 将交易添加到这个区块上
def addTransaction(self, transaction):
if transaction == None:
return False
if self.previousHash != "0":
if transaction.processTransaction() != True:
print "Transaction failed to process. Discarded."
return False
self.transactions.append(transaction)
print "Transaction Successfully added to Block"
return True

运行noobcoin

我们需要测试用钱包发送和接收币,更新我们区块链的合法性检查。有许多方法来创建新币,在比特币区块链,矿工可以用一笔交易作为挖到区块的奖励。现在,我们直接释放我们想用到的所有币,在第0个区块(genesis block)。就像比特币,我们将硬编码Genesis区块。
我们来更新noobchain.py文件:

  • 一个Genesis区块,释放100个noobcoin给walletA
  • 一个更新的链合法性检查,将交易考虑在内
  • 一些测试交易,来看看是否运行正常

代码清单 noobchain.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import config
from block import Block
from wallet import Wallet
from transaction import Transaction, TransactionOutput, TransactionInput

walletA = Wallet()
walletB = Wallet()
coinbase = Wallet()

# 将coinbase里的100个coin给A
genesisTransaction = Transaction(coinbase.publicKey, walletA.publicKey, 100, None)
genesisTransaction.generateSignature(coinbase.privateKey)
genesisTransaction.transactionId = "0"
genesisTransaction.outputs.append(TransactionOutput(genesisTransaction.reciepient, genesisTransaction.value, genesisTransaction.transactionId))
config.UTXOs[genesisTransaction.outputs[0].id] = genesisTransaction.outputs[0]

print "Creating and Mining Genesis block... "
genesis = Block("0")
genesis.addTransaction(genesisTransaction)
genesis.mineBlock(config.difficulty)
config.blockchain.append(genesis)

# testing
block1 = Block(genesis.hash)
print "walletA's balance is: %f" % walletA.getBalance()
print "walletA is attempting to send funds (40) to walletB..."
block1.addTransaction(walletA.sendFunds(walletB.publicKey, 40))
block1.mineBlock(config.difficulty)
config.blockchain.append(block1)
print "walletA's balance is: %f" % walletA.getBalance()
print "walletB's balance is: %f" % walletB.getBalance()

block2 = Block(block1.hash)
print "walletA is attempting to send more funds (1000) than it has..."
block2.addTransaction(walletA.sendFunds(walletB.publicKey, 1000))
block2.mineBlock(config.difficulty)
config.blockchain.append(block2)
print "walletA's balance is: %f" % walletA.getBalance()
print "walletB's balance is: %f" % walletB.getBalance()

block3 = Block(block2.hash)
print "walletB is attempting to send funds (20) to walletA..."
block3.addTransaction(walletB.sendFunds(walletA.publicKey, 20))
print "walletA's balance is: %f" % walletA.getBalance()
print "walletB's balance is: %f" % walletB.getBalance()

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python noobchain.py
Creating and Mining Genesis block...
Transaction Successfully added to Block
Block Mined!!! : 000ecc366d3c553e131693c972dc77b7f2bb7c8156aa7125bb10c9e4052d8874
walletA's balance is: 100.000000
walletA is attempting to send funds (40) to walletB...
Transaction Successfully added to Block
Block Mined!!! : 0005af73ad51e2a0fa80444bdd284342c102dc202c9139889f62a3f989d8343d
walletA's balance is: 60.000000
walletB's balance is: 40.000000
walletA is attempting to send more funds (1000) than it has...
#Not Enough funds to send transaction. Transaction Discarded.
Block Mined!!! : 00035606ee8cd61c0afb21c496193fdfc67a17172befd6cae90d7a9410a27d5f
walletA's balance is: 60.000000
walletB's balance is: 40.000000
walletB is attempting to send funds (20) to walletA...
Transaction Successfully added to Block
walletA's balance is: 80.000000
walletB's balance is: 20.000000

我们的钱包现在可以在区块链上发送资金了,我们有了一个本地的加密货币。最后,再加上检查区块链合法性的方法即可:
代码清单 noobchain.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def isChainValid():
hashTarget = '0' * config.difficulty
tempUTXOs = {}
tempUTXOs[genesisTransaction.outputs[0].id] = genesisTransaction.outputs[0]
for i in range(1, len(config.blockchain)):
curblock = config.blockchain[i]
preblock = config.blockchain[i-1]
if curblock.hash != curblock.calculateHash():
print 'Current Hashes not equal'
return False
if preblock.hash != curblock.previousHash:
print 'Previous Hashes not equal'
return False
if curblock.hash[:config.difficulty] != hashTarget:
print '#This block hasnt been mined'
return False
for t in range(len(curblock.transactions)):
curTransaction = curblock.transactions[t]
if not curTransaction.verifySignature():
print '#Signature on Transaction %d is Invalid' % t
return False
if curTransaction.getInputsValue() != curTransaction.getOutputsValue():
print '#Inputs are not equal to outputs on Transaction %d' % t
return False
for i in curTransaction.inputs:
tempOutput = tempUTXOs[i.transactionOutputId]
if tempOutput == None:
print '#Referenced input on Transaction %d is Missing' % t
return False
if i.UTXO.value != tempOutput.value:
print '#Referenced input Transaction %d value is Invalid' % t
return False
del tempUTXOs[i.transactionOutputId]
for o in curTransaction.outputs:
tempUTXOs[o.id] = o
if curTransaction.outputs[0].reciepient != curTransaction.reciepient:
print '#Transaction %d output reciepient is not who it should be' % t
return False
if curTransaction.outputs[1].reciepient != curTransaction.sender:
print '#Transaction %d output change is not sender' % t
return False
print 'Blockchain is valid'
return True

项目完整代码
reference
https://medium.com/programmers-blockchain/creating-your-first-blockchain-with-java-part-2-transactions-2cdac335e0ce
https://github.com/warner/python-ecdsa
https://www.cnblogs.com/fengzhiwu/p/5524324.html