Python 构建一个实用的去中心应用详解

在本章中,我们将在区块链上编写一个流行的应用,这将是一个由区块链提供动力的安全投票应用。您拥有开发此应用的所有工具,即 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/popul/github/python/bc/img/defaults.v9.config.json project.json

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

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

The latest version of Vyper is 0.1.0b6 which, breaks Populus. The developer needs some time to fix this problem. If the bug has still not been fixed by the time you are reading this book, you can patch Populus yourself.

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

(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()

在我们这里的例子中,这个 bug 还没有被修复。那么,让我们修补 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 链接—https://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 数据类型变量声明开始:

struct Proposal:
    name: bytes32
    vote_count: int128

数据结构是一个具有复合数据类型的变量,该类型包含提案的名称和提案的金额。Proposal结构中的vote_count数据类型为int128,而Proposal结构中的name数据类型为bytes32。您也可以在Proposal结构中使用uint256代替vote_countint128数据类型。不过,这不会有任何区别。然而,bytes32是一种新的数据类型。正如您在第 3 章中提到的,与 Vyper 实现智能合约,如果您想在 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'。这个字符串将被填充,直到达到 32 字节。默认情况下,应该使用bytes[20]bytes[256]作为字符串数据类型,而不是使用bytes32。那么为什么我要在这个智能合约中使用bytes32?我有一个很好的理由这样做,但我们需要先转到构造函数,以了解我使用bytes32保留提案名称的原因:*

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

这是我们第一次在智能合约中使用事件。event是 Vyper 中用于创建事件的关键字。事件是发生在我们的客户(T1 程序)想要订阅的智能合约中的事件。在此语句中,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上下文中的 Null 为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

这就是为什么我使用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的参数。在这里,用户投票支持带有整数的提案。因此,如果用户调用参数为0vote方法,如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功能旨在获取投票最多的提案的名称。此函数使用前面描述的私有函数。

这个智能合约很简单,但并不完美,因为存在一个 bug。例如,在vote函数中,我们没有处理投票的负值。除此之外,提案的数量被硬编码为 2。然而,它将完成这项工作。

然后,您可以按照通常的方式编译智能合约的代码:

(voting-venv) $ populus compile

作为一个好公民,让我们为这个智能合约写一个测试。在tests目录中创建一个名为test_simple_voting_app.py的文件。以下代码块的完整代码请参考以下 GitLab 链接:https://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)

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

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

这是为了在智能合约部署后检查其状态。有一件事在这里非常独特;Propositions 变量内的 struct 数据中 name 变量的长度为32,即使我们将其设置为b'messi'值,这也是bytes32数据类型的特点。这就是为什么我们切片变量以得到我们想要的。然后,对于下一个测试方法,除了使用voting参数外,我们还使用chain参数:

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})

这确保我们不能使用同一帐户进行多次投票。正如我们在Chapter 5Populus 开发框架中了解到的,您用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 网络或私有以太坊区块链。其次,我们的智能合约在构造函数中有一个参数。要部署带有参数的智能合约,我们需要使用不同的方法;我们不能使用Chapter 5、Populus 开发框架中展示的正常方法。在第五章【Pyplus 开发框架中,我们以populus deploy --chain localblock Donation的方式使用 Pyplus 部署了一个智能合约。

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']])

它接收两个关键字参数,transactionargs。transaction 参数是一个事务字典。这里,我们设置from参数。chain.web3.eth.coinbase是我们的默认账户,这在testing/development场景中很常见。这里,我们使用没有私钥的默认帐户。在这个交易对象中,我们还可以设置gas、gasPrice 等交易参数。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对象是区块链中描述交易确认的对象。在此上下文中,我们关注的是地址,即收据对象中的contractAddress键:

if __name__ == "__main__":
    main()

设计用于执行main功能。

与 Ganache 不同,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 链接中的代码文件–https://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

您已经了解了Web3IPCProviderwait``_for_transaction_receiptglob来自 Python 标准库。其目的是从目录中筛选文件:

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

我们使用插座连接到以太坊节点:

address = 'fa146d7af4b92eb1751c3c9c644fa436a60f7b75'

这是我们的默认帐户地址。你怎么知道的?您可以在连接到此私有以太坊区块链的脚本中找到它,也可以查看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
        }

在将最新的 nonce 值提供给事务对象之前,我们使用w3.eth.getTransactionCount检查它。交易对象有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 链接中的代码文件以了解完整的 cod:https://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,由于您已经设置了w3.eth.defaultAccount,所以省略了from

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 链接中的代码文件:https://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)

当然,我们必须创建一个功能,从智能合约中撤回以太。在这里,我们将以太发送到 manager 帐户。

现在,让我们继续测试智能合约。在tests目录中创建测试文件,并将其命名为test_commercial_voting.py。有关完整代码,请参阅以下 GitLab 链接中的代码文件:https://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

这是为了测试 manager 变量指向启动智能合约的帐户。请记住,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 链接中的代码文件:https://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是计算我们分配了多少令牌的变量。记住,我们想要限制令牌的数量。该经理是启动智能合约的人:

@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初始化为0,将maximum_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函数中,我们通过将投票者的地址键设置为falsetoken映射变量来烧录令牌。但首先,我们必须使用以下语句确保投票者是令牌的有效所有者:assert self.token[msg.sender]。我们还必须确保人们在分配完所有代币后可以投票。当然,就像上次的投票申请一样,我们增加了选民投票支持的提案的票数。

让我们为基于令牌的投票应用创建一个测试。为此,在tests目录中创建一个名为test_token_based_voting.py的文件。有关完整代码,请参阅以下 GitLab 链接中的代码文件:https://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)

这是一个helper函数,用于将8代币分配给不同的账户:

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'))

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

在传统 web 应用中实现这种透明性不是一件小事。在 GitHub/GitLab 中验证代码并不重要,因为开发人员可以在他们的服务器中部署不同的代码。您可以在他们的服务器上被授予一个来宾会话来验证代码的透明性,但是,同样,开发人员可以部署一种复杂的方法来欺骗您。您可以每秒从前端监视 web 应用,并手动部署监视策略,或在 MLC 的帮助下检测可疑活动。例如,您突然注意到一条注释被突然修改,但没有迹象表明它随后被编辑,因此您可以确定作弊发生在应用内部。然而,指责开发者并不容易,因为这是你反对他们的话。你可能会被指控伪造证据。

有效的办法是聘请一名可信和称职的审计员来完成这项工作。审计员获得对其 web 应用的访问权,并有足够的权限读取数据库日志和服务器日志,以确保没有欺诈行为发生。只有当审计人员不能被贿赂并且有足够的能力避免被开发人员欺骗时,这才有效。或者,你可以使用区块链。

投票是一个广泛的话题。我们尚未在此投票应用中实现委派功能。我所说的授权与许多国家的民主相似。在一些民主国家,人们不直接选择总理或总统。他们选出众议院议员。这些人当选后,成员们将选出总理。您可以创建实现委派系统的投票智能合约。如果您想进一步研究,请参阅进一步阅读部分。

最后,我们的投票智能合约非常透明。这可能是好的,也可能是坏的,这取决于具体情况。透明度很好,尤其是在金融交易中,因为你可以通过审计日志来发现洗钱案件。然而,当涉及到投票时,尤其是在政治领域,保密是一种可取的财产。如果选民不保密,他们可能会害怕受到他人的迫害。智能合约投票的保密性仍处于研究阶段。

在本章中,您已经学习了如何创建一个区块链技术可以发挥作用的现实世界应用。这个真实世界的应用是一个投票应用。从每个帐户都可以投票的简单投票智能合约,我们逐渐创建了一个投票应用,其中只有某些帐户可以使用代币系统投票。在构建这个投票智能合约时,我们还学习了如何编写脚本来与构造函数部署智能合约。在部署智能合约之后,我们还从智能合约中学到了一个特性,这是一个事件。在一个web3脚本中,我们订阅这个活动来了解我们感兴趣的事情。最后,我们创建了助手脚本来创建许多帐户,并将资金发送到其他帐户以用于开发目的。

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

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

MySQL实战45讲 -〔林晓斌〕

摄影入门课 -〔小麥〕

检索技术核心20讲 -〔陈东〕

流程型组织15讲 -〔蒋伟良〕

陶辉的网络协议集训班02期 -〔陶辉〕

数据分析思维课 -〔郭炜〕

如何讲好一堂课 -〔薛雨〕

陈天 · Rust 编程第一课 -〔陈天〕

说透元宇宙 -〔方军〕