Python-区块链开发实用指南(二)

251 阅读48分钟

Python 区块链开发实用指南(二)

原文:zh.annas-archive.org/md5/E6FBF7D7A6EED49747FB2B635A55F938

译者:飞龙

协议:CC BY-NC-SA 4.0

第二部分:Web3 和 Populus

本节包括一个实际项目,用于学习区块链编程,围绕一个安全的投票应用展开。

本节将涵盖以下章节:

  • 第四章,使用 Web3 与智能合约交互

  • 第五章,Populus 开发框架

  • 第六章,构建一个实用的去中心化应用

第四章:使用 Web3 与智能合约交互

在本章中,您将学习如何以编程方式连接到智能合约。在这里,您将使用 Python 编程语言来执行智能合约中的方法。为了实现这一点,您将使用web3.py库。在上一章中,您构建了一个智能合约并将其部署到以太坊区块链上。您还使用 Vyper 编写了一个智能合约。为了与该智能合约交互,您启动了 Truffle 控制台并输入了许多命令。这些命令被发送到区块链中的智能合约。根据您输入的命令,这可能会读取智能合约的状态或更改状态。在本章中,您将超越 Truffle 控制台。

本章将涵盖以下主题:

  • 去中心化应用介绍

  • Geth

  • 了解web3.py

  • 使用web3.py与智能合约交互

去中心化应用介绍

您将使用 Python 构建一个程序,以编程方式执行智能合约中的方法,我们称这个程序为去中心化应用。因此,有一个智能合约,还有一个去中心化应用。使用 Vyper 或 Solidity 编程语言编写的智能合约存在于以太坊区块链中。这意味着如果您将智能合约部署到以太坊生产区块链上,您的智能合约的字节码将写入每个以太坊节点。因此,如果我们在这个世界上有 1 万个以太坊节点,您的智能合约将被复制 1 万次。

然而,去中心化应用并不存储在以太坊区块链中。它存在于您的计算机中,邻居的计算机中,云中,但它并不存储在区块链上,并且不必像智能合约一样在全世界复制。人们使用各种编程语言构建去中心化应用。在以太坊的情况下,构建去中心化应用的最流行的编程语言是 Node.js 环境中的 Javascript 和 Python。在我们的情况下,我们将使用 Python 来构建去中心化应用。为此,我们需要一个库。在 Javascript 的情况下,我们需要一个web3.js库。在我们的情况下,也就是 Python,我们需要一个web3.py库。所有这些库的名称都包含了 web3 这个词。

人们喜欢把 web3 看作是互联网的第三个版本:一个去中心化的互联网。那么,如果这是第三个版本,你会问第一个和第二个版本是什么?互联网的第一个版本是您用来被动消费内容的互联网(想想静态网站)。互联网的第二个版本是社交互联网,您在其中生成内容并共同创造体验(想想 Facebook、Twitter 或 Instagram):

在前面的屏幕截图中,我们可以看到 Vyper 或 Solidity 字节码存在(复制)于许多以太坊节点(系统)中。但是使用web3库的程序可以存在于单个计算机(例如笔记本电脑或智能手机)中。

安装 web3

话不多说,让我们安装web3库。创建一个带有 Python 3.6 的虚拟环境如下:

$ virtualenv -p python3.6 web3-venv

激活虚拟环境并安装 Vyper 如下:

$ source web3-venv/bin/activate
(web3-venv) $ pip install vyper

然后,使用pip安装web3.py库:

(vyper-venv) $ pip install web3

现在,验证它是否按以下方式工作:

(vyper-venv) $ python
>>> import web3
>>> web3.__version__
'4.8.2'

如果您没有遇到任何错误,那就可以了。让我们使用web3连接到 Ganache 区块链。要做到这一点,首先启动 Ganache,然后返回到您的 Python 命令提示符:

>>> from web3 import Web3, HTTPProvider
>>> w3 = Web3(HTTPProvider('http://localhost:7545'))
>>> w3.eth.blockNumber
0
>>> w3.eth.getBlock('latest')
AttributeDict({'number': 0, 'hash': HexBytes('0x0bbde277e2147d93f12852a370e70e2efe9c66f45db6e80e0cba584508d3ebac'), 'parentHash': HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'), 'mixHash': 
...
...
HexBytes('0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421'), 'stateRoot': HexBytes('0x31740a2d8b535c624aa481ba7d6d696085438037246b7501b4f24f77f94f3994'), 'receiptsRoot': HexBytes('0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421'), 'miner': '0x0000000000000000000000000000000000000000', 'difficulty': 0, 'totalDifficulty': 0, 'extraData': HexBytes('0x'), 'size': 1000, 'gasLimit': 6721975, 'gasUsed': 0, 'timestamp': 1548300279, 'transactions': [], 'uncles': []})

我们在这里所做的是使用web3库连接到 Ganache 区块链。我们可以检索区块链上的信息,比如在这个特定区块链上挖掘了多少个区块。因为我们使用的是 Ganache,一个开发区块链,w3.eth.blockNumber返回0,因为我们在 Ganache 上没有创建任何交易。

Geth

Go EthereumGeth)是用 Go 语言编写的以太坊协议的实现。您可以使用 Geth 来同步以太坊节点,甚至构建私有以太坊区块链。如果您想成为矿工,这是您将使用的软件。您的以太坊节点是以太坊区块链的网关和一部分。您的程序与web3库需要以太坊节点才能与存储在区块链中的智能合约进行交互。

使用 Ganache 是很好的。但 Ganache 是一个假的区块链。没有矿工,因此很难模拟我们在真实以太坊区块链上可能遇到的一些情况。因此,让我们提高我们的游戏水平。我们现在不需要使用以太坊生产区块链,但我们可以使用介于开发和生产区块链之间的东西——Rinkeby 网络。如果以太坊生产区块链类似于生产服务器,那么 Rinkeby 网络就像一个暂存服务器。在 DevOps 的语言中,暂存服务器是尽可能模拟生产服务器的测试服务器。

因此,Rinkeby 不像 Ganache 那样是一个软件。它存在于互联网上。因此,使用 Rinkeby 网络,我们可以感受到与以太坊生产区块链打交道是什么感觉。在以太坊区块链的 Rinkeby 网络中,您可能会遇到的情况之一是确认交易需要时间。在 Ganache 中,确认交易只需要一小部分秒数。在 Rinkeby 网络中,确认交易可能需要 20-30 秒,甚至一分钟,因此您需要习惯。当然,并非以太坊生产区块链中的所有情况都可以在 Rinkeby 网络上复制。在以太坊生产区块链中特别发生的另一种情况是,它包括以太坊生产区块链有时会出现的高流量。例如,一个名为 Cryptokitties 的去中心化应用程序减慢了以太坊网络,因为有许多用户与该应用程序进行交互,可以在这里看到:techcrunch.com/2017/12/03/people-have-spent-over-1m-buying-virtual-cats-on-the-ethereum-blockchain/

还有另一个类似于 Rinkeby 网络的以太坊测试网络——Ropsten 网络。这里的区别在于 Rinkeby 网络在确认交易时使用权威证明PoA),而 Ropsten 网络使用工作量证明PoW)。现在不需要担心这种区别,因为使用 Rinkeby 网络类似于使用 Ropsten 网络。

连接到这种类型的以太坊区块链有两种方法——自己运行以太坊节点,或者使用其他人的节点。每种方法都有其优点和缺点。运行一个以太坊节点需要大量存储空间。连接到 Rinkeby 网络的节点需要大约 6GB 的存储空间。至于以太坊生产网络,需要高达 150GB 的存储空间。根据您的互联网连接,要完全运行,您需要一晚或几天时间,使节点与所有其他节点完全同步。

另一种方法是使用其他人的节点。有些人构建了一个连接到他们的以太坊节点的网络服务,因此您可以使用 API 连接到他们的以太坊节点。其中最受欢迎的服务之一是 Infura。在这里,您只需要在他们的网站上注册以获取他们的 API。

要在 Rinkeby 网络上运行我们自己的以太坊节点,请转到geth.ethereum.org/downloads/下载适用于您的操作系统的软件。对于 Ubuntu Linux,这是以tar.gz格式,因此您需要解压缩它。然后,将二进制文件放在方便的位置(例如/opt/bin/home/yourusername/Program/user/local/bin)。

完成此操作后,按以下方式同步节点:

$ ./geth --rinkeby

你可以使用不同的数据目录。默认情况下,Geth 将数据存储在~/.ethereum目录中:

$ ./geth --rinkeby --datadir /opt/data/ethereumdata

在我的情况下,这需要一个晚上。你的经验可能会因你的互联网连接速度而有所不同。

如果它完全同步(当输出不再经常更改时,你就知道这种情况),那么你可以在web3-venv虚拟环境中运行 Python,如下所示:

(web3-venv) $ python
>>> from web3 import Web3, IPCProvider
>>> w3 = Web3(IPCProvider("/home/yourusername/.ethereum/rinkeby/geth.ipc"))

在这里,我们使用了与之前示例中不同的提供者。在 Ganache 示例中,我们使用 HTTP 提供者。请记住,Ganache 使用http://localhost:7545,你需要在 Truffle 配置中使用这些信息。然而,在我们的情况下,当我们连接到以太坊节点时,我们使用进程间通信提供者IPC)。你还可以看到IPCProvider的一个参数,它是一个文件路径。因此,你的 Python 程序通过那个文件与以太坊节点通信。在计算机科学中,那个文件被称为pipe。你只需搜索geth.ipc文件在你的本地计算机上的位置。请记住,只有在运行geth软件时,geth.ipc才会出现。如果你停止它,geth.ipc文件将消失。

然后,在你像往常一样运行业务之前,你需要向 web3 中间件注入一些东西。这应该这样做,因为以太坊生产区块链中的区块大小与 Rinkeby 区块链中的区块大小不同:

>>> from web3.middleware import geth_poa_middleware
>>> w3.middleware_stack.inject(geth_poa_middleware, layer=0)

然后,你可以测试它,如下面的代码块所示:

>>> w3.eth.getBlock('latest')
AttributeDict({'difficulty': 2, 'proofOfAuthorityData': HexBytes('0xd883010813846765746888676f312e31312e32856c696e7578000000000000001c62ac5af9b2ea6bf897a99fff40af6474cd5680fc8239853f03db116b2154594d2ab77a6f18c41132ee819143d2d41819237468924d29cb4b1252d2385a862400'), 'gasLimit': 7000000, 'gasUsed': 1373640, 'hash': HexBytes('0xa14b569f874eefc75fe734bc28b7457755eff1da26794d6615f15e1739204067'), 'logsBloom': 
...
...
HexBytes('0x66e75c91271b45f5271d2fe2fd0efc66f48f641632e83a086fc57646a0c0bc3f'), 'uncles': []})

你得到的输出是 Rinkeby 网络区块链的最新区块的信息。你可以从区块链的区块中学到一些东西。你可以找到在这个区块中已确认的所有交易;使用的燃气,燃气限制等等。在 Rinkeby 网络中,矿工始终是零地址(0x0000000000000000000000000000000000000000),因为 Rinkeby 网络中的区块链使用权威证明。但在主网(生产网络)中,你可以找出谁获得了确认区块链的奖励。你可以从主网(以太坊生产网络)的最新区块中找到这些信息。当然,如果你愿意同步节点,你也可以从以太坊生产节点中找到相同的信息。

Geth 控制台

在我们继续使用web3库之前,让我们先尝试一下 Geth 软件。Geth 软件可以像 Truffle 控制台一样工作:

$ ./geth --rinkeby --verbosity 0 console

在该语句中,关键字是console,但为了使体验更愉快,你应该添加另一个--verbosity标志,值为0。这将防止你从geth软件中获得大量输出。

Welcome to the Geth JavaScript console!
instance: Geth/v1.8.16-stable-477eb093/darwin-amd64/go1.11
modules: admin:1.0 clique:1.0 debug:1.0 eth:1.0 miner:1.0 net:1.0 personal:1.0 rpc:1.0 txpool:1.0 web3:1.0
>

在 Geth 控制台中,你可以做任何你在 Truffle 控制台中可以做的事情。然而,我们现在想要创建一个以太坊账户。当你启动 Ganache 时,你会得到 10 个可以使用的账户。但在 Rinkeby 区块链中情况并非如此。你需要在 Rinkeby 中手动创建一个账户:

> personal.newAccount("password123")
"0x28f5b56b035da966afa609f65fd8f7d71ff68327"

这是创建一个新的以太坊账户的命令。你需要提供一个密码来在 Geth 控制台中创建一个账户。不要忘记这个账户的密码,因为没有恢复它的选项。该命令的输出是你账户的公共地址。

私钥被加密在以下目录的文件中:/home/yourusername/.geth/rinkeby/keystore

文件名类似于UTC—2018-10-12T09-30-20.687898000Z—28f5b56b035da966afa609f65fd8f7d71ff68327。这是时间戳和公钥的组合。你可以打开它,但你将无法在其中找到私钥。

{"address":"28f5b56b035da966afa609f65fd8f7d71ff68327","crypto":{"cipher":"aes-128-ctr","ciphertext":"38b091f59f879369a6afdd91f21c1a82deb59374677144c94dd529d3c9069d39","cipherparams":{"iv":"b168482d467df6e1fe4bdb5201a64a6a"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"bd94440d3f2bb9313a0020331bac9410ff3cdc9f32756f41f72dde1ef7bf32e1"},"mac":"3313b72603e85e73f84a47ef7ed0e931db85441e1702e0d96f2f001c54170cb6"},"id":"7a033367-92fe-42d3-bec5-970076f35d8a","version":3}

要解密它,你可以使用web3库。将脚本命名为extract_private_key.py

from web3 import Web3
w3 = Web3()

# Change the filepath to your keystore's filepath
with open('/opt/data/ethereumdata/keystore/UTC--2018-10-12T09-30-20.687898000Z--28f5b56b035da966afa609f65fd8f7d71ff68327') as keyfile:
    encrypted_key = keyfile.read()
    private_key = w3.eth.account.decrypt(encrypted_key, 'password123')
    print(private_key)

如果你执行该脚本,你将看到你的私钥,它可以在其他情况下使用:

(web3-venv) $ python extract_private_key.py
b'\xa0\xe2\xa2\xf0$j\xe9L\xb3\xc0\x14Q\xb0D\xec\xa16\xa1\xca\xdd\x07.\x0f\x0f=5\xbd\xc5mb(r'

请不要再在生产环境中使用这个账户,因为私钥已经暴露。只能用于开发目的。

此私钥以bytes格式。如果要将其转换为十六进制字符串,可以这样做:

(web3-venv) $ python
>>>  b'\xa0\xe2\xa2\xf0$j\xe9L\xb3\xc0\x14Q\xb0D\xec\xa16\xa1\xca\xdd\x07.\x0f\x0f=5\xbd\xc5mb(r'.hex()
'a0e2a2f0246ae94cb3c01451b044eca136a1cadd072e0f0f3d35bdc56d622872'

了解 web3.py 库

现在,让我们使用这个库编写一个去中心化的应用程序。最简单的去中心化应用程序脚本将是从一个账户向另一个账户发送资金。将脚本命名为send_money_ganache.py

from web3 import Web3, HTTPProvider

w3 = Web3(HTTPProvider('http://localhost:7545'))

private_key = '59e31694256f71b8d181f47fc67914798c4b96990e835fc1407bf4673ead30e2'

transaction = {
  'to': Web3.toChecksumAddress('0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5'),
  'value': w3.toWei('1', 'ether'),
  'gas': 100000,
  'gasPrice': w3.toWei('1', 'gwei'),
  'nonce': 0
}

signed = w3.eth.account.signTransaction(transaction, private_key)
tx = w3.eth.sendRawTransaction(signed.rawTransaction)

在执行此脚本之前,首先启动 Ganache。这样做后,选择任何您喜欢的公共地址,并将其放入交易字典中的to字段。这个账户将成为接收者。然后找到另一个账户,查看其私钥,并将值输入到private_key变量中:

value字段中放入一个以太。这意味着您想向另一个账户发送 1 个以太。这是一个简单的脚本,用来说明如何发送交易。它并不展示最佳实践,因为您不应该像这样将私钥嵌入代码中。例如,您可以从受限制的权限文件中读取私钥,或者可以从标准输入请求私钥。

如果您执行此脚本,您会注意到接收者的余额会增加 1 个 ETH,而发送者的余额会减少 1 个 ETH:

以下是输出:

Gas 和 gas 价格

正如您所知,以太坊区块链并不是免费的;有人必须维护它。我不是在谈论编写以太坊软件的开发人员,而是运行以太坊节点以确认交易的矿工。他们从以太坊软件本身获得 ETH 的报酬。此外,他们还从交易的费用中获得报酬。这个费用就是 gas 和 gas 价格。

为什么以太坊软件需要在奖励之上收取费用?这是为了防止用户滥发垃圾信息。如果交易是免费的,滥发者可以设置两个账户,之间来回发送资金。此外,这会给愿意支付更多的用户高优先级。如果有两个类似的交易,但第一笔交易使用的 gas 更多,它将在矿工的待办事项列表中具有更高的优先级。使用更少 gas 的交易最终会得到确认;它只需要等待更长一点时间。

因此有 gas 和 gas 价格。Gas 是您愿意在此交易中分配的 gas 数量。在先前的脚本中,您分配了 20,000 gas 来创建一个发送资金的交易。对于更复杂的交易,比如执行智能合约中的复杂方法,可能需要更多的 gas。如果您没有分配足够的 gas,您的交易将被拒绝,您也可能会损失 gas。但是,如果您放入的 gas 超过了所需量,如果您的交易成功,剩余的 gas 将退还给您。因此,您可能会想:为什么不尽可能多地设置 gas?有一个陷阱。如果一些方法在智能合约中失败了断言(例如:assert 1 == 2),您将失去直到断言行之前使用的所有 gas(但剩余的 gas 将被退还)。因此,您需要找到折中之道。

Gas 价格是 gas 的价格,因此 gas 不是免费的。它与以太坊本身分开。您可以用您拥有的 ETH 购买 gas。您可以在历史交易中查看 gas 价格。在以太坊生产区块链中,您可以在www.ethgasstation.info/上查看 gas 价格。

您如何估计交易所需的 gas?您需要了解 Solidity 或 Vyper 编程语言的所有复杂性。如果我分配一个 256 位整数变量并将其存储在存储器中,会花费多少?循环呢?构造一个结构呢?这听起来很复杂,但幸运的是,web3库有一个估算 gas 使用量的方法。首先,创建一个名为estimate_gas.py的脚本:

from web3 import Web3, HTTPProvider

w3 = Web3(HTTPProvider('http://localhost:7545'))

transaction = {
  'to': Web3.toChecksumAddress('0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5'),
  'value': w3.toWei('1', 'ether'),
  'gas': 100000,
  'gasPrice': w3.toWei('1', 'gwei'),
  'nonce': 0
}

print("Estimating gas usage: " + str(w3.eth.estimateGas(transaction)))
print("Gas price: " + str(w3.eth.gasPrice))

您将得到以下输出:

Estimating gas usage: 21000
Gas price: 2000000000

如果出现与随机数相关的错误,请将随机数更改为 1 或更高,直到它起作用。我们将在下一节讨论随机数。

随机数

您可能已经注意到,如果尝试多次执行发送资金脚本,会出现与随机数相关的错误。如果您还没有尝试,请尝试一下。您必须增加随机数才能使其再次工作。随机数就像是您使用一个账户进行了多少笔交易的指示器。对于第一笔交易(在创建新账户后),您在随机数中放入零值。然后,对于第二笔交易,您在随机数中放入 1 的值。然后,对于第三笔交易,您在随机数中放入 2 的值。

但是跟踪随机数值变得愚蠢,特别是如果您想使用一个旧账户,而您不知道随机数值有多高。幸运的是,有一种方法可以从以太坊区块链中获取最新的随机数值。创建一个名为get_latest_nonce.py的脚本:

from web3 import Web3, HTTPProvider
w3 = Web3(HTTPProvider('http://localhost:7545'))
transaction_count = w3.eth.getTransactionCount("0xcc6d61988CdcF6eB510BffAeD4FC0d904f8d3e7D")
print(transaction_count)

尝试使用更高的随机数再次发送资金,然后执行此脚本。再做一次。这将显示给您价值总额。

有了这些新知识,您必须小心创建以太坊区块链上的交易。在现实世界中,交易可能需要时间才能得到确认。想象一下,您同时创建了两笔不同的交易,它们的随机数相同。如果这两笔交易都有效,只有其中一笔会被记录在以太坊区块链上。另一笔将被拒绝,因为存在随机数问题。这两笔交易中的哪一笔将得到确认?这将是相当随机的。

相反,您必须决定要先确认哪笔交易。给它一个较低的随机数。对于第二笔交易,您给予一个较高的随机数。但是,如果第一笔交易失败,第二笔交易将被拒绝,因为随机数太高(随机数跳过一个数字)。这是您需要牢记的事情。

那么为什么要有这些随机数官僚主义?这是为了防止相同的交易再次发生。想象一下,您广播了一笔交易,向您的邻居发送了 1 个 ETH。我可以复制这笔交易并再次广播。由于这笔交易是通过您的签名验证的,我可以清空您的账户。

在 Rinkeby 上创建交易

现在您已经在 Ganache 上玩得很开心了,让我们尝试在 Rinkeby 网络上创建一笔交易。您可能会注意到一个问题——与 Ganache 不同,您在这里什么都没有。您必须默认创建一个账户。创建 10 个账户很容易。但是余额呢?您在 Rinkeby 网络上使用 Geth 软件创建的每个账户都带有 0 ETH 的余额。但是发送资金需要资金来支付交易费。

在以太坊生产区块链中,您可以从加密货币交易所或挖矿中获得 ETH。但在 Rinkeby 网络中,您通过乞讨来获得资金。以下是如何做到这一点。

转到faucet.rinkeby.io/,然后使用 Twitter、Google+或 Facebook 等社交媒体平台之一,在 Rinkeby 网络中创建一个包含您的公共地址的帖子。然后,在 Rinkeby 水龙头网站的表单中发布您的社交媒体帖子。您有三个选择:8 小时内的 3 个以太币,1 天内的 7.5 个以太币,或 3 天内的 18.5 个以太币:

为了确保您获得了余额,您可以从 Geth 软件中检查余额。首先同步它。正如我之前所说,这个过程可能需要很长时间,也许几个小时,或者在我这种情况下需要一整夜:

$ ./geth --rinkeby

在您的本地区块链节点与 Rinkeby 网络完全同步之后,首先终止geth进程,然后再次启动geth,但使用不同的标志:

$ ./geth --rinkeby --verbosity 0 console

geth控制台中执行此命令:

> web3.eth.getBalance('0x28f5b56b035da966afa609f65fd8f7d71ff68327')
3000000000000000000

将此地址更改为您的地址。您应该从水龙头中获得一些 ETH。

假设您已经拥有 ETH,您可以在 Rinkeby 网络中创建一个交易。以下是在 Rinkeby 网络中发送以太币的脚本。您可以在以下 GitLab 链接上引用完整代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_04/send_money_rinkeby.py

from web3 import Web3, IPCProvider
from web3.middleware import geth_poa_middleware

# Change the path of geth.ipc according to your situation.
w3 = Web3(IPCProvider('/opt/data/ethereumdata/geth.ipc'))

w3.middleware_stack.inject(geth_poa_middleware, layer=0)

...
...

nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(from_account))

transaction = {
  'to': Web3.toChecksumAddress(to_account),
  'value': w3.toWei('1', 'ether'),
  'gas': 21000,
  'gasPrice': w3.toWei('2', 'gwei'),
  'nonce': nonce
}

signed = w3.eth.account.signTransaction(transaction, private_key)
w3.eth.sendRawTransaction(signed.rawTransaction)

根据您的情况,更改接收账户地址、您的私钥加密文件位置、您的密码和geth.ipc文件位置。

请记住,我们的私钥是加密在一个文件中的。因此我们读取该文件,然后用密码解锁。请记住,您不应该直接将密码嵌入代码中。然后您可以在等待几分钟后在geth控制台中检查您目标账户的余额:

> web3.eth.getBalance('0x99fb2eee85acbf878d4154de73d5fb1b7e88c328')
100000000000000000

您可以通过使用私钥对其进行签名来发送交易。这是在以太坊中创建交易的最通用方式。但还有另一种方式,涉及仅使用密码。

您可以像这样使用私钥:

signed = w3.eth.account.signTransaction(transaction, private_key)
w3.eth.sendRawTransaction(signed.rawTransaction)

或者,您可以在签署交易时使用密码,如下所示:

w3.personal.sendTransaction(transaction, password)

只有在您控制节点时才能使用密码,因为它需要一个加密的私钥文件。我在我的以太坊节点中创建了一些账户。我只能在为这些账户签署交易时使用密码。但是使用私钥,我可以使用任何账户。

使用 web3.py 与智能合约进行交互

您已经使用web3库在 Ganache 和 Rinkeby 网络中使用 Python 脚本发送了以太币。现在,让我们创建一个与智能合约交互的脚本。但在这之前,您需要学习如何使用gethweb3库的 Python 脚本启动智能合约。在第三章中,使用 Vyper 实现智能合约,您使用 Truffle 启动了一个智能合约。

使用 Geth 启动智能合约

在下一节中,我们将使用web3连接到一个智能合约。以下是如何将智能合约部署到 Rinkeby 区块链的方法:

$ ./geth --rinkeby --verbosity 0 console

geth控制台中,使用 Geth 软件列出所有您的账户:

> eth.accounts
["0x8b55f0a88a1c53a8976953cde4f141752e847a00", "0x1db565576054af728b46ada9814b1452dd2b7e66", "0x28f5b56b035da966afa609f65fd8f7d71ff68327", "0x5b0d65b07a61c7b760bf372bbec1b3894d4b0225", "0x99fb2eee85acbf878d4154de73d5fb1b7e88c328"]

所有这些账户都来自您使用此命令创建的 keystore 文件:personal.newAccount("password")。假设您想要解锁第一个账户,那么您可以使用personal.unlockAccount方法:

> personal.unlockAccount(eth.accounts[0], "password123")
true

现在,获取bytecode并将其放入一个变量中。请记住,当您使用 Vyper 编译器编译源代码时,您会得到bytecode

> bytecode = "smart contract bytecode"
> tx = eth.sendTransaction({from: eth.accounts[0], data: bytecode, gas: 500e3}

然后,检查您的智能合约是否已在区块链上得到确认:

> web3.eth.getTransactionReceipt(tx)

如果已确认,则应该获得以下输出:

{
 blockHash: "0xfed7dcbd5e8c68e17bff9f42cd30d95588674497ae719a04fd6a2ff219bb001d",
 blockNumber: 2534930,
 contractAddress: "0xbd3ffb07250634ba413e782002e8f880155007c8",
 cumulativeGasUsed: 1071323,
 from: "0x1db565576054af728b46ada9814b1452dd2b7e66",
 gasUsed: 458542,
 logs: [],
 logsBloom: "0x00000...",
 status: "0x1",
 to: null,
 transactionHash: "0x1a341c613c2f03a9bba32be3c8652b2d5a1e93f612308978bbff77ce05ab02c7",
 transactionIndex: 4
}

使用 web3 启动智能合约

您还可以使用web3库使用 Python 脚本启动智能合约。将此脚本命名为deploy_smart_contract_to_ganache.py。您可以在以下 GitLab 链接上引用完整代码文件,gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_04/deploy_smart_contract_to_ganache.py

from web3 import Web3, HTTPProvider
from vyper import compile_codes

contract_source_code = '''
name: public(bytes[24])

@public
def __init__():
    self.name = "Satoshi Nakamoto"

...
...

# Change the account to your situation.
tx_hash = HelloSmartContract.constructor().transact({'from': '0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77'})

tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash)
print(tx_receipt)

运行脚本。但请确保 Ganache 正在运行。您应该获得以下输出:

AttributeDict({'transactionHash': HexBytes('0xcfce0a28d0f8232735f99bcf871762f9780f19ab916e92c03d32fdabfd6b9e9a'), 'transactionIndex': 0, 'blockHash': HexBytes('0x84139a5c9ad050cf7be0678feb4aefc9e8b2806636245f16c790048e50347dfe'), 'blockNumber': 1, 'from': '0xb105f01ce341ef9282dc2201bdfda2c26903da77', 'to': None, 'gasUsed': 339198, 'cumulativeGasUsed': 339198, 'contractAddress': '0x9Dc44aa8d05c86388E647F954D00CaA858837804', 'logs': [], 'status': 1, 'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), 'v': '0x1c', 'r': '0x74c63921055bd2fed65a731356b30220c6de3a28ec5fd26e296bf609d76d25ce', 's': '0x655395f422fa7b419caf87f99e2da09296b123eceb99aed4d19195e542b01bcd'})

首先,使用此语句创建一个智能合约对象:

HelloSmartContract = w3.eth.contract(abi=abi, bytecode=bytecode)

然后,为了部署一个智能合约,您只需要使用constructor方法:

tx_hash = HelloSmartContract.constructor().transact({'from': '0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77'})

最后一步非常重要,那就是等待交易得到确认。这很重要,因为您需要在交易得到确认后获得智能合约的地址。

如果要将智能合约部署到 Rinkeby 网络,需要修改此脚本。创建一个名为deploy_smart_contract_to_rinkeby.py的新文件。您可以在以下 GitLab 链接上参考完整代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_04/deploy_smart_contract_to_rinkeby.py

from web3 import Web3, IPCProvider
from vyper import compile_codes

contract_source_code = '''
name: public(bytes[24])

...
...

signed = w3.eth.account.signTransaction(transaction, private_key)
tx_hash = w3.eth.sendRawTransaction(signed.rawTransaction)

tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash)
print(tx_receipt)

将智能合约部署到 Rinkeby 网络时的区别(除了使用IPCProvider而不是HTTPProvider)是您还需要设置gasgasPricenonce。此外,您使用buildTransaction方法,获取transaction对象,并使用私钥对其进行签名。因为此脚本在 Rinkeby 网络中运行,所以需要确保您已分配足够的 gas。在 Rinkeby 网络上部署智能合约时,犯的一个常见错误是 gas 不足。然后,开发人员会困惑为什么之后无法访问智能合约。当您将此智能合约部署到 Rinkeby 网络时,需要等待一段时间。

玩转智能合约

在上一章中,我们使用 Vyper 开发了一个名为hello.vy的简单智能合约。让我们使用web3创建一个与此智能合约交互的脚本。如果您忘记了hello.vy的内容,这是文件的内容:

name: public(bytes[24])

@public
def __init__():
    self.name = "Satoshi Nakamoto"

@public
def change_name(new_name: bytes[24]):
    self.name = new_name

@public
def say_hello() -> bytes[32]:
    return concat("Hello, ", self.name)

编译并部署到 Ganache 或 Rinkeby 网络。现在,根据您是要连接到 Ganache 还是 Rinkeby 中的智能合约,选择以下选项之一。

第一个脚本是用于与 Rinkeby 网络中的智能合约进行交互。将脚本命名为play_with_smart_contract_in_rinkeby.py。您可以在以下 GitLab 链接上参考完整代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_04/play_with_smart_contract_in_rinkeby.py

from web3 import Web3, IPCProvider
from vyper import compile_codes

contract_source_code = '''
name: public(bytes[24])

...
...

signed_txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction)

w3.eth.waitForTransactionReceipt(signed_txn_hash)

print(Hello.functions.say_hello().call())

第二个脚本是用于与 Ganache 中的智能合约进行交互。将脚本命名为play_with_smart_contract_in_ganache.py。您可以在以下 GitLab 链接上参考完整代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_04/play_with_smart_contract_in_ganache.py

from web3 import Web3, HTTPProvider
from vyper import compile_codes

contract_source_code = '''
name: public(bytes[24])

...
...

signed_txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction)

w3.eth.waitForTransactionReceipt(signed_txn_hash)

print(Hello.functions.say_hello().call())

我们将逐行讨论代码以更好地理解概念:

from web3 import Web3, IPCProvider
from vyper import compile_codes

contract_source_code = '''
name: public(bytes[24])

...
...

smart_contract = {}
smart_contract['hello'] = contract_source_code

format = ['abi', 'bytecode']
compiled_code = compile_codes(smart_contract, format, 'dict')

abi = compiled_code['hello']['abi']

这部分脚本旨在获取智能合约的abibytecode

# Change the path of geth.ipc according to your situation.
w3 = Web3(IPCProvider('/opt/data/ethereumdata/geth.ipc'))

from web3.middleware import geth_poa_middleware
w3.middleware_stack.inject(geth_poa_middleware, layer=0)

# Change the address of the smart contract, the account, the password, and the path to the keystore according to your situation,
address = "0x58705EBBc791DB917c7771FdA6175b2D9F59D51A"
password = 'password123'
w3.eth.defaultAccount = '0x28f5b56b035da966afa609f65fd8f7d71ff68327'
with open('/opt/data/ethereumdata/keystore/UTC--2018-10-12T09-30-20.687898000Z--28f5b56b035da966afa609f65fd8f7d71ff68327') as keyfile:
    encrypted_key = keyfile.read()
    private_key = w3.eth.account.decrypt(encrypted_key, password)

Hello = w3.eth.contract(address=address, abi=abi)

此脚本适用于 Rinkeby 网络选项。您获取web3连接对象和私钥。然后,根据部署智能合约脚本中的abi和地址初始化智能合约对象:

w3 = Web3(HTTPProvider('http://localhost:7545'))

# Change the address of the smart contract, the private key, and the account according to your situation
address = "0x9Dc44aa8d05c86388E647F954D00CaA858837804"
private_key = '0x1a369cedacf0bf2f5fd16b5215527e8c8767cbd761ebefa28d9df0d389c60b6e'
w3.eth.defaultAccount = '0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77'

Hello = w3.eth.contract(address=address, abi=abi)

此脚本适用于 Ganache 选项。您获取web3连接对象并设置私钥。然后,根据部署智能合约脚本中的abiaddress初始化智能合约对象。

在两个(Ganache 和 Rinkeby)脚本中,将值设置为w3.eth.defaultAccount。如果将地址设置为w3.eth.defaultAccount,这意味着该地址将广播交易。它还将执行智能合约中的方法。如果您还记得在 Truffle 控制台中所做的,您使用from参数指定将执行智能合约中的方法的账户,如下所示:

Donation.at("0x3e9417399786347b6ab38f59d3f00829d6bba7b8").change_useless_variable("sky is blue", {from: "0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77" });

如果不使用默认账户,则在构建交易时需要指定from字段。

print(Hello.functions.name().call())

print(Hello.functions.say_hello().call())

要获取公共变量或不改变智能合约状态的公共方法的值,您需要使用合约对象的functions方法,然后是公共变量和公共方法(两者都必须使用()执行),最后执行call方法:

nonce = w3.eth.getTransactionCount(w3.eth.defaultAccount)

txn = Hello.functions.change_name(b"Vitalik Buterin").buildTransaction({
  'gas': 70000,
  'gasPrice': w3.toWei('1', 'gwei'),
  'nonce': nonce
})

如果您记得如何处理 nonce,那么有必要获取更新后的 nonce。对于将改变智能合约状态的交易,您需要使用buildTransaction,它提供了您已经认识的参数:gasgasPricenonce。如果您不使用w3.eth.defaultAccount,那么您需要在这里添加另一个参数:from。如果您想向智能合约发送一些以太币(例如 Donation 智能合约中的donate方法),您还需要添加另一个参数:value

如果您注意到,Rinkeby 网络脚本中的gasgasPrice参数要高得多:

txn = Hello.functions.change_name(b"Lionel Messi").buildTransaction({
        'gas': 500000,
        'gasPrice': w3.toWei('30', 'gwei'),
        'nonce': nonce
      })

在 Ganache 中,您可以使用 70,000 gas 和1 gwei的燃气价格。然而,在 Rinkeby 网络中,您必须小心。为了安全起见,在与 Rinkeby 网络中的智能合约交互时,我提高了燃气和燃气价格。如果您在 Rinkeby 网络中未能改变智能合约的状态,有时意味着您没有分配足够的燃气,燃气价格也不够高:

signed_txn = w3.eth.account.signTransaction(txn, private_key=private_key)

您使用私钥对此交易进行签名。然而,在 Ganache 中,您不必这样做。相反,您可以直接进行交易而无需私钥:

Hello.functions.change_name(b"Vitalik Buterin").transact()

相比之下,在 Rinkeby 网络或以太坊生产区块链中,您必须对交易进行签名:

signed_txn_hash = w3.eth.sendRawTransaction(signed_txn.rawTransaction)

然后,您广播您的交易:

w3.eth.waitForTransactionReceipt(signed_txn_hash)

在 Ganache 中,方法的执行速度会非常快,但在 Rinkeby 中可能需要几分钟。在一个合适的去中心化应用程序中,您可以使用异步编程或线程来处理这个问题:

print(Hello.functions.say_hello().call())

最后一行是为了确保它已经改变了智能合约中的name变量。

总结

在本章中,您学会了如何安装web3库。这个库是设计用来连接智能合约的。除此之外,您还学会了如何在 Rinkeby 网络上运行以太坊节点。您配置了web3以连接到 Rinkeby 网络上的以太坊区块链。您还学会了如何告诉web3连接到以太坊测试网络,比如 Ganache。此外,您创建了一个脚本,用于从一个账户发送以太币到另一个账户。最后,您创建了一个脚本来执行智能合约的方法,无论是读取公共变量的值还是改变智能合约的状态。

在下一章中,您将使用一个名为Populus的智能合约开发框架,它会处理与智能合约相关的手动工作,比如编译代码和部署代码。此外,Populus 框架提供了一种集成的方式来测试智能合约。

第五章:Populus 开发框架

在本章中,您将学习如何使用 Populus,这是一个智能合约开发框架。Populus 与 Truffle 一样,是一个旨在使您更容易开发智能合约的工具。如果我们回顾一下第三章,使用 Vyper 实现智能合约,您可能还记得我们必须手动创建一个.json文件,然后将控制台中的abi输出复制到.json文件中。使用 Populus,您可以避免重复执行先前执行过的手动工作。

本章将探讨以下主题:

  • 设置 Populus

  • 智能合约单元测试

  • 使用 Populus 部署智能合约

设置 Populus

Populus 是一个智能合约开发框架,就像 Truffle 一样。那么为什么我们应该使用 Populus 而不是 Truffle 呢?基本上,这是个人选择。Populus 是用 Python 编写的,并默认支持 Python 进行单元测试。如果您使用 Truffle,则默认使用 JavaScript 进行单元测试。这就像选择web3.py(Python)或web3.js(JavaScript)来构建去中心化应用程序。

要设置 Populus,请遵循以下步骤:

  1. 从头开始创建一个虚拟环境:
$ virtualenv -p python3.6 populus-venv
$ source populus-venv/bin/activate
  1. 我们安装web3populusvyper
(populus-venv) $ pip install eth-abi==1.2.2
(populus-venv) $ pip install eth-typing==1.1.0
(populus-venv) $ pip install py-evm==0.2.0a33
(populus-venv) $ pip install web3==4.7.2
(populus-venv) $ pip install -e git+https://github.com/ethereum/populus#egg=populus

我们必须安装特定版本的web3的原因是因为最新版本的web3(4.8.2)会破坏 Populus。

  1. 安装 Solidity 编译器:
(populus-venv) $ python
>>> from solc import install_solc
>>> install_solc('v0.4.25')
  1. 创建一个符号链接:
(populus-venv) $ ln -s /home/yourusername/.py-solc/solc-v0.4.25/bin/solc populus-venv/bin/
  1. 创建一个项目目录:
(populus-venv) $ mkdir populus_tutorial
(populus-venv) $ cd populus_tutorial
  1. 使用populus初始化此项目目录:
(populus_venv) $ populus init

这个命令就像truffle init。学习了populus之后,您可能甚至不再需要这个命令。这个命令将在您的项目目录中放置两个目录—contractstests

contracts中,有一个示例 Solidity 文件:Greeter.sol。在tests目录中,有一个示例测试文件:test_greeter.py

打开Greeter.sol,一个简单的智能合约,如下所示:

pragma solidity ⁰.4.0;

contract Greeter {
    string public greeting;

    // TODO: Populus seems to get no bytecode if `internal`
    function Greeter() public {
        greeting = 'Hello';
    }

    function setGreeting(string _greeting) public {
        greeting = _greeting;
    }

    function greet() public constant returns (string) {
        return greeting;
    }
}

在您可以编译智能合约之前,您必须创建一个名为project.json的项目配置。您必须首先进入项目目录:

(populus-venv) $ cp ../populus-
   venv/src/populus/populus/assets/defaults.v9.config.json project.json

populus-venv是您创建虚拟环境的地方。如果您看一下,它是一个重要的文件,包含 255 行。您可以使用以下命令编译它:

(populus-venv) $ populus compile

编译的结果是build/contracts.json。您可以在该.json文件中找到abibytecode。除了abibytecode之外,该.json文件中还有其他信息,例如编译器版本。

您已经熟悉了智能合约开发的工作流程。编译Solidity/Vyper文件,以便我们可以获得abibytecode。然后,使用web3库的abibytecode

现在,您将遇到一些新的东西。有另一个目录是您还没有见过的:tests目录。在该目录中,有一个名为test_greeter.py的测试文件,这是一个单元测试。在这里看一下:

def test_greeter(chain):
    greeter, _ = chain.provider.get_or_deploy_contract('Greeter')

    greeting = greeter.call().greet()
    assert greeting == 'Hello'

def test_custom_greeting(chain):
    greeter, _ = chain.provider.get_or_deploy_contract('Greeter')

    set_txn_hash = greeter.transact().setGreeting('Guten Tag')
    chain.wait.for_receipt(set_txn_hash)

    greeting = greeter.call().greet()
    assert greeting == 'Guten Tag'

您可以按以下方式执行此测试文件:

(populus-venv) $ py.test tests

这将给您以下输出:


============================= test session starts ==============================
platform linux -- Python 3.6.6, pytest-3.9.1, py-1.7.0, pluggy-0.8.0
rootdir: /tmp/pop_tut, inifile:
plugins: populus-2.2.0
collected 2 items                                                              

tests/test_greeter.py ..                                                [100%]

=============================== warnings summary ===============================

...

===================== 2 passed, 3 warnings in 0.88 seconds =====================

您的测试现在将通过。我们将在本章后面更深入地讨论智能合约单元测试。

添加对 Vyper 的支持

正如您所知,我们在本书中并不专注于 Solidity,而是 Vyper。我们需要为 Vyper 添加支持。首先,因为这是一个新的虚拟环境,所以安装vyper

(populus-venv) $ pip install vyper

然后,在项目目录中,在contracts目录中创建一个名为Greeter.vy.vy文件:

greeting: bytes[20]

@public
def __init__():
    self.greeting = "Hello"

@public
def setGreeting(x: bytes[20]):
    self.greeting = x

@public
def greet() -> bytes[20]:
    return self.greeting

在您可以编译这个 Vyper 代码之前,您需要在project.json文件中更改一些内容。

转到compilation键。该键的值是一个具有这些键的对象:backendbackendscontract_source_dirsimport_remappings。删除backends键,然后将backend键更改为以下代码:

"backend": {
      "class": "populus.compilation.backends.VyperBackend"
},

因此,compilation键的内容如下:

  "compilation": {
    "backend": {
      "class": "populus.compilation.backends.VyperBackend"
    },
    "contract_source_dirs": [
      "./contracts"
    ],
    "import_remappings": []
  },

然后,像往常一样运行编译:

(populus-venv) $ populus compile

确保这确实是一个 Vyper 编译,而不是 Solidity 编译,你可以打开build/contracts.json。在里面,你将能够看到以下内容:

{
  "Greeter": {
    …
    "source_path": "contracts/Greeter.vy"
  }
}

Vyper 的最新版本是 0.1.0b6,它破坏了 Populus。开发者需要一些时间来修复这个问题。如果在你阅读本书时这个 bug 还没有被修复,你可以自己修补 Populus。

首先,使用以下命令检查 bug 是否已经修复:

(populus-venv) $ cd populus-venv/src/populus
(populus-venv) $ grep -R "compile(" populus/compilation/backends/vyper.py
 bytecode = '0x' + compiler.compile(code).hex()
 bytecode_runtime = '0x' + compiler.compile(code, bytecode_runtime=True).hex()

在我们的情况下,这个 bug 还没有被修复。所以,让我们修补 Populus 来解决这个 bug。确保你仍然在同一个目录下(populus-venv/src/populus):

(populus-venv) $ wget https://patch-diff.githubusercontent.com/raw/ethereum/populus/pull/484.patch
(populus-venv) $ git apply 484.patch
(populus-venv) $ cd ../../../

我们的 Populus 开发框架现在支持 Vyper。

智能合约单元测试

如果你运行单元测试,你将收到一个错误,因为数据类型不正确。解决方法是将所有字符串数据类型更改为bytes数据类型。然后,由于弃用警告,你应该改变调用智能合约方法的方式。

最终,你的单元测试,位于tests/test_greeter.py,应该是这样的:

def test_greeter(chain):
    greeter, _ = chain.provider.get_or_deploy_contract('Greeter')

    greeting = greeter.functions.greet().call()
    assert greeting == b'Hello'

def test_custom_greeting(chain):
    greeter, _ = chain.provider.get_or_deploy_contract('Greeter')

    set_txn_hash = greeter.functions.setGreeting(b'Guten Tag').transact()
    chain.wait.for_receipt(set_txn_hash)

    greeting = greeter.functions.greet().call()
    assert greeting == b'Guten Tag'

然后,如果你再次运行你的单元测试,它将会成功。

让我们来看一下第一个方法:

def test_greeter(chain):
    greeter, _ = chain.provider.get_or_deploy_contract('Greeter')

    greeting = greeter.functions.greet().call()
    assert greeting == b'Hello'

你的单元测试函数大部分时间应该接受chain参数。从chain对象中,你可以获取一个提供者。如果你记得的话,提供者是一个连接到以太坊区块链的对象,无论是通过 HTTP 还是 IPC。从那里,你可以获取合约对象。测试框架是直接从区块链获取合约对象,还是在获取合约之前先部署合约,这取决于情况。在我们的情况下,是后者选项。测试框架在内存中创建一个合约对象并与之交互。单元测试结束后,一切都消失了。

之后,你将从永久区块链中获取合约对象。provider对象的get_or_deploy_contract返回两个对象。你通常只需要关注第一个对象,即合约对象。

contract对象开始,执行智能合约的方法对你来说应该很熟悉。要执行public方法或获取公共变量的值,你将使用functions,接着是方法或公共变量加上(),最后以call()结束。

执行公共方法后,你将得到返回值。在这种情况下,你会得到一个byte对象,不像 Solidity 示例中的string。在 Vyper 中,没有一流的字符串支持。字符串被存储为byte对象。Vyper 仍处于活跃的开发模式中,但预计将来会发生变化。

在第二个单元测试中,你将测试智能合约中改变变量状态的方法:

def test_custom_greeting(chain):
    greeter, _ = chain.provider.get_or_deploy_contract('Greeter')

    set_txn_hash = greeter.functions.setGreeting(b'Guten Tag').transact()
    chain.wait.for_receipt(set_txn_hash)

    greeting = greeter.functions.greet().call()
    assert greeting == b'Guten Tag'

一切都和第一个单元测试一样,只是这里有两行新的内容:

set_txn_hash = greeter.functions.setGreeting(b'Guten Tag').transact()
chain.wait.for_receipt(set_txn_hash)

这是使用交易的方式。记住,交易是任何涉及改变合约状态的事情。你调用functions,接着是将改变合约状态的方法加上(),最后以transact()和必要的参数结束。之后,使用chain对象,你等待交易完成。在测试期间,这非常快。但如果你在另一个以太坊区块链上测试,比如在 Rinkeby 网络上,交易可能会持续几分钟。

让我们来看一个更复杂的例子。还记得你在第三章中编写的捐赠智能合约吗,使用 Vyper 实现智能合约。为什么我们不测试一下那个智能合约呢?

让我们将捐赠智能合约保存在您在第三章中开发的源代码文件夹中,使用 Vyper 实现智能合约。您可以将代码保存在contracts/donation.vy中。如果您忘记了这一点,可以参考以下 GitLab 链接上的代码文件获取完整的代码,gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_05/populus_tutorial/contracts/Donation.vy

struct DonaturDetail:
    sum: uint256(wei)
    name: bytes[100]
    time: timestamp

donatur_details: public(map(address, DonaturDetail))

donaturs: public(address[10])

donatee: public(address)

index: int128

...
...

@public
def withdraw_donation():
    assert msg.sender == self.donatee

    send(self.donatee, self.balance)

我们想在这里测试许多不同的事情。

首先,让我们看一下构造方法:

@public
def __init__():
    self.donatee = msg.sender

我们想测试donatee变量是否包含启动智能合约的账户地址。

让我们编写我们的第一个单元测试。您可以将此单元测试保存在tests/test_donation.py中:

def test_donatee(web3, chain):
    donation, _ = chain.provider.get_or_deploy_contract('Donation')

    donatee = donation.functions.donatee().call()
    assert donatee == web3.eth.coinbase

在这个单元测试中,我们使用了两个参数版本,因为我们想获取web3对象。如果您交换参数的顺序,实际上并不重要。def test_donatee(web3, chain):def test_donatee(chain, web3):是一样的。

get_or_deploy_contract中的参数取自您的.vy文件的名称。因此,在命名源代码文件名时要小心。

有一件新的事情(但实际上并不新,因为您已经在第四章中遇到过这个),那就是web3.eth.coinbase。这是默认账户。在这个上下文(单元测试)中,它表示启动智能合约的账户的地址。

如果这个单元测试成功,让我们继续测试这个智能合约中的另一个方法:

@payable
@public
def donate(name: bytes[100]):
    assert msg.value >= as_wei_value(1, "ether")
    assert self.index < 10

    self.donatur_details[msg.sender] = DonaturDetail({
                                         sum: msg.value,
                                         name: name,
                                         time: block.timestamp
                                       })

    self.donaturs[self.index] = msg.sender
    self.index += 1

在这个方法中,您被迫向智能合约发送至少 1 个以太币。

通过编写第二个单元测试来测试失败情况:

def test_donate_less_than_1_eth(web3, chain):
    donation, _ = chain.provider.get_or_deploy_contract('Donation')

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        donation.transact({'value': web3.toWei('0.8', 'ether')}).donate(b'Taylor Swift')

现在,您需要添加两个import语句:

import pytest
import eth_tester

让我们学习如何在这个单元测试中处理异常。您可以将有问题的交易放在with语句中,该语句将捕获异常。基本上,您期望交易失败。当程序中出现故障时,它会返回一个特殊代码(例如,-1)或抛出异常。在这种情况下,它会抛出异常。

运行测试以查看它是否有效。然后,让我们测试成功的情况,也就是捐赠被接受(请参考以下 GitLab 链接上的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_05/populus_tutorial/tests/test_donation.py):

def test_donate_1_eth(web3, chain):
    import time

    donation, _ = chain.provider.get_or_deploy_contract('Donation')

    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

...
...

    assert donatur == account2
    assert donation_sum == web3.toWei('1', 'ether')
    assert donation_name == donatur_name
    assert (int(time.time()) - donation_time) < 600 # could be flaky

    assert web3.eth.getBalance(donation.address) == web3.toWei('1', 'ether')

这是很多东西要消化的,所以让我们一步一步地讨论。

import time

我们将使用time库来检查此单元测试中的时间戳:

donation, _ = chain.provider.get_or_deploy_contract('Donation')

您已经知道这个声明。使用chain对象,您可以获取提供者,然后使用该提供者的get_or_deploy_contract方法。结果是一个捐赠智能合约对象:

t = eth_tester.EthereumTester()
account2 = t.get_accounts()[1]

eth_tester是一个旨在使您更轻松地测试智能合约的库。为此,您创建一个EthereumTester对象。该库中有许多辅助函数。其中一个函数是为您提供一些加载了大量 ETH 的账户,有点像 Ganache。第一个账户是管理账户或启动智能合约的账户。如前所示,您也可以从web3.eth.coinbase获取该账户。第二个账户等等是您可以在单元测试中使用的测试账户。

t.get_accounts()会给您很多账户。但是不要使用第一个账户,因为那将是管理账户。在这里,您使用第二个账户:

    donatur_name = b'Taylor Swift'
    set_txn_hash = donation.functions.donate(donatur_name).transact({'from': account2, 'value': web3.toWei('1', 'ether')})
    chain.wait.for_receipt(set_txn_hash)

你在之前的与 greeter 智能合约的单元测试中看到了transact函数。在这个transact方法中,你可以指定你想发送到智能合约的以太币金额。你也可以使用不同的账户。在这些transact参数中,我们还使用了辅助函数web3.toWei。否则,你必须使用很多零的数字。然后,你等待交易被确认:

donatur = donation.functions.donaturs(0).call()
donation_sum = donation.functions.donatur_details__sum(donatur).call()
donation_name = donation.functions.donatur_details__name(donatur).call()
donation_time = donation.functions.donatur_details__time(donatur).call()

在完成这些之后,你可以访问公共变量来确认状态的变化。

对于数组,你在数组方法中放入一个索引,就像donation.functions.donaturs(0).call()中一样。

对于映射,你提供映射方法的参数中的键,而不是整数索引,就像donation.functions.donatur_details__sum(donatur).call()中一样。

对于结构体,你通过追加两个下划线来访问成员,就像donation.functions.donatur_details__sum(donatur).call()中一样。

然后,我们通过断言所有这些变量来测试捐款:

assert donatur == account2

然后我们检查捐赠者地址是否被正确记录:

assert donation_sum == web3.toWei('1', 'ether')

然后我们检查捐款金额是否被正确记录:

assert donation_name == donatur_name

然后我们检查捐赠者的姓名是否被正确记录:

assert (int(time.time()) - donation_time) < 600 # could be flaky

然后我们检查捐款发生的时间。我们这样做是因为时间是在交易在区块链中确认时记录的,所以你永远不知道交易何时被确认。在我们的情况下,这会很快。然而,如果你在 Rinkeby 网络中测试,这是需要牢记的事情。在这里,我确保单元测试时间和智能合约中记录的时间戳之间的差异不超过 10 分钟(600 秒)。

然后我们直接从智能合约的余额中检查捐款金额:

assert web3.eth.getBalance(donation.address) == web3.toWei('1', 'ether')

这与之前的测试不同,之前的测试是通过检查公共变量的值来测试余额。在我们的情况下,我们有很多捐款,所以它们会不同。

让我们来看看智能合约中的最后一个方法:

@public
 def withdraw_donation():
     assert msg.sender == self.donatee

     send(self.donatee, self.balance)

这是提取捐款的方法。不要笑;有些人启动了一个没有提取以太币方法的智能合约。这些智能合约中的所有以太币都将永远被锁定,这就是为什么测试很重要。它确保你不会犯这样的愚蠢错误。

在这个提款方法中,你想要测试你能否从智能合约中提取以太币。然后,你需要确保只有特定的账户(在这种情况下是经理账户)能够从智能合约中提取以太币。

让我们为此创建一个单元测试。首先,我们创建一个单元测试,以确保另一个账户无法从智能合约中提取以太币:

def test_other_account_could_not_withdraw_money(web3, chain):
    donation, _ = chain.provider.get_or_deploy_contract('Donation')

    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    donatur_name = b'Taylor Swift'
    set_txn_hash = donation.functions.donate(donatur_name).transact({'from': account2, 'value': web3.toWei('1', 'ether')})
    chain.wait.for_receipt(set_txn_hash)

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        donation.functions.withdraw_donation().transact({'from': account2})

现在一切应该对你来说都很熟悉了。你在with语句中包装提取以太币的失败情况,以捕获异常。

现在,让我们测试经理账户确实能够提取以太币的成功情况:

def test_manager_account_could_withdraw_money(web3, chain):
     donation, _ = chain.provider.get_or_deploy_contract('Donation')

     t = eth_tester.EthereumTester()
     account2 = t.get_accounts()[1]

     donatur_name = b'Taylor Swift'
     set_txn_hash = donation.functions.donate(donatur_name).transact({'from': account2, 'value': web3.toWei('1', 'ether')})
     chain.wait.for_receipt(set_txn_hash)

     initial_balance = web3.eth.getBalance(web3.eth.coinbase)
     set_txn_hash = donation.functions.withdraw_donation().transact({'from': web3.eth.coinbase})
     chain.wait.for_receipt(set_txn_hash)

     after_withdraw_balance = web3.eth.getBalance(web3.eth.coinbase)

     assert abs((after_withdraw_balance - initial_balance) - web3.toWei('1', 'ether')) < web3.toWei('10', 'gwei')

这里的一切对你来说都很熟悉,除了最后一行。经理账户提取后的余额减去initial_balance不会完全是1个以太币,因为经理账户需要支付手续费。在这种情况下,我们只是使用一个粗略的测量来确保提取成功。只要差异低于 10 gwei,那就没问题。Gwei 是我们用来计算发送交易手续费的货币。1 gwei 等于 1,000,000,000 wei。如果你觉得这很困惑,你可以使用以太币货币进行断言,如下所示:

assert abs((after_withdraw_balance - initial_balance) - web3.toWei('1', 'ether')) < web3.toWei('0.00000001', 'ether')

运行测试:

(populus-venv) $ py.test tests/test_donation.py

你应该得到一个成功的结果:

使用构造函数中的参数

我们测试的两个智能合约在构造函数中都没有参数。但是如果我们编写一个构造函数中带有参数的智能合约呢?我们如何测试呢?

让我们编写一个简单的智能合约,构造函数中带有参数。命名为contracts/Greeter2.vy

greeting: bytes[20]

@public
def __init__(greeting_param: bytes[20]):
    self.greeting = greeting_param

@public
def setGreeting(x: bytes[20]):
    self.greeting = x

@public
def greet() -> bytes[20]:
    return self.greeting

然后,编写以下测试。命名为tests/test_greeter2.py

import pytest

@pytest.fixture()
def greeter2_contract(chain):
    Greeter2Factory = chain.provider.get_contract_factory('Greeter2')
    deploy_txn_hash = Greeter2Factory.constructor(b'Hola').transact()
    contract_address = chain.wait.for_contract_address(deploy_txn_hash)
    return Greeter2Factory(address=contract_address)

def test_greeter2(greeter2_contract):
    greeting2 = greeter2_contract.functions.greet().call()
    assert greeting2 == b'Hola'

在这里,您可以使用pytestfixture功能自定义部署智能合约。首先,您从链的提供者那里获取合约工厂,然后将参数放在factory对象的constructor方法中,以transact()方法结束。然后,等待直到它在区块链上部署。最后,您将收到地址。

在测试函数中,您将参数放置在与 fixture 函数名称完全相同的位置。在这里,我们的 fixture 函数是greeter2_contract。在测试函数中,您只需将此对象用作类似于chain.provider.get_or_deploy_contract('Donation')返回的对象的智能合约对象。

现在,按以下方式进行测试:

(populus-venv) $ py.test tests/test_greeter2.py

您应该得到一个成功的结果:

您仍然可以测试许多事项。我们只测试了一次捐赠,但我们可以接受多达 10 次捐赠。这也应该被检查。您希望测试覆盖率有多完整取决于您和项目的要求。还有许多单元测试方面我们在这里没有讨论,例如设置、拆卸和测试驱动开发。

使用 Populus 部署智能合约

Populus 不仅是一个旨在轻松开发和测试智能合约的框架。它还有一个部署智能合约到区块链的工具,包括私有链。

私有链基本上是您的私有以太坊区块链,类似于 Ganache,只是您自己构建它。就像在 Rinkeby 网络中的区块链一样,但您是唯一的矿工。您可以使用 geth 软件手动创建私有区块链;populus只是让您更容易。

创建新的私有chain的命令如下:

(populus-venv) $ populus chain new localblock

此命令将在您的项目目录中创建一些文件。所有生成的文件都位于chains目录内。生成的文件之一是chains/localblock/genesis.json。创世文件的目的是指示以太坊区块链的初始配置:

{
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "coinbase": "0xcb22827ab291b3094076de25d583c49b902a5606",
  "extraData": "0x686f727365",
  "config": {
    "daoForkBlock": 0,
    "daoForSupport": true,
    "homesteadBlock": 0
  },
  "timestamp": "0x0",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "nonce": "0xdeadbeefdeadbeef",
  "alloc": {
    "0xcb22827ab291b3094076de25d583c49b902a5606":{
      "balance": "1000000000000000000000000000000"
    }
  },
  "gasLimit": "0x47d5cc",
  "difficulty": "0x01"
}

您不需要知道这些键的所有含义。coinbase表示所有挖矿奖励应该进入这个账户。alloc表示账户的初始余额。

除了创世文件之外,您还应该获得chains/localblock/init_chain.sh。打开它,您应该有以下内容:

#!/bin/sh
geth --rpc --rpcaddr 127.0.0.1 --rpcport 8545 --rpcapi admin,debug,eth,miner,net,personal,shh,txpool,web3,ws --ws --wsaddr 127.0.0.1 --wsport 8546 --wsapi admin,debug,eth,miner,net,personal,shh,txpool,web3,ws --datadir /home/yourusername/populus_tutorial/chains/localblock/chain_data --maxpeers 0 --networkid 1234 --port 30303 --ipcpath /home/yourusername/populus_tutorial/chains/localblock/chain_data/geth.ipc --nodiscover --mine --minerthreads 1 init /home/yourusername/populus_tutorial/chains/localblock/genesis.json

这个脚本基本上是用特定参数运行 geth 软件。最重要的参数是init,您将其提供给您的genesis.json文件。然后,运行您的私有区块链的初始化:

(populus-venv) $ ./chains/localblock/init_chain.sh

然后,打开chains/localblock/run_chain.sh文件,您应该有以下内容:

#!/bin/sh
geth --rpc --rpcaddr 127.0.0.1 --rpcport 8545 --rpcapi admin,debug,eth,miner,net,personal,shh,txpool,web3,ws --ws --wsaddr 127.0.0.1 --wsport 8546 --wsapi admin,debug,eth,miner,net,personal,shh,txpool,web3,ws --datadir /home/yourusername/populus_tutorial/chains/localblock/chain_data --maxpeers 0 --networkid 1234 --port 30303 --ipcpath /home/yourusername/populus_tutorial/chains/localblock/chain_data/geth.ipc --unlock 0xcb22827ab291b3094076de25d583c49b902a5606 --password /home/yourusername/populus_tutorial/chains/localblock/password --nodiscover --mine --minerthreads 1

您现在需要注意的重要标志是--mine--password--ipcpath--unlock--datadir--mine表示您想在这个私有区块链中进行挖矿,--password是密码文件的位置,--unlock解锁带有密码文件的账户,--datadir是您的私有以太坊区块链目录的位置,--ipcpath是当您运行您的私有以太坊区块链时geth.ipc将位于的位置。

在运行区块链之前,编辑chains/localblock/run_chain.sh脚本,将--ipcpath标志的值更改为/tmp/geth.ipc,然后运行区块链:

(populus-venv) $ ./chains/localblock/run_chain.sh

现在,编辑project.json文件。chains键有一个包含四个键的对象:testertempropstenmainnet。在这个对象中添加localblock键及其值。因此,localblock键必须与testermainnettempropsten键相邻,如下面的代码块所示:

     "localblock": {
       "chain": {
         "class": "populus.chain.ExternalChain"
       },
       "web3": {
         "provider": {
           "class": "web3.providers.ipc.IPCProvider",
         "settings": {
           "ipc_path":"/tmp/geth.ipc"
         }
        }
       },
       "contracts": {
         "backends": {
           "JSONFile": {"$ref": "contracts.backends.JSONFile"},
           "ProjectContracts": {
             "$ref": "contracts.backends.ProjectContracts"
           }
         }
       }
     }

然后,您可以将您的智能合约部署到您的私有区块链中,如下所示:

(populus_venv) $ populus deploy --chain localblock Donation
> Found 2 contract source files
  - contracts/Donation.vy
  - contracts/Greeter.vy
> Compiled 2 contracts
  - contracts/Donation.vy:Donation
  - contracts/Greeter.vy:Greeter
Beginning contract deployment. Deploying 1 total contracts (1 Specified, 0 because of library dependencies).

Donation
Deploying Donation
Deploy Transaction Sent: b'v\xc4`\x06h\x17\xf6\x10\xd7\xb2\x7f\xc6\x94\xeb\x91n\xae?]-\xf43\xb8F\xdc=}\xb33\x03|\xd4'
Waiting for confirmation...

Transaction Mined
=================
Tx Hash : b'v\xc4`\x06h\x17\xf6\x10\xd7\xb2\x7f\xc6\x94\xeb\x91n\xae?]-\xf43\xb8F\xdc=}\xb33\x03|\xd4'
Address : 0xab3B30CFeC1D50DCb0a13671D09d55e63b7cFf40
Gas Provided : 467715
Gas Used : 367715

Verified contract bytecode @ 0xab3B30CFeC1D50DCb0a13671D09d55e63b7cFf40
Deployment Successful.

然后,你可以像在第四章使用 Web3 与智能合约交互中那样玩弄你的智能合约。你可以参考以下 GitLab 链接的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_05/populus_tutorial/interact_smart_contract_in_private_chain.py,获取以下代码块的完整代码:

>>> from web3 import Web3, IPCProvider
>>> w3 = Web3(IPCProvider(ipc_path="/tmp/geth.ipc"))
>>> w3.eth.coinbase
'0xcB22827aB291b3094076DE25D583C49b902a5606'
>>> w3.eth.getBalance(w3.eth.coinbase)
1000000011875000000000000000000
>>> address = "0xab3B30CFeC1D50DCb0a13671D09d55e63b7cFf40"
>>> false = False
>>> true = True
>>> abi = 
...
...
>>> donation = w3.eth.contract(address=address, abi=abi)
>>> donation.functions.donatee().call()
'0xcB22827aB291b3094076DE25D583C49b902a5606'

你还在使用 Ganache 吗?如果是的,我们可以部署到 Ganache 区块链。首先启动 Ganache,然后将以下内容添加到projects.json文件中的chains键对象中:

     "ganache": {
       "chain": {
         "class": "populus.chain.ExternalChain"
       },
       "web3": {
         "provider": {
           "class": "web3.providers.HTTPProvider",
           "settings": {
             "endpoint_uri": "http://localhost:7545"
           }
         }
       },
       "contracts": {
         "backends": {
           "JSONFile": {"$ref": "contracts.backends.JSONFile"},
           "ProjectContracts": {
             "$ref": "contracts.backends.ProjectContracts"
           }
         }
       }
     }

按照以下方式运行部署:

(populus_venv) $ populus deploy --chain ganache Donation
> Found 2 contract source files
  - contracts/Donation.vy
  - contracts/Greeter.vy
> Compiled 2 contracts
  - contracts/Donation.vy:Donation
  - contracts/Greeter.vy:Greeter
Beginning contract deployment. Deploying 1 total contracts (1 Specified, 0 because of library dependencies).

Donation
Deploying Donation
Deploy Transaction Sent: b'\xd4\xeb,{\xa0d\n\xb2\xb0\xb2\x1b\x18\xdd \xa1A\x89\xea`\xa8b?A\x14L\x99\xd1rR4\xc7\xfa'
Waiting for confirmation...

Transaction Mined
=================
Tx Hash : b'\xd4\xeb,{\xa0d\n\xb2\xb0\xb2\x1b\x18\xdd \xa1A\x89\xea`\xa8b?A\x14L\x99\xd1rR4\xc7\xfa'
Address : 0x9Dc44aa8d05c86388E647F954D00CaA858837804
Gas Provided : 467715
Gas Used : 367715

Verified contract bytecode @ 0x9Dc44aa8d05c86388E647F954D00CaA858837804
Deployment Successful.

然后,当你想在 Ganache 中玩弄你的智能合约时,你只需要像在[第四章中学到的那样调整提供者,使用 Web3 与智能合约交互

 >>> from web3 import Web3, HTTPProvider
 >>> w3 = Web3(HTTPProvider('http://localhost:7545'))

其余的代码是一样的。你可以将脚本命名为interact_smart_contract_in_ganache.py。在 Ganache 中,coinbase 账户和 manager 账户是 Ganache 中的第一个账户。参考 GitLab 链接获取与 Ganache 中智能合约交互的完整代码,gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_05/populus_tutorial/interact_smart_contract_in_ganache.py

from web3 import Web3, HTTPProvider
w3 = Web3(HTTPProvider("http://localhost:7545"))

print(w3.eth.coinbase)
print(w3.eth.getBalance(w3.eth.coinbase))

# Change this address to your smart contract address
address = "0x9Dc44aa8d05c86388E647F954D00CaA858837804"
false = False
true = True
abi = [
...
...
donation = w3.eth.contract(address=address, abi=abi)
print(donation.functions.donatee().call())

摘要

在本章中,你学会了如何使用 Populus 开发框架开发智能合约。Populus 不支持 Vyper,只支持 Solidity。因此,你需要通过安装 Vyper 并编辑 Populus 项目配置文件来添加 Vyper 支持。你还学会了如何为智能合约创建单元测试。在这个单元测试中,你使用web3对象与智能合约进行交互并进行程序化测试。之后,你学会了如何创建私有链。最后,你将智能合约部署到了这个私有链和 Ganache。

在下一章中,你将构建一个比捐赠智能合约更复杂的区块链应用。这个应用是去中心化的投票应用。

第六章:构建一个实用的去中心化应用程序

在本章中,我们将在区块链上编写一个流行的应用程序,这将是一个由区块链驱动的安全投票应用程序。您有所有开发此应用程序的工具,即 populus 和web3.py

以下是本章将涵盖的主题:

  • 开发一个简单的投票应用

  • 了解智能合约中的事件

  • 开发一个商业投票应用程序

  • 开发基于令牌的投票应用

  • 讨论另一种类型的投票应用程序

开发一个简单的投票应用

首先,我们将构建最简单的投票应用程序,比 Vyper 软件源代码中提供的投票应用程序示例还要简单。让我们设置我们的 Populus 项目目录:

$ virtualenv -p python3.6 voting-venv
$ source voting-venv/bin/activate (voting-venv) $ pip install eth-abi==1.2.2 (voting-venv) $ pip install eth-typing==1.1.0 (voting-venv) $ pip install web3==4.7.2 (voting-venv) $ pip install -e git+https://github.com/ethereum/populus#egg=populus (voting-venv) $ pip install vyper
(voting-venv) $ mkdir voting_project
(voting-venv) $ cd voting_project
(voting-venv) $ mkdir tests contracts
(voting-venv) $ cp ../voting-venv/src/populus/populus/assets/defaults.v9.config.json project.json

然后,通过将键编译的值更改为以下内容,将 Vyper 支持添加到project.json中:

"compilation": {
    "backend": {
      "class": "populus.compilation.backends.VyperBackend"
    },
    "contract_source_dirs": [
      "./contracts"
    ],
    "import_remappings": []
},

Vyper 的最新版本是 0.1.0b6,这破坏了 Populus。开发人员需要一些时间来解决这个问题。如果到您阅读本书时错误仍未被修复,您可以自行修补 Populus。

首先,使用以下命令检查错误是否已修复:

(voting-venv) $ cd voting-venv/src/populus
(voting-venv) $ grep -R "compile(" populus/compilation/backends/vyper.py
            bytecode = '0x' + compiler.compile(code).hex()
            bytecode_runtime = '0x' + compiler.compile(code, bytecode_runtime=True).hex()

在我们的案例中,错误尚未被修复。因此,让我们修补 Populus 以修复错误。确保您仍然在相同的目录(voting-venv/src/populus)中:

(voting-venv) $ wget https://patch-diff.githubusercontent.com/raw/ethereum/populus/pull/484.patch
(voting-venv) $ git apply 484.patch

现在,在contracts目录中创建一个简单的投票智能合约。命名为SimpleVoting.vy。*请参考以下 GitLab 链接获取完整代码 - gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/contracts/SimpleVoting.vy

struct Proposal:
    name: bytes32
    vote_count: int128

Voting: event ({_from: indexed(address), _proposal: int128})

proposals: public(map(int128, Proposal))

proposals_count: public(int128)
voters_voted: public(map(address, int128))

...
...

@public
@constant
def winner_name() -> bytes32:
    return self.proposals[self.winning_proposal()].name

让我们讨论这个简单的投票智能合约。它受到 Vyper 源代码中投票示例的启发,但这个示例甚至更简化。原始示例具有委托功能,这将使事情难以理解。我们从结构数据类型变量声明开始:

struct Proposal:
    name: bytes32
    vote_count: int128

数据结构是一个具有复合数据类型的变量,其中包含提案的名称和提案的金额。Proposal结构中的vote_count数据类型为int128,而Proposal结构中的name数据类型为bytes32。您也可以使用uint256而不是int128数据类型来表示Proposal结构中的vote_count。不过,这不会有任何区别。但是,bytes32是一个新的数据类型。正如您可能还记得的那样,如果要在 Vyper 中使用字符串(或字节数组)数据类型,如果该字符串的长度小于 20,则使用bytes[20]

bytes32 是另一种类似于bytes[32]的字符串数据类型,但有一个特殊之处;如果您将b'messi'字符串设置为具有bytes[32]类型的变量,并使用web3检索它,您将得到b'messi'。但是,如果您将b'messi'字符串设置为具有bytes32类型的变量,并使用web3检索它,您将得到b'messi\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'。此字符串将填充直到达到 32 字节。默认情况下,您应该使用bytes[20]bytes[256]作为字符串数据类型,而不是使用bytes32。那么为什么我在这个智能合约中使用bytes32?我有一个很好的理由这样做,但我们需要先继续到构造函数以了解我为什么使用bytes32来保存提案的名称:

Voting: event ({_from: indexed(address), _proposal: int128})

这是我们第一次在智能合约中使用事件。event是 Vyper 中的一个关键字,用于创建事件。事件是智能合约内发生的事情,我们的客户端(web3程序)希望订阅。在这个语句中,Voting是事件的名称,有两个参数。第一个参数是_from,类型为addressindexed用于使用_from作为过滤器进行事件过滤。第二个参数是_proposal,类型为int128。记住,int128是一个 128 位整数。当我们在客户端程序中订阅它时,这个事件将变得更清晰。现在,让我们继续下一个:

proposals: public(map(int128, Proposal))

这个变量是一个映射数据类型变量,将一个int128数据类型变量映射到一个Proposal结构变量。基本上,这是一个提案列表:

proposals_count: public(int128)

这是一个辅助变量,用来计算这个智能合约中有多少提案:

voters_voted: public(int128[address])

这用于检查一个账户是否已经投票。我们不希望一个账户对同一个提案投票多次。记住,这是一个映射数据类型。默认情况下,不存在的值指向一个空值。在int128的上下文中,空值是0

@public
def __init__(_proposalNames: bytes32[2]):
    for i in range(2):
        self.proposals[i] = Proposal({
            name: _proposalNames[i],
            vote_count: 0
        })
        self.proposals_count += 1

这个构造函数有一个参数,是一个bytes32数组。在构造函数内部,它将迭代两次(我们将提案的数量硬编码为两个)。每次迭代都会将一个新成员设置到proposals映射变量中。name是从参数中设置的,vote_count初始化为 0。然后,对于每次迭代,proposals_count都会增加 1。

我之所以使用bytes32作为提案名称的数据类型,是因为如果我使用bytes[128]作为提案名称的数据类型,我无法将其作为参数发送。

Vyper 编程语言中的智能合约方法不能接受嵌套数组,比如bytes[128][2]作为参数(至少在 Vyper 的最新版本中是这样):

@public
def vote(proposal: int128):
    assert self.voters_voted[msg.sender] == 0
    assert proposal < self.proposals_count

    self.voters_voted[msg.sender] = 1
    self.proposals[proposal].vote_count += 1

    log.Voting(msg.sender, proposal)

这是投票的函数。它接受一个名为proposal的参数。在这里,用户用一个整数为提案投票。所以,如果用户使用0这样的参数调用vote方法,比如vote(0),这意味着用户为第一个提案投票。当你设计自己的投票智能合约时,当然你也可以使用字符串来投票,比如vote(b'proposal1')。在这里,我使用整数来简化事情。

在这个函数中,我们断言选民还没有使用这个语句投票:assert self.voters_voted[msg.sender] == 0。投票后,我们将voters_voted的值设置为选民的地址作为键的1self.voters_voted[msg.sender] = 1。我们还通过检查投票的值是否小于提案的数量(即2)来验证投票是否有效。这个函数的关键是以下语句:self.proposals[proposal].vote_count += 1。在这个函数的结尾,我们的Voting事件在这个语句中被使用:log.Voting(msg.sender, proposal)。这类似于广播发生了重要的事情——嘿,世界!有一个Voting事件,有两个参数,msg.sender作为address参数,proposal作为int128参数。然后,任何订阅了这个事件的人都会收到通知。事件的订阅发生在客户端,使用web3库,如下面的代码所示:

@private
@constant
def winning_proposal() -> int128:
    winning_vote_count: int128 = 0
    winning_proposal: int128 = 0
    for i in range(2):
        if self.proposals[i].vote_count > winning_vote_count:
            winning_vote_count = self.proposals[i].vote_count
            winning_proposal = i
    return winning_proposal

这个私有函数设计用来检查哪个提案获得了最多的投票:

@public
@constant
def winner_name() -> bytes32:
    return self.proposals[self.winning_proposal()].name

这个public函数设计用来获取获得最多票数的提案的名称。这个函数使用了前面描述的私有函数。

这个智能合约很简单,但并不完美,因为存在一个错误。例如,在vote函数中,我们没有处理投票的负值。此外,提案的数量是硬编码为 2。但是,它可以完成工作。

然后,你可以像通常一样编译智能合约的代码:

(voting-venv) $ populus compile

作为一个好公民,让我们为这个智能合约编写一个测试。在tests目录中创建一个名为test_simple_voting_app.py的文件。参考以下 GitLab 链接获取以下代码块的完整代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/tests/test_simple_voting_app.py

import pytest
import eth_tester

@pytest.fixture()
def voting(chain):
    SimpleVotingFactory = chain.provider.get_contract_factory('SimpleVoting')
    deploy_txn_hash = SimpleVotingFactory.constructor([b'Messi', b'Ronaldo']).transact()
    contract_address = chain.wait.for_contract_address(deploy_txn_hash)
    return SimpleVotingFactory(address=contract_address)
...
...
    assert voting.functions.proposals__vote_count(0).call() == 2
    assert voting.functions.proposals__vote_count(1).call() == 1
    assert voting.functions.winner_name().call()[:5] == b'Messi'

让我们逐个讨论这个测试:

@pytest.fixture()
def voting(chain):
    SimpleVotingFactory = chain.provider.get_contract_factory('SimpleVoting')
    deploy_txn_hash = SimpleVotingFactory.constructor([b'Messi', b'Ronaldo']).transact()
    contract_address = chain.wait.for_contract_address(deploy_txn_hash)
    return SimpleVotingFactory(address=contract_address)

因为我们简单的投票智能合约的构造函数需要一个参数,所以我们需要在测试中使用一个 fixture,如第五章Populus 开发框架中所讨论的那样。然后,我们的 fixture 可以作为测试方法的参数使用:

def test_initial_state(voting):
    assert voting.functions.proposals_count().call() == 2

    messi = voting.functions.proposals__name(0).call()
    assert len(messi) == 32
    assert messi[:5] == b'Messi'
    assert voting.functions.proposals__name(1).call()[:7] == b'Ronaldo'
    assert voting.functions.proposals__vote_count(0).call() == 0
    assert voting.functions.proposals__vote_count(1).call() == 0

这是为了检查部署后智能合约的状态。这里有一件非常独特的事情;在提案变量内的结构数据的名称变量的长度是32,即使我们将其设置为值b'messi',这就是bytes32数据类型的特殊之处。这就是为什么我们要切片变量以获取我们想要的内容。然后,对于下一个测试方法,我们使用chain参数以及voting参数:

def test_vote(voting, chain):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    assert voting.functions.proposals__vote_count(0).call() == 0

    set_txn_hash = voting.functions.vote(0).transact({'from': account2})
    chain.wait.for_receipt(set_txn_hash)

    assert voting.functions.proposals__vote_count(0).call() == 1

这用于测试vote函数。我们测试vote函数是否确实改变了proposals变量的vote_count属性:

def test_fail_duplicate_vote(voting, chain):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    set_txn_hash = voting.functions.vote(0).transact({'from': account2})
    chain.wait.for_receipt(set_txn_hash)

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        voting.functions.vote(1).transact({'from': account2})

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        voting.functions.vote(0).transact({'from': account2})

这确保了我们不能使用同一个账户投票超过一次。正如我们在第五章Populus 开发框架中学到的那样,您可以使用pytest.raises with语句将失败的情况包装起来。最后一个测试用例是检查获胜的提案:

def test_winning_proposal(voting, chain):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]
    account3 = t.get_accounts()[2]
    account4 = t.get_accounts()[3]

    set_txn_hash = voting.functions.vote(0).transact({'from': account2})
    chain.wait.for_receipt(set_txn_hash)

    set_txn_hash = voting.functions.vote(0).transact({'from': account3})
    chain.wait.for_receipt(set_txn_hash)

    set_txn_hash = voting.functions.vote(1).transact({'from': account4})
    chain.wait.for_receipt(set_txn_hash)

    assert voting.functions.proposals__vote_count(0).call() == 2
    assert voting.functions.proposals__vote_count(1).call() == 1
    assert voting.functions.winner_name().call()[:5] == b'Messi'

在这个测试中,您可以使用t.get_accounts辅助方法使用三个账户。

部署一个构造函数中带有参数的智能合约

让我们将这个智能合约部署到以太坊区块链。然而,我们首先需要意识到有一些事情会使情况复杂化。首先,event在 Ganache 中不起作用,因此我们必须将其部署到 Rinkeby 网络或私有以太坊区块链。其次,我们的智能合约在构造函数中有一个参数。要部署带有参数的智能合约,我们需要使用不同的方法;我们不能像在第五章Populus 开发框架中演示的那样使用普通方法。在第五章Populus 开发框架中,我们使用 Populus 以这种方式部署了一个智能合约:populus deploy --chain localblock Donation.

Populus 方法只能部署一个没有参数的构造函数的智能合约。让我们一一克服这些障碍。我们需要做的第一件事是将其部署到私有以太坊区块链,如下所示:

  1. voting_project目录中,运行以下命令:
(voting-venv) $ populus chain new localblock
  1. 然后,使用init_chain.sh脚本初始化私有链:
(voting-venv) $ ./chains/localblock/init_chain.sh
  1. 编辑chains/localblock/run_chain.sh并将--ipcpath标志的值更改为/tmp/geth.ipc。然后,运行区块链:
(voting-venv) $ ./chains/localblock/run_chain.sh
  1. 现在,编辑project.json文件。chains键有一个对象,其中有 4 个键:testertempropstenmainnet。在这个对象中添加一个名为localblock的键及其值:
    "localblock": {
      "chain": {
        "class": "populus.chain.ExternalChain"
      },
      "web3": {
        "provider": {
          "class": "web3.providers.ipc.IPCProvider",
        "settings": {
          "ipc_path":"/tmp/geth.ipc"
        }
       }
      },
      "contracts": {
        "backends": {
          "JSONFile": {"$ref": "contracts.backends.JSONFile"},
          "ProjectContracts": {
            "$ref": "contracts.backends.ProjectContracts"
          }
        }
      }
    }

运行区块链需要一个专用的终端。因此,打开一个新的终端,执行一个虚拟环境脚本,然后进入voting_project目录。创建这个文件并命名为deploy_SmartVoting.py

from populus import Project
from populus.utils.wait import wait_for_transaction_receipt

def main():

    project = Project()

    chain_name = "localblock"

    with project.get_chain(chain_name) as chain:

        SimpleVoting = chain.provider.get_contract_factory('SimpleVoting')

        txhash = SimpleVoting.deploy(transaction={"from": chain.web3.eth.coinbase}, args=[[b'Messi', b'Ronaldo']])
        receipt = wait_for_transaction_receipt(chain.web3, txhash)
        simple_voting_address = receipt["contractAddress"]
        print("SimpleVoting contract address is", simple_voting_address)

if __name__ == "__main__":
    main()

现在,让我们讨论一下这个程序的作用:

from populus import Project
from populus.utils.wait import wait_for_transaction_receipt

我们从populus库中导入工具,Project代表project.json配置文件。wait_for_transaction_receipt是一个等待我们的交易在以太坊区块链中确认的函数:

def main():

    project = Project()

    chain_name = "localblock"

    with project.get_chain(chain_name) as chain:

main函数中,我们初始化了一个Project实例,然后获取了localblock链:

    "localblock": {
      "chain": {
        "class": "populus.chain.ExternalChain"
      },
      "web3": {
        "provider": {
          "class": "web3.providers.ipc.IPCProvider",
        "settings": {
          "ipc_path":"/tmp/geth.ipc"
        }
       }
      },
      "contracts": {
        "backends": {
          "JSONFile": {"$ref": "contracts.backends.JSONFile"},
          "ProjectContracts": {
            "$ref": "contracts.backends.ProjectContracts"
          }
        }
      }
    }

chain对象现在代表project.json文件中的这个json对象。

我们从build/contracts.json中获取SimpleVoting智能合约工厂:

SimpleVoting = chain.provider.get_contract_factory('SimpleVoting')

然后,我们将我们的智能合约部署到私有以太坊区块链上:

txhash = SimpleVoting.deploy(transaction={"from": chain.web3.eth.coinbase}, args=[[b'Messi', b'Ronaldo']])

它接收两个关键字参数,transactionargstransaction参数是一个交易字典。在这里,我们设置了from参数。chain.web3.eth.coinbase是我们的默认账户,在testing/development场景中很常见。在这里,我们使用默认账户而不使用私钥。在这个交易对象中,我们还可以设置gasgasPrice和其他交易参数。args关键字参数允许我们向智能合约的构造函数发送参数。它是一个嵌套数组,[[b'Messi', b'Ronaldo']],因为内部数组是智能合约构造函数中的_proposalNames参数。

外部数组被设计用来封装构造函数中的其他参数,但在这种情况下我们只有一个参数:

@public
def __init__(_proposalNames: bytes32[2]):
    for i in range(2):
        self.proposals[i] = {
            name: _proposalNames[i],
            vote_count: 0
        }
        self.proposals_count += 1

receipt = wait_for_transaction_receipt(chain.web3, txhash)

我们等待交易确认。然后,我们从部署过程中获取智能合约的地址:

simple_voting_address = receipt["contractAddress"]
print("SimpleVoting contract address is", simple_voting_address)

receipt对象是区块链中描述交易确认的对象。在这种情况下,我们关心的是地址,也就是receipt对象中的contractAddress键:

if __name__ == "__main__":
    main()

这是为了执行main函数而设计的。

不像 Ganache,那里有 10 个账户(每个账户都有 100 个以太币),在 Populus 的默认设置下的私有以太坊区块链中,你只有一个账户,配备了 1 万亿以太币!以下脚本允许你查看默认账户有多少以太币:

from web3 import Web3, IPCProvider

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

print(w3.fromWei(w3.eth.getBalance(w3.eth.coinbase), 'ether'))

在这个智能合约中,我们想要使用多个账户来玩我们的智能合约。所以让我们在这个以太坊私有区块链中创建 10 个账户。在voting_project目录中创建一个新文件,命名为create_10_accounts_on_private_chain.py

from web3 import Web3, IPCProvider

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

with open('10_accounts.txt', 'w') as f:
    for i in range(10):
        f.write(w3.personal.newAccount('password123') + "\n")

我们将新账户的地址写入文件,以便以后重复使用。你需要注意的函数是w3.personal.newAccount('password123')。这将给你公共地址。私钥将使用password123进行加密。这将保存在chains/localblock/chain_data/keystore目录中。加密文件的名称类似于UTC—2018-10-26T13-13-25.731124692Z—36461a003a03f857d60f5bd0b8e8a64aab4e4535。文件名的结尾部分是public地址。在这个文件名的示例中,public地址是36461a003a03f857d60f5bd0b8e8a64aab4e4535。执行这个脚本。10 个账户的public地址将被写入10_accounts.txt文件。

如果你查看chains/localblock/chain_data/keystore目录,你会看到至少 11 个文件。

这 10 个新账户中的每一个都配备了 0 个以太币。要在我们的智能合约中投票,你不能有空余的余额。那么,为什么我们不把默认账户的钱分发给这 10 个账户呢?在voting_project目录下创建一个文件,命名为distribute_money.py。参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/distribute_money.py

from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
import glob

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

address = 'fa146d7af4b92eb1751c3c9c644fa436a60f7b75'

...
...

        signed = w3.eth.account.signTransaction(transaction, private_key)
        txhash = w3.eth.sendRawTransaction(signed.rawTransaction)
        wait_for_transaction_receipt(w3, txhash)

现在,让我们逐行讨论这个脚本:

from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
import glob

您已经了解了Web3IPCProvider*,*和wait_for_transaction_receiptglob来自 Python 标准库。它的目的是从目录中过滤文件:

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

我们使用套接字连接到以太坊节点:

address = 'fa146d7af4b92eb1751c3c9c644fa436a60f7b75'

这是我们默认账户的地址。您怎么知道?您可以在连接到这个私有以太坊区块链的脚本中使用w3.eth.coinbase找到它,或者您可以查看chains/localblock/chain_data/keystore目录中的文件名。在初始化和运行私有以太坊区块链后,只有一个文件名。现在,在您初始化另外 10 个账户后,文件的数量自然会变成 11:

with open('chains/localblock/password') as f:
    password = f.read().rstrip("\n")

解锁默认账户的密码存储在chains/localblock/password的纯文本文件中:

    encrypted_private_key_file = glob.glob('chains/localblock/chain_data/keystore/*' + address)[0]
    with open(encrypted_private_key_file) as f2:
        private_key = w3.eth.account.decrypt(f2.read(), password)

找到这个之后,我们使用w3.eth.account.decrypt方法解密加密文件:

w3.eth.defaultAccount = w3.eth.coinbase

这是为了避免在创建交易时提供from参数的义务:

with open('10_accounts.txt', 'r') as f:
    accounts = f.readlines()
    for account in accounts:

我们打开了10_accounts.txt,里面包含了我们拥有的所有新账户,然后我们一个一个地迭代这些账户:

        nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(w3.eth.defaultAccount))
        transaction = {
          'to': Web3.toChecksumAddress(account.rstrip("\n")),
          'value': w3.toWei('10', 'ether'),
          'gas': 1000000,
          'gasPrice': w3.toWei('20', 'gwei'),
          'nonce': nonce
        }

在将其提供给交易对象之前,我们使用w3.eth.getTransactionCount检查最新的 nonce 值。交易对象有tovaluegasgasPrice,以及nonce键。在这里,我们想给每个账户发送 10 个以太币:

        signed = w3.eth.account.signTransaction(transaction, private_key)
        txhash = w3.eth.sendRawTransaction(signed.rawTransaction)

我们用我们的私钥对交易进行签名,然后使用w3.eth.sendRawTransaction方法将交易广播给矿工:

wait_for_transaction_receipt(w3, txhash)

这很重要。如果您只向一个账户发送资金,您可以跳过它。但是,由于我们按顺序广播了 10 笔交易,您必须在广播下一笔交易之前等待每笔交易先确认。

这样想:您广播了一个 nonce 为 3 的发送 10 个以太币的交易,然后矿工需要时间来确认这笔交易。但是,在短时间内,您广播了一个新的 nonce 为 4 的交易。得到这笔交易的矿工会向您抱怨,因为您试图从 nonce 2 跳到 nonce 4。请记住,nonce 3 的交易需要时间来确认。

执行文件后,您可以检查您的 10 个账户每个都有 10 个以太币。

让我们基于智能合约创建我们的简单去中心化投票应用。离开voting_project,创建一个新目录来包含我们的应用。创建目录后,输入以下内容:

(voting-venv) $ mkdir voting_dapp
(voting-venv) $ cd voting_dapp

让我们创建一个订阅Voting事件的程序。将此文件命名为watch_simple_voting.py

from web3 import Web3, IPCProvider

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

false = False
true = True
abi = …. # Take the abi from voting_projects/build/contracts.json.

with open('address.txt', 'r') as f:
    content = f.read().rstrip("\n")

address = content

SimpleVoting = w3.eth.contract(address=address, abi=abi)

event_filter = SimpleVoting.events.Voting.createFilter(fromBlock=1)

import time
while True:
    print(event_filter.get_new_entries())
    time.sleep(2)

现在,让我们逐行讨论这个程序:

from web3 import Web3, IPCProvider

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

We connect to private Ethereum blockchain using socket.

false = False
true = True
abi = …. # Take the abi from voting_projects/build/contracts.json.

我们需要abi来连接到一个智能合约。您可以从智能合约的编译中获得这个。由于abi是一个json对象,其中有一个布尔值设置为truefalse,而 Python 的布尔值是TrueFalse(注意大写),我们需要调整它:

with open('address.txt', 'r') as f:
    content = f.read().rstrip("\n")

address = content

要连接到一个智能合约,您需要一个地址。这是部署脚本中的地址。您也可以将地址设置为代码中硬编码的地址,如下所示:

address = '0x993FFADB39D323D8B134F6f0CdD83d510c45D306'

但是,我更喜欢把它放在一个外部文件中:

event_filter = SimpleVoting.events.Voting.createFilter(fromBlock=1)

这是为了创建一个订阅SimpleVoting智能合约的Voting事件。语法如下:

<name of smart contract>.events.<name of event>.createFilter(fromBlock=1)

fromBlock是历史指针。块越低,历史越早:

import time
while True:
    print(event_filter.get_new_entries())
    time.sleep(2)

然后,我们订阅投票事件。您会得到类似于这样的东西:

[]
[]
[]

让这个脚本运行。不要退出应用程序。打开一个新的终端,执行我们的虚拟环境脚本,并进入voting_dapp项目。这样做后,创建一个新的脚本,并将其命名为simple_voting_client.py。参考以下 GitLab 链接中的代码文件获取完整的代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_dapp/simple_voting_client.py

from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
import glob

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

with open('client_address.txt', 'r') as f:
    content = f.read().rstrip("\n")

address = content.lower()

...
...

signed = w3.eth.account.signTransaction(txn, private_key=private_key)
w3.eth.sendRawTransaction(signed.rawTransaction)

现在,让我们逐行讨论这个。我们从脚本的顶部开始:

from web3 import Web3, IPCProvider
from populus.utils.wait import wait_for_transaction_receipt
import glob

w3 = Web3(IPCProvider(ipc_path='/tmp/geth.ipc'))

with open('client_address.txt', 'r') as f:
    content = f.read().rstrip("\n")

address = content.lower()

encrypted_private_key_file = glob.glob('../voting_project/chains/localblock/chain_data/keystore/*' + address)[0]
with open(encrypted_private_key_file) as f:
    password = 'password123'
    private_key = w3.eth.account.decrypt(f.read(), password)
    w3.eth.defaultAccount = '0x' + address

这里的逻辑与之前的脚本相同。您首先使用password123打开加密文件。然后在client_address.txt文件中设置选民的账户地址,以使此脚本灵活。您可以在脚本中硬编码选民的账户地址:

false = False
true = True
abi = …

在这里,您以通常的方式从智能合约编译中设置abi

with open('address.txt', 'r') as f:
    content = f.read().rstrip("\n")

smart_contract_address = content

SimpleVoting = w3.eth.contract(address=smart_contract_address, abi=abi)

请记住,在这个脚本中有两个地址。第一个是选民或客户的地址。第二个是智能合约的地址。然后,您需要获取 nonce:

nonce = w3.eth.getTransactionCount(Web3.toChecksumAddress(w3.eth.defaultAccount))

在构建交易时,您使用此 nonce:

txn = SimpleVoting.functions.vote(0).buildTransaction({
        'gas': 70000,
        'gasPrice': w3.toWei('1', 'gwei'),
        'nonce': nonce
      })

这是vote函数。在这里,我们为索引为0的提案投票,即b'messi'。您提交gasgasPricenonce,并且省略from,因为您已经设置了w3.eth.defaultAccount

signed = w3.eth.account.signTransaction(txn, private_key=private_key)
w3.eth.sendRawTransaction(signed.rawTransaction)

最后几行是用于签名和广播交易的。

执行脚本,然后转到您运行watch_simple_voting.py脚本的终端。然后您会得到类似于这样的东西:

[]
[]
[]
[]
[AttributeDict({'args': AttributeDict({'_from': '0xf0738EF5635f947f13dD41F34DAe6B2caa0a9EA6', '_proposal': 0}), 'event': 'Voting', 'logIndex': 0, 'transactionIndex': 0, 'transactionHash': HexBytes('0x61b4c59425a6305af4f2560d1cd10d1540243b1f74ce07fa53a550ada2e649e7'), 'address': '0x993FFADB39D323D8B134F6f0CdD83d510c45D306', 'blockHash': HexBytes('0xb458542d9bee85ed7673d94f036e55f8daca188e5871cc910eb49cf4895964a0'), 'blockNumber': 3110})]
[]
[]
[]
[]
[]
[]

就是这样。在实际应用中,此事件可用于在分散式应用程序中提供通知。然后,您可以更新投票的排名或其他任何您喜欢的内容。

您还可以从一开始获取所有事件。还记得获取事件的代码吗?如下所示:

import time
while True:
    print(event_filter.get_new_entries())
    time.sleep(2)

您可以使用get_all_entries而不是get_new_entries来检索从一开始的所有事件,如下所示:

event_filter.get_all_entries()

开发商业投票应用程序

让我们将我们的智能合约升级为商业智能合约。为了投票,选民需要支付一小笔钱。这类似于美国偶像,人们通过短信投票来决定谁获胜。

返回voting_project目录,打开contracts目录中的新文件,命名为CommercialVoting.vy。有关此代码块的完整代码,请参考以下 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/contracts/CommercialVoting.vy

struct Proposal:
    name: bytes32
    vote_count: int128

proposals: public(map(int128, Proposal))

voters_voted: public(map(address, int128))

manager: public(address)

...
...

@public
def withdraw_money():
    assert msg.sender == self.manager

    send(self.manager, self.balance)

这个智能合约类似于SimpleVoting.vy,但具有额外的支付功能。我们不会逐行讨论它,但我们将看一下之前的智能合约和这个之间的区别:

@public
def __init__(_proposalNames: bytes32[2]):
    for i in range(2):
        self.proposals[i] = Proposal({
            name: _proposalNames[i],
            vote_count: 0
        })
    self.manager = msg.sender

在这个构造函数中,我们保存了启动智能合约的账户的地址:

@public
@payable
def vote(proposal: int128):
    assert msg.value >= as_wei_value(0.01, "ether")
    assert self.voters_voted[msg.sender] == 0
    assert proposal < 2 and proposal >= 0

    self.voters_voted[msg.sender] = 1
    self.proposals[proposal].vote_count += 1

在这个vote函数中,我们添加了@payable装饰器,以便人们在想要投票时可以发送资金。除此之外,我们要求最低支付为0.01以太币,使用此语句:assert msg.value >= as_wei_value(0.01, "ether")

@public
def withdraw_money():
    assert msg.sender == self.manager

    send(self.manager, self.balance)

当然,我们必须创建一个从智能合约中提取以太币的函数。在这里,我们将以太币发送到经理账户。

现在,让我们继续测试智能合约。在tests目录中创建测试文件,命名为test_commercial_voting.py。有关完整代码,请参考以下 GitLab 链接中的代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/tests/test_commercial_voting.py

import pytest
import eth_tester

@pytest.fixture()
def voting(chain):
    CommercialVotingFactory = chain.provider.get_contract_factory('CommercialVoting')
    deploy_txn_hash = CommercialVotingFactory.constructor([b'Messi', b'Ronaldo']).transact()
    contract_address = chain.wait.for_contract_address(deploy_txn_hash)
    return CommercialVotingFactory(address=contract_address)

...
...

    assert abs((after_withdraw_balance - initial_balance) - web3.toWei('1', 'ether')) < web3.toWei('10', 'gwei')

让我们逐个讨论测试函数:

def test_initial_state(voting, web3):
    assert voting.functions.manager().call() == web3.eth.coinbase

这是为了测试经理变量是否指向启动智能合约的账户。请记住,web3.eth.coinbase是默认账户。测试投票是否需要一定数量的以太币和账户,我们可以从t.get_accounts()中获取:

def test_vote_with_money(voting, chain, web3):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]
    account3 = t.get_accounts()[2]

    set_txn_hash = voting.functions.vote(0).transact({'from': account2,
                                                      'value': web3.toWei('0.05', 'ether')})
    chain.wait.for_receipt(set_txn_hash)

    set_txn_hash = voting.functions.vote(1).transact({'from': account3,
                                                      'value': web3.toWei('0.15', 'ether')})
    chain.wait.for_receipt(set_txn_hash)

    assert web3.eth.getBalance(voting.address) == web3.toWei('0.2', 'ether')

这是为了测试您可以在vote函数中发送以太币。您还测试了在智能合约中累积的以太币的余额:

def test_vote_with_not_enough_money(voting, web3):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        voting.functions.vote(0).transact({'from': account2,
                                           'value': web3.toWei('0.005', 'ether')})

这是为了测试在您想要投票时,您需要发送至少0.01以太币:

def test_manager_account_could_withdraw_money(voting, web3, chain):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    set_txn_hash = voting.functions.vote(0).transact({'from': account2, 'value': web3.toWei('1', 'ether')})
    chain.wait.for_receipt(set_txn_hash)

    initial_balance = web3.eth.getBalance(web3.eth.coinbase)
    set_txn_hash = voting.functions.withdraw_money().transact({'from': web3.eth.coinbase})
    chain.wait.for_receipt(set_txn_hash)
    after_withdraw_balance = web3.eth.getBalance(web3.eth.coinbase)

    assert abs((after_withdraw_balance - initial_balance) - web3.toWei('1', 'ether')) < web3.toWei('10', 'gwei')

这是这个智能合约中最重要的测试之一。它旨在测试您是否可以正确地从智能合约中提取以太币。您可以在提取前后检查余额,并确保差额大约为 1 个以太币(因为您需要支付燃气费)。

开发基于代币的投票应用

现在,让我们在区块链上开发一个基于代币的投票应用。我所说的基于代币的投票是指,为了投票,您必须拥有在智能合约中创建的代币。如果您用这个代币投票,那么这个代币就会被销毁,这意味着您不能投两次票。在这个智能合约中,代币的数量也是有限的,不像之前的投票应用程序,无限的账户可以投票。让我们在contracts目录中编写一个智能合约,并将文件命名为TokenBasedVoting.vy。请参考以下 GitLab 链接中的代码文件获取完整代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/contracts/TokenBasedVoting.vy

struct Proposal:
    name: bytes32
    vote_count: int128

proposals: public(map(int128, Proposal))

...
...
@public
@constant
def winner_name() -> bytes32:
    return self.proposals[self.winning_proposal()].name

让我们逐行讨论这个脚本:

struct Proposal:
    name: bytes32
    vote_count: int128

proposals: public(map(int128, Proposal))

token: public(map(address, bool))
index: int128
maximum_token: int128
manager: address

您已经熟悉了proposals变量,它与之前的投票应用具有相同的目的。token是一个新变量,旨在跟踪代币的所有者。indexmaximum_token是用来计算我们分配了多少个代币的变量。请记住,我们希望限制代币的数量。manager是启动智能合约的人:

@public
def __init__(_proposalNames: bytes32[2]):
    for i in range(2):
        self.proposals[i] = Proposal({
            name: _proposalNames[i],
            vote_count: 0
        })
    self.index = 0
    self.maximum_token = 8
    self.manager = msg.sender

在构造函数中,在设置proposals变量之后,我们将index初始化为0maximum_token初始化为8。在这个智能合约中只有8个代币可用,这意味着只能尝试8次投票。manager变量初始化为启动智能合约的变量:

@public
def assign_token(target: address):
    assert msg.sender == self.manager
    assert self.index < self.maximum_token
    assert not self.token[target]
    self.token[target] = True
    self.index += 1

在这个函数中,所有者可以将代币分配给任何账户。为了指示代币的所有者,我们将true值设置给token变量,并将其键指向targetindex增加了一,所以以后我们不能创建超过maximum_token变量的代币:

@public
def vote(proposal: int128):
    assert self.index == self.maximum_token
    assert self.token[msg.sender]
    assert proposal < 2 and proposal >= 0

    self.token[msg.sender] = False
    self.proposals[proposal].vote_count += 1

在这个vote函数中,我们通过将token映射变量设置为投票者的地址键的false来销毁代币。但首先,我们必须确保投票者是代币的有效所有者,使用这个语句:assert self.token[msg.sender]。我们还必须确保在分配了所有代币之后人们可以投票。当然,就像之前的投票应用程序一样,我们增加了投票者投票的提案的计数。

让我们为基于代币的投票应用创建一个测试。为此,在tests目录中创建一个名为test_token_based_voting.py的文件。请参考以下 GitLab 链接中的代码文件获取完整代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_06/voting_project/tests/test_token_based_voting.py。将以下代码添加到新文件中:

import pytest
import eth_tester

@pytest.fixture()
def voting(chain):
    TokenBasedVotingFactory = chain.provider.get_contract_factory('TokenBasedVoting')
    deploy_txn_hash = TokenBasedVotingFactory.constructor([b'Messi', b'Ronaldo']).transact()
    contract_address = chain.wait.for_contract_address(deploy_txn_hash)
    return TokenBasedVotingFactory(address=contract_address)

...
...

    set_txn_hash = voting.functions.vote(0).transact({'from': account2})
    chain.wait.for_receipt(set_txn_hash)

让我们逐行讨论这个脚本。我们从fixture函数开始:

import pytest
import eth_tester

@pytest.fixture()
def voting(chain):
    TokenBasedVotingFactory = chain.provider.get_contract_factory('TokenBasedVoting')
    deploy_txn_hash = TokenBasedVotingFactory.constructor([b'Messi', b'Ronaldo']).transact()
    contract_address = chain.wait.for_contract_address(deploy_txn_hash)
    return TokenBasedVotingFactory(address=contract_address)

像往常一样,我们通过手动部署智能合约来创建这个智能合约的fixture

def assign_tokens(voting, chain, web3):
    t = eth_tester.EthereumTester()
    accounts = t.get_accounts()

    for i in range(1, 9):
        set_txn_hash = voting.functions.assign_token(accounts[i]).transact({'from': web3.eth.coinbase})
        chain.wait.for_receipt(set_txn_hash)

这是一个为不同账户分配8个代币的helper函数:

def test_assign_token(voting, chain):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    assert not voting.functions.token(account2).call()

    set_txn_hash = voting.functions.assign_token(account2).transact({})
    chain.wait.for_receipt(set_txn_hash)

    assert voting.functions.token(account2).call()

这个test函数旨在检查assign_token函数是否可以将代币分配给目标地址:

def test_cannot_vote_without_token(voting, chain, web3):
    t = eth_tester.EthereumTester()
    account10 = t.get_accounts()[9]

    assign_tokens(voting, chain, web3)

    with pytest.raises(eth_tester.exceptions.TransactionFailed):
        voting.functions.vote(0).transact({'from': account10})

这个test函数旨在确保只有代币的所有者可以在这个智能合约中投票:

def test_can_vote_with_token(voting, chain, web3):
    t = eth_tester.EthereumTester()
    account2 = t.get_accounts()[1]

    assign_tokens(voting, chain, web3)

    assert voting.functions.proposals__vote_count(0).call() == 0

    set_txn_hash = voting.functions.vote(0).transact({'from': account2})
    chain.wait.for_receipt(set_txn_hash)

    assert voting.functions.proposals__vote_count(0).call() == 1

这个test函数旨在确保代币的所有者可以成功为提案投票。

让我解释一下为什么基于代币的投票非常了不起。只有8个可用的代币,这些代币可以用来在这个智能合约中投票。编写和部署这个智能合约的程序员甚至在这个智能合约上线后也无法改变规则。选民可以通过要求从程序员那里获取智能合约的源代码,并验证编译的字节码是否与智能合约地址中的字节码相同来验证规则是否公平。要从智能合约地址获取字节码,你可以这样做:

from web3 import Web3, HTTPProvider

w3 = Web3(HTTPProvider('http://127.0.0.1:8545'))
print(w3.eth.getCode('0x891dfe5Dbf551E090805CEee41b94bB2205Bdd17'))

然后,你编译作者的智能合约源代码并进行比较。它们一样吗?如果是,那么你可以审计智能合约,确保没有作弊。如果不是,那么你可以向作者投诉或决定不参与他们的智能合约。

在传统的网络应用程序中实现这种透明度并不容易。在 GitHub/GitLab 中验证代码并不意味着太多,因为开发者可能在他们的服务器上部署不同的代码。你可以被授予在他们服务器上的访客会话来验证代码的透明性,但是,开发者可能会部署一种复杂的方式来欺骗你。你可以每秒监视前端的网络应用程序,并部署一种监视策略,无论是手动还是借助 ML 来检测可疑活动。例如,你突然注意到一个评论突然被修改了,但后来没有被编辑的迹象,所以你可以肯定作弊发生在应用程序内部。然而,指责开发者并不容易,因为这是你的话对他们的话。你可能会被指控制造虚假证据。

有效的方法是雇佣一个可信赖和称职的审计员来做这项工作。审计员可以访问他们的网络应用程序,并有足够的权限读取数据库日志和服务器日志,以确保没有作弊发生。这只有在审计员无法被贿赂并且足够称职以避免被开发者欺骗的情况下才能实现。或者,你可以使用区块链。

投票是一个广泛的主题。我们在这个投票应用程序中还没有实现委托功能。我所说的委托类似于许多国家的民主制度。在一些民主国家,人们不直接选择他们的总理或总统。他们选择众议院的成员。在这些成员当选后,他们将选择总理。你可以创建一个实现委托系统的投票智能合约。如果你想进一步研究这个问题,请参考进一步阅读部分。

最后,我们的投票智能合约非常透明。这可能是好事,也可能是坏事,这取决于情况。透明度是好的,特别是在金融交易中,因为你可以审计日志以发现洗钱案件。然而,当涉及到投票,特别是在政治方面,保密性是一个可取的特性。如果选民没有保密性,他们可能会害怕被其他人迫害。智能合约中的投票保密性仍处于研究阶段。

总结

在本章中,你已经学会了如何创建一个区块链技术可以发挥作用的真实应用程序。这个真实应用程序是一个投票应用程序。从每个账户都可以投票的简单投票智能合约开始,我们逐渐创建了一个只有特定账户可以使用代币系统投票的投票应用程序。在构建这个投票智能合约时,我们还学习了如何编写一个脚本来部署带有构造函数的智能合约。在部署智能合约后,我们还学习了智能合约的一个特性,即事件。在web3脚本中,我们订阅这个事件来了解我们感兴趣的事情。最后,我们创建了辅助脚本来创建许多账户,并向其他账户发送资金以进行开发目的。

在下一章中,您将为您的web3脚本创建一个前端。您将构建一个适当的去中心化应用程序,以桌面应用程序的形式。

更多阅读