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

397 阅读1小时+

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

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

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

区块链被视为所有加密货币交易的公共账本的主要技术解决方案。本书是开发完全成熟的使用 Python 与区块链应用程序的各种构建模块进行交互的去中心化应用程序的实用指南。

面向 Python 开发人员的区块链实战首先演示了区块链技术和加密货币哈希是如何工作的。您将了解智能合约的基本原理和好处,比如抗审查和交易准确性。随着您的稳步进展,您将开始使用类似 Python 的 Vyper 构建智能合约。这种经验将进一步帮助您揭开智能合约的其他好处,比如可靠的存储和备份,以及效率。您还将使用 web3.py 与智能合约进行交互,并利用 web3.py 和 Populus 框架的力量构建提供安全性和与加密货币无缝集成的去中心化应用程序。随着您探索后续章节,您将学习如何在以太坊上创建自己的代币,并使用 PySide2 库构建一个可以处理以太坊和以太坊请求的评论(ERC-20)代币的加密货币钱包图形用户界面(GUI)。这将使用户能够无缝地存储、发送和接收数字货币。最后,您将在您的去中心化应用程序中实现星际文件系统(IPFS)技术,以提供一个可以存储和公开媒体的点对点文件系统。

通过本书,您将精通区块链编程,并能够使用 Python 在各种领域构建端到端的去中心化应用程序。

这本书是为谁准备的

如果您是一名想要进入区块链世界的 Python 开发人员,面向 Python 开发人员的区块链实战适合您。本书将成为您精通区块链生态系统并使用 Python 和库支持构建自己的去中心化应用程序的指南。

这本书涵盖了什么

第一章,区块链编程简介,讲述了比特币的故事以及比特币的价值所在。您将了解赋予比特币力量的基础技术,即区块链技术。此外,您还将了解以太坊最初的创建目的。

第二章,智能合约基础,展示了传统程序和智能合约之间的区别。您将了解传统程序存在哪些缺陷,以及为什么智能合约有潜力克服这些缺陷。您还将看到智能合约现在正在被应用在哪些领域。

第三章,使用 Vyper 实现智能合约,教您如何使用类似 Python 的 Vyper 编程语言编写智能合约。您将学习 Vyper 编程语言的许多重要特性。

第四章,使用 Web3 与智能合约交互,向您展示如何安装web3.py库,如何与智能合约交互,以及如何部署智能合约。

第五章,Populus 开发框架,向您展示如何使用 Populus 开发框架,并认识到它对开发人员的价值。

第六章,构建实用的去中心化应用程序,教您如何使用web3.py库和 Populus 开发框架构建去中心化应用程序。

第七章,前端去中心化应用程序,向您展示如何使用桌面前端构建类似 Twitter 的去中心化应用程序。

第八章,在以太坊中创建代币,教你如何创建自己的代币。这是一个关于如何启动自己的加密货币的实践学习指南。

第九章,加密货币钱包,向您展示如何使用桌面前端构建以太坊钱包。

第十章,星际文件系统-一个全新的文件系统,是对星际文件系统的介绍,人们可以在其中存储分布式文件。在区块链中,存储是昂贵的。在区块链上存储图像文件(更不用说视频文件了)已经是不可行的。IPFS 是一种新技术,旨在解决这个问题。您将了解 IPFS 是什么,以及这项技术目前的状态。

第十一章,使用 ipfsapi 与 IPFS 交互,教你如何使用 Python 库连接到 IPFS 节点。

第十二章,使用 IPFS 实现去中心化应用,向您展示如何实现一个利用 IPFS 技术的去中心化视频分享应用。

充分利用本书

必须具备 Python 的基本知识。本书适用于希望从事区块链开发的 Python 开发人员。

下载示例代码文件

您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,文件将直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. 登录或注册www.packt.com

  2. 选择 SUPPORT 选项卡。

  3. 点击“代码下载和勘误”。

  4. 在搜索框中输入书名,然后按照屏幕上的说明操作。

文件下载后,请确保使用最新版本的解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Hands-On-Blockchain-for-Python-Developers。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自我们丰富的书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图像

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788627856_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。例如:"如果你在 Linux 平台上,你将下载这个文件:qt-unified-linux-x64-3.0.5-online.run。"

代码块设置如下:

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

任何命令行输入或输出都是这样写的:

$ python3.6 -m venv qt-venv $ source qt-venv/bin/activate (qt-venv) $ pip install PySide2

粗体:表示一个新术语、一个重要单词或者屏幕上看到的单词。例如,菜单中的单词或对话框中的单词会出现在文本中,就像这样。例如:"点击下一步。然后你会看到一个登录屏幕。"

警告或重要说明会出现在这样的地方。提示和技巧会出现在这样的地方。

第一部分:区块链和智能合约

介绍区块链技术、智能合约和去中心化应用。

本节将涵盖以下章节:

  • 第一章,区块链编程简介

  • 第二章,智能合约基础

  • 第三章,使用 Vyper 实现智能合约

第一章:区块链编程简介

在本书中,我们将学习区块链编程,以便在寻找区块链机会时成为一个不可忽视的力量。为了实现这一点,你需要从理解区块链技术及其包含的内容开始。在本章中,我们将学习什么是区块链技术。区块链如何赋予比特币和以太坊力量?我们将直观地理解区块链技术。我们还将复制区块链背后的一些基本功能。

本章将涵盖以下主题:

  • 加密货币和区块链的崛起

  • 区块链技术

  • 密码学

  • 哈希函数

  • 共识

  • 区块链上的编码

加密货币和区块链的崛起

假设你在 2017 年不是隐居在山上的隐士,你一定听说过加密货币,尤其是比特币。你不必远去就能听到这个话题的风靡,它的术语和价值的增长。在这一点上,其他加密货币也开始增长,为**以太坊达到 1000 美元!**等标题铺平了道路。在这狂热中,人们讨论了有关加密货币的一切,从价格的波动到其背后的技术,即区块链。

区块链被视为将为人类带来新时代的正义和繁荣的技术。它将使财富民主化。它将夺取寡头的权力并归还给人民。它将保护人民的数据。然后到了 2018 年,加密货币下跌了。派对结束了。比特币现在的价格是 6000 美元,而以太坊的价格不到 400 美元。

然而,尽管围绕加密货币的炒作已经平息,但它仍然是一个经常讨论的话题。区块链会议和聚会在许多地方涌现,而投资继续涌入区块链初创公司。硅谷的巨头安德烈森·霍洛维茨从其有限合伙人那里获得了高达 3 亿美元的专门区块链基金。在这种情况下,机会就在资金流入的地方。区块链开发者的首席招聘官 Katheryn Griffith Hill 声称,目前每个区块链开发者有 14 个职位空缺。此外,我参加了雅加达的一个本地区块链活动的一位朋友评论说,我看到大约 100 名观众,但只有大约四五名开发者。50%的观众是投资者。有人想把钱投入区块链,但能够开发产品的人却更少。

区块链开始被用作无中间人的支付解决方案,即比特币。然后,人们发现区块链还具有一些其他有趣的特性。首先,它是透明的,意味着人们可以审计它,以检查是否存在洗钱行为。其次,它在一定程度上为用户提供隐私,可以用来避免个人资料被追踪。

然后,在以太坊发布后,人们突然开始创造如何在现实生活中应用区块链。从创建代表某物所有权的代币,比如自治组织或具有完全隐私支付的数字资产,到不能复制的数字资产(不像 MP3 文件)。

区块链技术

大多数人知道比特币存在是因为区块链。但区块链是什么?它是一个仅追加的数据库,由哈希链接的区块组成。在这里,每个区块包含许多由加密技术保护的价值转移交易(但也可能是其他东西);持有相同数据库的许多节点之间的共识决定下一个要追加的新区块。

你现在不必理解这个定义;这些都是很多需要消化的话!首先,我会向你解释区块链,这样你就可以适应这个新知识,随着我们在本书中的进展。

回到区块链的定义,我们可以总结定义为只能追加的数据库。一旦你把东西放入数据库,它就不能被改变;没有撤销。我们将在第二章 智能合约基础中讨论这一特性的影响。这个定义涉及许多事情,并开启了一个全新的世界。

那么,你可以把什么放入这个只能追加的数据库中呢?这取决于加密货币。对于比特币,你可以存储价值转移的交易。例如,纳尔逊向迪安发送 1 比特币。然而,在将它们附加到数据库之前,我们将许多交易累积到一个区块中。对于以太坊,你可以将更丰富的东西放入只能追加的数据库中。这不仅包括价值转移的交易,还可以是状态的改变。我在这里所说的状态是非常一般的。例如,购买演出门票的队列可以有一个状态。这个状态可以是空的或者满的。与比特币类似,在以太坊中,你需要在将所有交易一起附加到这个只能追加的数据库之前收集它们。

为了更清楚,我们将所有这些交易放入区块中,然后再将它们附加到只能追加的数据库中。除了交易列表,我们还在这个区块中存储其他东西,比如我们将区块追加到只能追加的数据库中的时间,目标的难度(如果你不了解这个,不用担心),以及父哈希(我马上会解释这个),以及其他许多东西。

现在你了解了区块链的区块元素,让我们来看看链元素。如前所述,除了交易列表,我们还将父哈希放在区块中。但现在,让我们只使用一个简单的 ID 来表示父级,而不是使用哈希。父 ID只是上一个区块的 ID。在这里,想象一下堆栈。一开始,没有区块。相反,我们放置了区块 A,其中包含三笔交易:交易 1交易 2交易 3。由于区块 A是第一个区块,它没有父级。然后,我们将区块 B应用到区块 A,其中包括两笔交易:交易 4交易 5区块 B不是这个区块链中的第一个区块。因此,我们将区块 B中的父级部分设置为区块 A的 ID,因为区块 A区块 B的父级。然后,我们将区块 C放入区块链中,其中包含两笔交易:交易 6交易 7

区块 C中的父级部分将是区块 B的 ID,依此类推。为了简化事情,我们为每个新区块从 0 开始递增 ID:

让我们实现一个数据库来记录人们喜欢和讨厌的历史。这意味着当你在历史上某个时刻说你喜欢猫时,你将无法改变那段历史。当你改变主意时(例如,如果你后来讨厌猫),你可以添加新的历史,但这不会改变你过去喜欢它们的事实。因此,我们可以看到在过去你喜欢猫,但现在你讨厌它们。我们希望使这个数据库充满诚信并且安全防止作弊。看一下以下代码块:

class Block:
    id = None
    history = None
    parent_id = None

block_A = Block()
block_A.id = 1
block_A.history = 'Nelson likes cat'

block_B = Block()
block_B.id = 2
block_B.history = 'Marie likes dog'
block_B.parent_id = block_A.id

block_C = Block()
block_C.id = 3
block_C.history = 'Sky hates dog'
block_C.parent_id = block_B.id

如果你学过计算机科学,你会认识到这种数据结构,它被称为链表。现在,有一个问题。假设玛丽讨厌纳尔逊,并希望给纳尔逊抹黑。玛丽可以通过改变区块 A 的历史来做到这一点:

block_A.history = 'Nelson hates cat'

这对喜欢猫的纳尔逊是不公平的。因此,我们需要添加一种只有纳尔逊才能写下自己偏好历史的方法。这样做的方法是使用私钥和公钥。

在区块链中签署数据

在区块链中,我们使用两个密钥对数据进行签名,以验证消息的真实性并保护免受未经授权的用户篡改。这两个密钥如下:

  • 私钥

  • 公钥

私钥的保密性受到保护,不向公众公开。另一方面,您可以将公钥公开。您可以告诉每个人,嘿,这是我的公钥

让我们生成私钥。为此,我们需要openssl软件。您可以通过以下方式安装它:

$ sudo apt-get install openssl

因此,Nelson 生成私钥,即nelsonkey.pem文件。他必须保守这个密钥。生成如下:

$ openssl genrsa -out nelsonkey.pem 1024

从私钥中,Nelson 生成公钥:

$ openssl rsa -in nelsonkey.pem -pubout > nelsonkey.pub

Nelson 可以与所有人分享这个公钥nelsonkey.pub。现实世界中,我们可以建立一个简单的公钥及其所有者的字典,如下所示:

{
'Nelson': 'nelsonkey.pub',
'Marie': 'mariekey.pub',
'Sky': 'skykey.pub'
}

我们现在将看一下 Nelson 如何证明他是唯一能够对其历史进行更改的人。

首先,让我们创建一个 Python 虚拟环境:

$ python3 -m venv blockchain
$ source blockchain/bin/activate
(blockchain) $

接下来,安装库:

(blockchain) $ pip install --upgrade pip
(blockchain) $ pip install wheel
(blockchain) $ pip install cryptography

这是可以用来签署消息的 Python 脚本。将此脚本命名为verify_message.py(请参考以下 GitLab 链接中的代码文件获取完整代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_01/verify_message.py):

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

# Generate private key
#private_key = rsa.generate_private_key(
# public_exponent=65537,
# key_size=2048,
# backend=default_backend()
#)
...
...

# Message validation executed by other people
public_key.verify(
    signature,
    message,
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256())

执行此脚本时,如预期的那样,不会发生任何事情。这意味着消息已通过公钥的签名进行了验证。签名只能由 Nelson 创建,因为您需要私钥才能创建签名。但是,要使用签名验证消息,您只需要公钥。

让我们看一个案例,Marie 试图使用名为falsify_message.py的脚本伪造事实。Marie 试图将Nelson hates cat放入历史数据库中,如下所示:

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

message = b'Nelson hates cat'
signature = b'Fake Signature'

with open("nelsonkey.pub", "rb") as key_file:
    public_key = serialization.load_pem_public_key(
        key_file.read(),
        backend=default_backend())

public_key.verify(
 signature,
 message,
 padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256())

这就是验证方法的工作原理。Nelson 计算消息的哈希值,然后用他的私钥对其进行加密。结果就是签名。例如,如果 Sky 想要验证签名,他有消息和签名。他计算消息的哈希值。然后,他使用公钥解密签名。结果与消息的哈希值进行比较。如果相同,那么一切正常。如果不同,要么消息已被更改,要么用于签署消息的私钥不同。

这样做时,您将获得以下输出:

那么签名是什么样的?回到verify_message.py,并将以下行附加到文件末尾。然后再次运行脚本:

print(signature)

签名看起来像这样:

每条消息都有不同的签名,Marie 不可能猜到签名以伪造消息。因此,有了私钥和公钥,我们可以验证消息是否确实来自授权人员,即使我们在不安全的通道上进行通信。

因此,有了私钥,Nelson 可以创建一个对其尝试签署的消息唯一的签名:

世界上每个拥有 Nelson 公钥的人都可以验证 Nelson 确实写了消息 A。Nelson 可以通过展示签名 A来证明他确实写了消息 A。每个人都可以获取这两个输入并验证真相:

因此,要验证是否是 Nelson 写的Nelson likes cat,请输入以下内容(请参考以下 GitLab 链接中的代码文件获取完整代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_01/validate_message.py):

# validate_message.py
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization

def fetch_public_key(user):
    with open(user + "key.pub", "rb") as key_file:
        public_key = serialization.load_pem_public_key(
           key_file.read(),
           backend=default_backend())
    return public_key

# Message coming from user
message = b"Nelson likes cat"

# Signature coming from user, this is very specific to public key.
# Download the public key from Gitlab repository of this code so this signature matches the message.
# Otherwise, you should generate your own signature.
signature = 
...
...
    padding.PSS(mgf=padding.MGF1(hashes.SHA256()),
                salt_length=padding.PSS.MAX_LENGTH),
    hashes.SHA256())

从链表到区块链

现在我们知道只有尼尔森能写尼尔森喜欢猫尼尔森讨厌猫,我们可以安心了。然而,为了使教程代码简短,我们不会集成使用私钥和公钥进行验证。我们假设只有授权的人能够在区块中写历史。看一下以下代码块:

>>> block_A.history = 'Nelson likes cat'

当发生这种情况时,我们假设是尼尔森写了那段历史。那么,用链表记录数据的问题是什么呢?

问题在于数据可以很容易地被更改。比如尼尔森想成为一名参议员。如果他的选区有很多人不喜欢猫,他们可能不会喜欢尼尔森喜欢它们的事实。因此,尼尔森想要更改历史:

>>> block_A.history = 'Nelson hates cat'

就像这样,历史已经改变了。我们可以通过每天在区块中记录所有历史的方式来避免这种作弊。因此,当尼尔森改变数据库时,我们可以将今天区块链中的数据与昨天区块链中的数据进行比较。如果不同,我们可以确认发生了可疑的事情。这种方法可能有效,但让我们看看是否能想出更好的办法。

让我们将我们的链表升级为区块链。为此,我们在Block类中添加一个新属性,即父哈希:

import hashlib
import json

class Block:
    id = None
    history = None
    parent_id = None
    parent_hash = None

block_A = Block()
block_A.id = 1
block_A.history = 'Nelson likes cat'

block_B = Block()
block_B.id = 2
block_B.history = 'Marie likes dog'
block_B.parent_id = block_A.id
block_B.parent_hash = hashlib.sha256(json.dumps(block_A.__dict__).encode('utf-8')).hexdigest()

block_C = Block()
block_C.id = 3
block_C.history = 'Marie likes dog'
block_C.parent_id = block_B.id
block_C.parent_hash = hashlib.sha256(json.dumps(block_B.__dict__).encode('utf-8')).hexdigest()

让我们演示hashlib()函数的作用:

>>> print(block_B.__dict__)
{'parent_hash': '880baef90c77ae39d49f364ff1074043eccb78717ecec85e5897c282482012f1', 'history': 'Marie likes dog', 'id': 2, 'parent_id': 1}
>>> print(json.dumps(block_B.__dict__))
{"parent_hash": "880baef90c77ae39d49f364ff1074043eccb78717ecec85e5897c282482012f1", "parent_id": 1, "history": "Marie likes dog", "id": 2}
>>> print(json.dumps(block_B.__dict__).encode(‘utf-8'))
b'{"id": 2, "parent_hash": "69a1db9d3430aea08030058a6bd63788569f1fde05adceb1be6743538b03dadb", "parent_id": 1, "history": "Marie likes dog"}'
>>> print(hashlib.sha256(json.dumps(block_B.__dict__).encode('utf-8')))
<sha256 HASH object @ 0x7f58518e3ee0>
>>> print(hashlib.sha256(json.dumps(block_B.__dict__).encode('utf-8')).hexdigest())
25a7a88637c507d33ae1402ba6b0ee87eefe9c90e33e75c43d56858358f1704e

如果我们改变block_A的历史,以下代码看起来像这样:

>>> block_A.history = 'Nelson hates cat'

再次,历史已经被改变就像那样。然而,这一次有一个转折。我们可以通过打印block_C的原始父哈希来验证这一变化已经发生:

>>> print(block_C.parent_hash)
ca3d23274de8d89ada13fe52b6000afb87ee97622a3edfa3e9a473f76ca60b33

现在,让我们重新计算每个区块的父哈希:

>>> block_B.parent_hash = hashlib.sha256(json.dumps(block_A.__dict__).encode('utf-8')).hexdigest()
>>> block_C.parent_hash = hashlib.sha256(json.dumps(block_B.__dict__).encode('utf-8')).hexdigest()
>>> print(block_C.parent_hash)
10b7d80f3ede91fdffeae4889279f3acbda32a0b9024efccc9c2318e2771e78c

这些区块是不同的。通过观察这些,我们可以非常确定历史已经被更改。因此,尼尔森将被当场抓住。现在,如果尼尔森想要更改历史而不被抓住,仅仅更改block_A中的历史已经不够了。尼尔森需要更改每个区块中的parent_hash属性(当然除了block_A)。这是更难的作弊。仅有三个区块,尼尔森就需要更改两个parent_hash属性。有了 1000 个区块,尼尔森就需要更改 999 个parent_hash属性!

密码学

区块链最流行的用途是创建加密货币。由于加密货币中有crypto这个词,你可能会认为你需要精通密码学才能成为区块链程序员。这是不正确的。你只需要了解密码学的两件事:

  • 私钥和公钥(非对称加密)

  • 哈希

这两个在本章的前一部分已经解释过了。你不需要知道如何设计哈希算法或私钥和公钥算法。你只需要对它们的工作方式和这些技术的含义有直观的理解。

私钥和公钥的含义是它实现了去中心化账户。在普通应用中,你有一个用户名和密码。这两个字段使某人能够访问他们的账户。但是拥有私钥和公钥使某人能够以去中心化的方式拥有一个账户。

对于哈希,它是一个单向函数,意味着给定一个输入,你可以轻松地得到输出。但是给定一个输出,你无法得到输入。一个简单版本的单向函数可能是这样的:

这是一个附加过程。如果我告诉你这个函数的一个输出是 999,然后问你输入是什么,你无法猜出答案。它可以是从 1 和 998 到 500 和 499 的任何东西。哈希函数就像这样。算法就像天空一样清晰(你可以在互联网上阅读任何哈希函数的算法),但是很难逆转算法。

因此,关于哈希,你需要知道的是:给定输入 input,你会得到这个 SHA-256 输出(十六进制):c96c6d5be8d08a12e7b5cdc1b207fa6b2430974c86803d8891675e76fd992c20。如果你不知道输入,你就无法仅凭这个输出得到输入。假设你知道输入 input,要找到另一个产生相同输出的输入是非常困难的。我们甚至不知道是否存在这样的输入。

这就是你成为区块链开发者时需要了解的有关加密的一切。但是,只有当你成为某种类型的区块链开发者时才是真的,这种开发者会在以太坊之上创建程序。

对称和非对称加密

对称加密使用发送方和接收方之间相同的密钥。这个密钥用于加密和解密消息。例如,你想创建一个加密函数来加密文本。对称加密可能就是简单地将文本加 5。如果 A(或者 ASCII 中的 65)是要加密的文本,那么这个加密函数将 65 加 5。加密后的文本将是 F(或者 ASCII 中的 71)。要解密它,只需从加密后的文本 F 中减去 5。

非对称加密是一种不同的东西。有两个密钥:公钥和私钥。它们之间有一个特殊的数学关系。如果你用公钥加密一条消息,你只能用私钥解密它。如果你用私钥加密一条消息,你只能用公钥解密它。与对称密钥(加减相同的数字)之间的关系不同,公钥和私钥之间没有直接的关系。有几种非对称加密算法。我将解释最简单的一种,即 RSA 算法。

生成两个素数,称为 pq。它们应该是非常大的数字(至少有数百位数),但是对于这个例子,我们选择了较小的数字:11 和 17。这些是你的私钥。不要让别人知道这些数字:

n = p x q

n 是一个合数。在我们的例子中,n187

然后,我们找到 e 数,它应该与 (p-1)x(q-1) 互质:

(p-1) x (q-1) = 160

互质意味着 e(p-1) x (q-1) 除了 1 之外不能被任何数字因数分解。除了 1 之外,没有其他数字可以整除它们而不产生余数。因此,e7。但是,e 也可以是 11。在这个例子中,我们选择 7 作为 e

en 是你的公钥。你可以把这些数字告诉你在公交车上遇到的陌生人、你的祖母、友好的邻居或者你的约会对象。

假设我们要加密的消息是 A。在现实世界中,加密这样一个短消息是不安全的。我们必须填充这个短消息。因此,A 将变成类似 xxxxxxxxxxxxxxxxxxxA。如果你查看本章早些时候加密消息的脚本,你会看到有一个填充函数。但是在这个例子中,我们不会填充消息。

加密函数是这样的:

encrypted_message = messagee (mod n)

因此,encrypted_message 将是 65 ** 7 % 187 = 142

在我们能够解密消息之前,我们需要找到 d 数:

e x d = 1 (mod (p-1) x (q-1))

d23

解密函数是这样的:

decrypted_message = encrypted_messaged mod n

因此,decrypted_message 将是 142 ** 23 % 187 = 65。65 在 ASCII 中是 A。

显然,x^y mod n 很容易计算,但是找到整数模 ny 次根确实很难。我们称之为陷门置换。对 n 因数分解以找到 pq 是非常困难的(从公钥生成私钥)。但是,从 pq 找到 n 是容易的(从私钥生成公钥)。这些属性使得非对称加密成为可能。

与对称加密相比,非对称加密使人们能够在不需要先交换密钥的情况下进行安全通信。你有两个密钥(私钥和公钥)。你把公钥给任何人。你只需要保护私钥的保密性。私钥就像是你的比特币/以太坊账户的密码。在任何加密货币中创建账户只是生成一个私钥。你的地址(或者说在加密货币中的用户名)是由公钥派生出来的。公钥本身可以由私钥派生出来。比特币的私钥的一个例子是钱包导入格式WIF):5K1vbDP1nxvVYPqdKB5wCVpM3y99MzNqMJXWTiffp7sRWyC7SrG

它有 51 个十六进制字符。每个字符可以有 16 种组合。因此,私钥的数量如下:16 ^ 51 = 25711008708143844408671393477458601640355247900524685364822016(实际数量并非如此,因为比特币的私钥的第一个数字在主网上始终为 5,但你明白我的意思)。这是一个巨大的数字。因此,通过强大的随机过程生成私钥时,有人找到另一个已经充满比特币的账户的概率是非常非常低的。但是私钥和公钥生成的账户类型没有重置密码功能。

如果有人向你的地址发送比特币,而你忘记了你的私钥,那么它就永远丢失了。因此,尽管你的公钥被记录在每个比特币节点中的区块链上,但人们不会得到私钥。

哈希函数

哈希是一个函数,它接受任意长度的输入并将其转换为固定长度的输出。因此,为了更清楚地说明这一点,我们可以看下面的代码示例:

>>> import hashlib
>>> hashlib.sha256(b"hello").hexdigest()
'2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
>>> hashlib.sha256(b"a").hexdigest()
'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb'
>>> hashlib.sha256(b"hellohellohellohello").hexdigest()
'25b0b104a66b6a2ad14f899d190b043e45442d29a3c4ce71da2547e37adc68a9'

正如你所看到的,输入的长度可以是15,甚至20个字符,但输出始终是64个十六进制数字字符的长度。输出看起来是乱码,似乎输入和输出之间没有明显的联系。然而,如果给出相同的输入,它每次都会产生相同的输出:

>>> hashlib.sha256(b"a").hexdigest()
'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb'
>>> hashlib.sha256(b"a").hexdigest()
'ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb'

即使只改变一个字符的输入,输出也会完全不同:

>>> hashlib.sha256(b"hello1").hexdigest()
'91e9240f415223982edc345532630710e94a7f52cd5f48f5ee1afc555078f0ab'
>>> hashlib.sha256(b"hello2").hexdigest()
'87298cc2f31fba73181ea2a9e6ef10dce21ed95e98bdac9c4e1504ea16f486e4'

现在输出的长度是固定的,这种情况下是 64,当然会有两个不同的输入产生相同的输出。

有趣的是:找到两个不同的输入,使得这个哈希函数的输出相同是非常困难的。不可能的任务:即使你劫持了世界上所有的计算机并让它们运行哈希计算,你也不太可能找到两个不同的输入产生相同的输出。

然而,并非所有的哈希函数都是安全的。SHA-1在 2017 年已经消亡。这意味着人们可以找到两个不同的长字符串,它们具有相同的输出。在这个例子中,我们将使用SHA-256

哈希函数的输出可以用作数字签名。想象一下,你有一个长度为 1000 万的字符串(比如你在写一部小说),为了确保这部小说没有被篡改,你告诉所有潜在的读者,他们必须数一遍这 1000 万个字符,以确保小说没有被损坏。但是没有人会这样做。但是通过哈希,你可以用只有 64 个字符的输出验证(比如通过 Twitter),你的潜在读者可以对他们购买/下载的小说进行哈希,并进行比较,以确保他们的小说是合法的。

因此,我们在块类中添加了父哈希。这样,我们在我们的块中保留了父块的数字签名。这意味着如果我们有任何块的内容发生变化,任何子块中的父哈希将无效,你将被当场抓住。

但是,如果你想改变任何区块的内容,你可以改变子区块的父哈希吗?显然可以。然而,改变内容的过程变得更加困难。你需要有两个步骤。现在,想象一下你有 10 个区块,你想改变第一个区块的内容:

  1. 在这种情况下,你必须改变其直接子区块中父区块的哈希。但是,遗憾的是,这会带来看不见的后果。从技术上讲,其直接子区块中的父哈希是该区块中的一部分。这意味着其子区块中的父哈希(第一个区块的孙子)将无效。

  2. 现在,你必须改变那个孙子的父哈希,但这会影响后续的区块,依此类推。现在,你必须改变所有区块的父哈希。为此,需要进行十个步骤。使用父哈希使篡改变得更加困难。

工作证明

所以,在这种情况下,我们有三个参与者:纳尔逊,玛丽和斯凯。但还有另一种类型的参与者:在区块链术语中,写入区块链的人被称为矿工。为了将交易放入区块链,矿工需要先做一些工作。

以前,我们有三个区块(block_Ablock_Bblock_C),但现在我们有一个候选区块(block_D),我们希望将其添加到区块链中,如下所示:

block_D = Block()
block_D.id = 4
block_D.history = 'Sky loves turtle'
block_D.parent_id = block_C.id

但是,我们不是直接将block_D添加到区块链中,而是首先要求矿工做一些谜题工作。我们对该区块进行序列化,并要求矿工应用一个额外的字符串,当附加到该区块的序列化字符串时,如果进行哈希,将显示前面至少有五个零的哈希输出。

这些都是需要仔细思考的话。首先,我们对区块进行序列化:

import json
block_serialized = json.dumps(block_D.__dict__).encode('utf-8')
print(block_serialized)
b'{"history": "Sky loves turtle", "parent_id": 3, "id": 4}'

如果对序列化的区块进行哈希,如果我们希望哈希输出的前面至少有五个零,那意味着我们希望输出看起来像这样:

00000aa21def23ee175073c6b3c89b96cfe618b6083dae98d2a92c919c1329be

或者,我们希望它看起来像这样:

00000be7b5347509c9df55ca35d27091b41a93acb2afd1447d1cc3e4b70c96ab

因此,这个谜题就像这样:

string serialization + answer = hash output with (at least) 5 leading zeros

矿工需要猜出正确的答案。如果将这个谜题转换为 Python 代码,它会是这样的:

answer = ?
input = b'{"history": "Sky loves turtle", "parent_id": 3, "id": 4}' + answer
output = hashlib.sha256(input).hexdigest()
// output needs to be 00000???????????????????????????????????????????????????????????

那么,矿工如何解决这样的问题呢?我们可以使用穷举法:

import hashlib

payload = b'{"history": "Sky loves turtle", "parent_id": 3, "id": 4}'
for i in range(10000000):
  nonce = str(i).encode('utf-8')
  result = hashlib.sha256(payload + nonce).hexdigest()
  if result[0:5] == '00000':
    print(i)
    print(result)
    break

因此,结果将如下所示:

184798
00000ae01f4cd7806e2a1fccd72fb18679cb07ede3a2a7ef028a0ecfd4aec153

这意味着答案是184798,或者{"history": "Sky loves turtle", "parent_id": 3, "id": 4}184798的哈希输出是前面有五个零的那个。在这个简单的脚本中,我们从 0 迭代到 9999999,并将其附加到输入中。这是一种天真的方法,但它有效。当然,你也可以附加字符而不是数字,比如 a、b 或 c。

现在,试着将前导零的数量增加到六个,甚至十个。在这种情况下,你能找到哈希输出吗?如果没有输出,你可以将范围限制从 10000000 增加到更高的数字,比如 1000000000000。一旦你对这个工作的艰辛有所了解,试着理解这一点:比特币在这本书写作时需要大约 18 个前导零的哈希输出。前导零的数量不是固定的,会根据情况而变化(但你不需要担心这个)。

那么,为什么我们需要工作证明呢?我们首先需要看一下共识的概念。

共识

正如我们所看到的,哈希函数使得篡改历史变得困难,但并非太难。即使我们有一个由 1000 个区块组成的区块链,利用现代计算机轻而易举地改变第一个区块的内容,并将其他区块上的 999 个父哈希值更改。因此,为了确保坏人无法更改历史(或者至少使其变得非常困难),我们将这个追加数据库分发给每个想要保存它的人(让我们称他们为矿工)。假设有十个矿工。在这种情况下,你不能仅仅在你的副本中更改区块链,因为其他九个矿工会责骂你,说一些像“嘿,我们的记录显示历史 A,但你的记录显示 B”的话。在这种情况下,多数人胜出。

然而,共识不仅仅是选择大多数人选择的区块链。问题在于当我们想要向区块链中添加一个新区块时。我们从哪里开始?我们该如何做?答案是我们广播。当我们广播包含新交易的候选区块时,它不会立即到达每个矿工。你可能会到达站在你旁边的矿工,但你的消息需要时间才能到达远离你的矿工。

这就是有趣的地方:远离你的矿工可能会先收到另一个新的候选区块。那么,我们如何同步所有这些事情,并确保大多数人拥有相同的区块链呢?简单的规则是选择最长的链。因此,如果你是中间的矿工,你可能会同时收到两个不同的候选区块,如下图所示:

你从西边得到这个:

block_E = Block()
block_E.id = 5
block_E.history = 'Sherly likes fish'
block_E.parent_id = block_D.id

你从东边得到这个:

block_E = Block()
block_E.id = 5
block_E.history = 'Johny likes shrimp'
block_E.parent_id = block_D.id

因此,我们将保留block_E的两个版本。我们的区块链现在有一个分支。然而,不久后,来自东边的其他区块已经到达。现在的情况是这样的:

这是来自西边的:

block_E = Block()
block_E.id = 5
block_E.history = 'Sherly likes fish'
block_E.parent_id = block_D.id

这是来自东边的:

block_E = Block()
block_E.id = 5
block_E.history = 'Johny likes shrimp'
block_E.parent_id = block_D.id

block_F = Block()
block_F.id = 6
block_F.history = 'Marie hates shark'
block_F.parent_id = block_E.id

block_G = Block()
block_G.id = 7
block_G.history = 'Sarah loves dog'
block_G.parent_id = block_F.id

到这一点,我们可以摆脱区块链的西侧版本,因为我们选择了更长的版本。

问题来了。假设 Sherly 讨厌鲨鱼,但她想从一个地区获得选票,那里的大多数人只投票给喜欢鲨鱼的候选人。为了获得更多选票,Sherly 广播了一个包含以下谎言的区块:

block_E = Block()
block_E.id = 5
block_E.history = 'Sherly loves shark'
block_E.parent_id = block_D.id

一切都很好。投票会话持续一天。一天过去后,区块链又增加了两个区块:

block_E = Block()
block_E.id = 5
block_E.history = 'Sherly loves shark'
block_E.parent_id = block_D.id

block_F = Block()
block_F.id = 6
block_F.history = 'Lin Dan hates crab'
block_F.parent_id = block_E.id

block_G = Block()
block_G.id = 7
block_G.history = 'Bruce Wayne loves bat'
block_G.parent_id = block_F.id

以下图示了三个区块:

现在,Sherly 需要从另一个地区获得选票,那里的大多数人只投票给讨厌鲨鱼的候选人。那么,Sherly 如何篡改区块链以使其对她有利呢?Sherly 可以广播四个区块!

block_E = Block()
block_E.id = 5
block_E.history = 'Sherly hates shark'
block_E.parent_id = block_D.id

block_F = Block()
block_F.id = 6
block_F.history = 'Sherly loves dog'
block_F.parent_id = block_E.id

block_G = Block()
block_G.id = 7
block_G.history = 'Sherly loves turtle'
block_G.parent_id = block_F.id

block_H = Block()
block_H.id = 8
block_H.history = 'Sherly loves unicorn'
block_H.parent_id = block_G.id

以下图示了四个区块:

矿工将选择来自 Sherly 而不是他们之前保存的区块链,其中包含“Sherly 爱鲨鱼”的历史。因此,Sherly 已经能够改变历史。这就是我们所说的双重支付攻击。

我们可以通过工作证明(添加区块的激励)来防止这种情况。我们在本章前面解释了工作证明,但我们还没有解释激励系统。激励意味着如果矿工成功地将新区块添加到区块链中,系统会给予他们数字奖励。我们可以将其整合到代码中如下:

import hashlib

payload = b'{"history": "Sky loves turtle", "parent_id": 3, "id": 4}'
for i in range(10000000):
  nonce = str(i).encode('utf-8')
  result = hashlib.sha256(payload + nonce).hexdigest()
  if result[0:5] == '00000':
 // We made it, time to claim the prize
 reward[miner_id] += 1
    print(i)
    print(result)
    break

如果 Sherly 想要更改历史(替换一些区块),她需要花费一些资源来在短时间内解决四个难题。等她完成这个过程时,大多数矿工保存的区块链可能已经添加了更多的区块,使得它比 Sherly 的区块链更长。

这是因为大多数矿工希望以最有效的方式获得我们提到的奖励。为了做到这一点,他们会获得一个新的候选区块,努力找到工作证明的答案,然后尽快将其添加到最长的链上。但是,为什么他们想要将其添加到最长的链而不是其他链呢?这是因为它保障了他们的奖励。

假设我们有两个版本的区块链。一个有三个区块,而另一个有八个区块。添加新区块的最明智的方法是将其添加到有八个区块的区块链中。如果有人将其添加到只有三个区块的区块链中,它更有可能被丢弃。因此,奖励将被从矿工那里拿走。最长的链无论如何都会吸引最多的矿工,您希望在被更多人保留的区块链版本中。

一些矿工可能会坚持将区块添加到只有三个区块的区块链中,而其他矿工也可能会坚持将区块添加到有八个区块的区块链中。我们称之为硬分叉。大多数时候,矿工会坚持保持最长的链的区块链。

要改变历史,Sherly 将需要击败至少 50%以上的矿工,这是不可能的。区块越老,其中的历史就越安全。假设一个人需要 5 分钟来完成谜题工作。在这种情况下,为了替换区块链中的最后五个区块,Sherly 需要超过 25 分钟(因为 Sherly 至少需要六个区块来说服矿工替换他们区块链中的最后五个区块)。但是在这 25 分钟内,其他矿工将继续向最受欢迎的区块链添加新的区块。因此,当 25 分钟过去时,最受欢迎的区块链将增加五个额外的区块!也许矿工们会打个小盹,一个小时不再添加任何区块。在这种情况下,Sherly 可以积累六个区块来篡改最受欢迎的区块链。然而,嵌入在区块链中的激励使矿工们 24/7 保持清醒,因为他们希望尽可能多地获得奖励。因此,对于 Sherly 来说,这是一场失败的战斗。

在区块链上编码

在撰写本书时,最受欢迎的两种加密货币是比特币和以太坊(偶尔,瑞波会占据第二位)。如果您向了解加密货币的人提出一个简单的问题,您可能会得到这样的答案:比特币只是用来发送货币,但您可以在以太坊上创建程序。该程序可以是代币、拍卖或托管等。但这只是半真。您也可以在比特币上创建程序。通常,人们称这个程序为脚本。事实上,在比特币交易中必须提供一个脚本。比特币交易可能很普通,所以如果我想向您发送 1 个比特币(比特币中的货币单位)并且您的比特币地址是 Z,我需要将这样的脚本上传到比特币区块链中:

What's your public key? If the public key is hashed, does it equal Z? If yes, could you provide your private key to prove that you own this public key?

但它可能会更加复杂。假设您想要至少需要四个授权签名中的两个签名来解锁此帐户;您可以使用比特币脚本来实现。发挥创造力,您可以想出类似这样的东西:

This transaction is frozen until 5 years from now. Then business will be as usual, that the spender must provide public key and private key.

但是比特币脚本是用简单的编程语言创建的,甚至无法循环。它是基于堆栈的。因此,您可以放置指令:对公钥进行哈希,检查签名,并检查当前时间。然后,它将在比特币节点上从左到右执行。

这意味着您无法在比特币上创建一个复杂的程序,比如拍卖。比特币的设计只是用来存储和转移价值(货币)。因此,它特意设计成避免复杂的程序。在比特币节点中,每个脚本都会被执行。没有循环,比特币脚本将会非常简单,您知道它何时会停止。但是如果您在比特币脚本中有一个循环,您就不知道它何时会停止。它可能在第四次迭代中停止,或者在第一百万次迭代中停止,或者在遥远的未来停止。

有些人对此限制感到不满,因此创建了以太坊。您在以太坊区块链上配备的编程语言比比特币中的编程语言要复杂得多(有whilefor结构)。从技术上讲,您可以在以太坊区块链上创建一个永远运行的程序。

您可以像在比特币中那样存储和转移价值。但在以太坊中,您可以做的远不止这些。您可以创建一个投票程序、担保服务、在线拍卖,甚至在其上创建另一种加密货币。因此,人们喜欢区分比特币BTC)和以太坊ETH)的货币。BTC 就像是数字黄金。ETH 就像是石油和天然气。如果我们采用这个类比,两者都是有价值的。但是,您可以使用石油和天然气来创造一个全新的世界,例如通过创造塑料、燃料等。另一方面,您可以做的事情与黄金相比相当有限,除了创造珠宝。

在以太坊上创建加密货币非常容易。如果您是一名熟练的程序员,您只需要一个周末。您只需继承一个类,并设置您的代币名称和供应限制。然后,您编译它并发布到以太坊生产区块链上,您就会拥有自己的加密货币。在此之前,创建另一种加密货币意味着分叉比特币。所需的技能水平相当深(C++、CMake,并替换比特币核心文件的许多部分)。

其他类型的区块链程序员…

本章旨在让您直观地了解区块链的工作原理。然而,这并不是它的完整范围。我的解释与比特币(甚至以太坊)的工作方式有很大不同。以太坊不使用SHA-256进行哈希;它通常使用Keccak-256算法。在我们的情况下,我们只在一个区块中放入一个历史记录/交易/有效负载,但比特币可以在一个区块中保存超过 1,000 笔交易。然后,我们通过使用 RSA 密码学生成私钥和公钥,而比特币和以太坊使用椭圆曲线密码学。在我们的情况下,有效负载是历史记录(谁喜欢/爱/讨厌动物),但在比特币中,它是对先前有效负载的依赖性交易。在以太坊本身中,它是程序的状态。因此,如果有效负载中的变量a等于整数5,它可能是类似于将变量a更改为整数7。在比特币共识中,我们选择具有最高哈希率功率的区块链,而不是具有最长链的区块链。例如,区块链 A 有两个区块,但每个区块的答案都是以 12 个前导零解决谜题,而区块链 B 有十个区块,但每个区块的答案只有五个前导零。在这种情况下,区块链 A 具有最高的哈希率功率。

现在,让我们回到以下问题:成为区块链程序员意味着什么?有多少种类型的区块链程序员?这本书的范围是什么?

区块链编程可能意味着您正在努力改进比特币的状态或创建比特币的分叉,如比特币现金。您需要 C++和 Python。如果您正在创建比特币分叉,例如比特币黄金,您需要更深入地了解密码学。在比特币黄金中,开发人员将工作证明哈希函数从 SHA-256 更改为 Equihash,因为 Equihash 是 ASIC 抗性的。ASIC 抗性意味着您无法创建特定的机器来进行哈希。您需要一台带有 GPU 的计算机来执行 Equihash 哈希函数,但本书不会讨论这一点。

此外,区块链编程可能意味着您正在努力改进以太坊虚拟机。您需要 Go、C++或 Python。您需要了解如何与低级加密库函数进行交互。对基本密码学的直观理解是不够的,但本书也不会讨论这一点。

区块链编程可能意味着你正在在以太坊上编写程序。为此,你需要 Solidity 或 Vyper,这本书将讨论这些内容。你只需要对基本密码学的直观理解。你已经摆脱了低级密码学。偶尔,你可能会在你编写的程序中使用哈希函数,但没有什么花哨的。

区块链编程可能意味着你正在编写一个与以太坊上的程序进行交互的程序,听起来有点元。但你为此所需取决于平台。如果是移动应用程序,你需要 Kotlin、Java、Swift、Obj-C,甚至 C++。如果是 Web 前端,你很可能需要 JavaScript。只需要对基本密码学的直观理解。这本书将讨论其中的一些内容。

这就好像我问你,“当有人想成为 Web 开发人员时,这意味着什么?”答案是多种多样的。我应该学习 Ruby、Java、PHP 还是 Python?我应该学习 Ruby on Rails、Laravel 还是 Django?

这本书将教你如何在以太坊上构建程序(不要与构建以太坊本身混淆)。将这与 Web 开发进行比较,这就好像说这本书将教你如何使用 Ruby on Rails 构建 Web 应用程序,但这本书不会教你如何解剖 Ruby on Rails 框架本身。这并不意味着 Ruby on Rails 的内部不重要,只是大多数情况下你不需要它们。

这本书将教你使用 Python 编程语言,假设你已经有 Python 的基本知识。但为什么选择 Python 呢?答案是老生常谈:Python 是最简单和最流行的编程语言之一。它降低了想要进入区块链领域的人的门槛。

总结

在这一章中,我们研究了比特币和以太坊等加密货币背后的技术。这项技术使价值或代码的去中心化存储成为可能。我们还涵盖了使用私钥和公钥来保护任何数据的完整性的密码学。此外,我们还学习了哈希函数、工作证明、共识和区块链编程的基本概念。

在下一章中,我们将学习智能合约,这是以太坊中的一种程序。智能合约不同于在服务器中运行的程序,比如使用 Ruby on Rails、Laravel 或 Django 编写的应用程序。区别不仅仅在于语法;这个概念与普通的 Web 应用程序完全不同。

参考资料

第二章:智能合约基础知识

在本章中,我们将探讨智能合约的基础知识。在比特币中,我们存储价值,在以太坊中,我们存储代码。我们在以太坊中存储的代码称为智能合约。智能合约是一种无需信任的代码,这意味着代码的完整性由算法和密码学保护。我们可以存储无需审查的代码,并且能够避免第三方干预,甚至是智能合约的开发者。这为创建许多类型的应用程序打开了可能性,例如透明的数字代币、无需信任的众售、安全的投票系统和自治组织。

本章将涵盖以下主题:

  • 安装以太坊开发环境

  • 编写智能合约

  • 将智能合约部署到以太坊区块链

  • 与智能合约交互

  • 为什么要使用智能合约?

安装以太坊开发环境

现在是时候创建一个智能合约了。如果您熟悉 Solidity、Truffle、Ganache 和智能合约基础知识,可以直接跳转到第三章,使用 Vyper 实现智能合约。在这里,我们将专注于使用 Solidity 创建的智能合约的内容。在本书的其余部分,我们将使用 Vyper 编程语言来开发智能合约。但是,在本书的其余部分,我们仍将使用相同的开发环境(如 Ganache)。

安装 Node.js

Node.js 是一个用于开发 Web 应用程序、移动应用程序和去中心化应用程序的流行框架。前往nodejs.org并下载最新版本(目前是版本 10)。以下是如何在 Ubuntu Linux 平台上安装 Node.js:

$ curl -sL https://deb.nodesource.com/setup_10.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ node --version
v10.15.0

安装 Truffle 和 Solidity

Truffle 是一个用于使用 Solidity 开发智能合约的开发框架。您可以在没有 Truffle 的情况下创建智能合约,但 Truffle 会让这一过程变得更加简单。没有 Truffle,您仍然可以编写智能合约,但是要编译它,您必须使用特定标志启动 Solidity 编译器。然后,为了将这个智能合约部署到区块链上,您必须创建一个脚本将字节码发送到区块链。有了 Truffle,为了编译,您可以调用truffle compile命令,为了将智能合约部署到区块链上,您可以在编写一个简单的迁移脚本后调用truffle migrate命令。Truffle 还为您提供了一个与区块链网络中的智能合约进行交互的工具。它拥有您开发智能合约所需的一切。然而,正如之前所述,我们在下一章中将不使用这个框架。

我们将从使用 Node.js 包管理器安装 Truffle 开始。在 Ubuntu Linux 中,为了全局安装 Truffle,我们必须使用sudo。正如前面所述,Truffle 是一个智能合约开发框架,包含许多工具,包括与区块链网络和开发区块链软件进行交互的控制台应用程序。除此之外,使用 Truffle,您还会得到 Solidity 编译器。

但首先,您需要确保npm在您的主目录中全局安装软件:

$ mkdir ~/.npm-global
$ npm config set prefix '~/.npm-global' 

然后将这行添加到~/.配置文件中:

export PATH=~/.npm-global/bin:$PATH

现在,打开一个新的终端,以便新的配置文件生效,或者,可以按照以下步骤操作:

$ source ~/.profile

然后,我们可以按照以下步骤安装 Truffle:

$ npm install -g truffle
$ truffle version
Truffle v5.0.2 (core: 5.0.2)
Solidity v0.5.0 (solc-js)
Node v10.15.0 

安装 Ganache

开发智能合约时,人们通常使用 Ganache。Ganache 是一个私有的、开发中的以太坊网络,您只能在以太坊的开发阶段使用它。Truffle 框架已经包含了与 Ganache 相同目的的以太坊区块链网络。两者之间的区别在于 Ganache 有一个前端 GUI 和更加用户友好的界面。

启动 Ganache 时,您将配备 10 个帐户,每个帐户都有 100 个以太币,这是以太坊区块链中的货币。正如您将在本章后面看到的,以太坊编程中的货币概念(如持有、发送和接收货币以及跟踪余额)是重要的。您需要花钱才能在以太坊区块链上启动智能合约。您可以从一个帐户向智能合约发送资金,智能合约反过来可以向其他智能合约或其他帐户发送资金。

要下载软件,请访问 Ganache 网站:www.truffleframework.com/ganache。对于 Linux 平台,该软件称为ganache-1.2.3-x86_64.AppImage。下载后,您必须在执行之前设置正确的权限:

$ chmod a+x ganache-1.2.3-x86_64.AppImage
$ ./ganache-1.2.3-x86_64.AppImage

编写智能合约

安装所有必需的软件后,我们可以开始编写智能合约。首先,我们将创建一个新目录,然后使用 Truffle 开发工具对其进行初始化:

$ mkdir my_first_smart_contract
$ cd my_first_smart_contract
$ truffle init

truffle init命令的输出如下:

这将命令 Truffle 初始化您的目录以成为智能合约开发项目。在该项目目录中开发智能合约时,有几个目录可供您使用:

$ ls
contracts migrations test truffle-config.js

通常,您会将智能合约的源代码合并到contracts文件夹中。migrations文件夹包含用于部署智能合约的文件,test文件夹包含test文件。您可以在truffle-config.js文件中配置智能合约部署设置。我们将使用以下代码创建第一个智能合约并将其命名为donation.sol

pragma solidity ⁰.5.0;

contract Donation {
  address public donatur;
  address payable donatee;
  uint public money;
  string public useless_variable;

  constructor() public {
    donatee = msg.sender;
    useless_variable = "Donation string";
  }

  function change_useless_variable(string memory param) public {
    useless_variable = param;
  }

  function donate() public payable {
    donatur = msg.sender;
    money = msg.value;
  }

  function receive_donation() public {
    donatee.transfer(address(this).balance);
  }
}

如果您是智能合约的新手,前面的示例中可能会有一些陌生的关键字。在本章中,我们不打算讨论与 Solidity 有关的所有内容。相反,我们只会研究构建智能合约和学习智能合约概念所必需的 Solidity 功能。

但首先,让我们将这个用 Solidity 编写的智能合约编译成以太坊字节码和应用程序二进制接口abi)。为此,我们将在 Truffle 项目目录中运行以下命令:

$ truffle compile

编译的结果可以在build/contracts文件夹中看到,名为Donation.json

如果您打开该文件,您会看到许多有趣的东西。这个.json文件有 1530 行长。此文件中的json对象有 14 个键。您现在只需要考虑两个键。第一个是接口(称为abi),第二个是可以在以太坊虚拟机上执行的二进制文件(称为bytecode)。有关本节中代码的代码文件,请参考以下 GitLab 链接:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_02/my_first_smart_contract/build/contracts/Donation.json

我们无法像编译 C 代码并直接执行二进制文件时那样运行此二进制文件。我们需要将此二进制文件放入以太坊虚拟机中。接口本身是我们在开发去中心化应用程序时以后与智能合约进行交互所需的。当您将智能合约部署到以太坊区块链时,您需要字节码。当您想要与已在以太坊区块链上部署的智能合约进行交互时,您需要abi接口。

将智能合约部署到以太坊区块链

以下是使用 Truffle 将智能合约部署到以太坊区块链的步骤:

  1. 编写迁移脚本:要部署您的智能合约,您需要编写一个迁移文件。创建一个名为migrations/2_deploy_donation.js的新文件。然后,我们用以下脚本填充这个文件:
var Donation = artifacts.require("./Donation.sol");

module.exports = function(deployer) {
  deployer.deploy(Donation);
};

至于migrations/1_initial_migration.jscontracts/Migrations.sol文件,我们暂时保持它们不变。Truffle 需要这些文件才能部署智能合约。

  1. 启动 Ganache(以太坊开发的区块链):现在您需要启动 Ganache。假设您已经获得了适当的权限,运行以下命令行来执行文件:
./ganache-1.2.3-x86_64.AppImage

如下截图所示,您有多个帐户,每个帐户的余额为 100 以太币:

从 Ganache 屏幕上您会注意到的一件事是RPC SERVER,它位于http://127.0.0.1:7545。这是您的以太坊区块链位于Truffle项目目录中的地方。

  1. 编辑 Truffle 配置文件:如果您打开truffle-config.js文件,在删除注释行后,代码将如下所示:
module.exports = {
  networks: {
  },
  mocha: {
  },
  compilers: {
    solc: {
    }
  }
};

清除它,并将以下代码添加到truffle-config.js文件中:

module.exports = {
  networks: {
    "development": {
      network_id: 5777,
      host: "localhost",
      port: 7545
    },
  }
};

hostport是从 Ganache 屏幕上的 RPC 服务器中获取的,network_id是从 Ganache 屏幕上的 Network ID 中获取的。

  1. 执行迁移脚本:要部署您的智能合约,您可以按照以下方式执行它:
$ truffle migrate

Truffle 框架将获取您在Donation.json文件中定义的字节码,并将其发送到以太坊区块链或 Ganache。这将为您提供以下输出:

2_deploy_donation.js部分,注意单词contract address:后面的十六进制数字,即0x3e9417399786347b6ab38f59d3f00829d6bba7b8。这是智能合约的地址,类似于 Web 应用程序的 URL。

如果您尝试部署智能合约时输出了Network is up to date.,您可以删除build/contracts目录中的文件,并使用truffle migrate命令运行这个版本:

$ truffle migrate --reset

现在,让我们来看看 Ganache 屏幕上的变化:

最重要的一点是,第一个帐户0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77失去了钱。余额不再是 100 ETH,而是**99.98 ETH**。那么 0.02 ETH 去哪了?矿工需要有动力才能将您的智能合约写入以太坊区块链。请注意,当前区块不再是 0,而是 4。因此,0.02 ETH 将是成功将包含您的智能合约的区块放入区块链的矿工的费用。但是,在这种情况下,当然没有矿工,因为我们使用的是以太坊开发区块链 Ganache。Ganache 只是通过将智能合约纳入区块链来模拟交易费用。

如果您点击“TRANSACTIONS”选项卡,您将看到类似于这样的内容:

您现在已经创建了两个合约(Donation 和 Migrations)。一旦部署了智能合约,除非您应用了关闭它的方法,否则它将永远存在于区块链中。如果您的智能合约中有错误,您无法修补它。您必须在不同的地址部署一个修复后的智能合约。

与智能合约交互

要与驻留在以太坊区块链中的智能合约进行交互,在您的Truffle项目目录中执行以下命令:

$ truffle console

然后,在truffle控制台提示符中,执行以下命令:

truffle(development)> Donation.deployed().then(function(instance) { return instance.useless_variable.call(); });
'Donation string'

如果您对then感到困惑,Truffle 控制台使用回调的概念,通过它执行访问智能合约对象是异步执行的。在 Truffle 控制台中,此语句在回调被执行之前立即返回。在回调函数中,您将接受智能合约实例作为instance参数。然后,我们可以从这个instance参数中访问我们的useless_variable变量。然后,要检索值,我们必须在该变量上执行call方法。

Truffle 框架将使用Donation.json文件中定义的abi来了解您的智能合约中有哪些接口可用。请记住,在您的智能合约中定义了useless_variable并将其设置为构造函数(或初始化)函数中的Donation string。以这种方式读取公共变量是免费的;因为它存储在区块链中,所以不需要任何以太币。

让我提醒您,如果变量存储在区块链中意味着什么。如果您将此智能合约纳入以太坊生产区块链,useless_variable变量将存储在每个以太坊节点中。在撰写本文时,大约有 10,000 个节点。这个数字会不断变化,可以在这里看到:www.ethernodes.org。一个节点可以在一台计算机上,一台计算机可以容纳几个节点。但是,一台计算机很可能只持有一个节点,因为成为节点主机的要求非常高。如果您想与区块链交互,就需要一个节点(也有其他选项,例如使用 API 与他人的节点进行交互)。因此,免费读取useless_variable变量,因为您只是从自己的计算机上读取它。

如果您对这个免费概念感到困惑,让我们通过将useless_variable变量更改为其他内容来使其更清晰:

truffle(development)> Donation.deployed().then(function(instance) { return instance.change_useless_variable("sky is blue", {from: "0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77" }); });

您将获得以下输出:

在单词from后面还有另一个神秘的十六进制数字,即0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77。这是 Ganache 中第一个账户的公共地址。您可以通过查看 Ganache 屏幕来确认。在这里,您读取useless_variable变量的方式有所不同,并且使用不同的内容进行设置。更改变量的内容需要不同的语法,更重要的是需要一个要使用的账户。需要账户是因为在区块链中更改变量时需要花一些钱。当您在以太坊生产区块链中的智能合约中更改useless_variable变量的值时,基本上是向以太坊生产区块链中的所有以太坊节点广播,该区块链中有大约 10,000 个可用于更新useless_variable内容的节点。我们正在使用 Ganache,这是以太坊开发区块链,但在生产环境中,您需要使用私钥签署交易以更改变量的内容。私钥的作用类似于账户上的密码,但私钥无法更改,而您可以随意更新密码。如果您忘记密码,可以重置密码并单击确认电子邮件中的链接进行更新。在区块链中,这是不可能的。

如果您现在检查 Ganache,您的余额保持不变;只有区块编号从 4 增加到 5:

这是因为所需的金额非常小。您可以在执行最后一个命令后查看更改useless_variable值的输出。查看使用的燃气字段;这是在智能合约中执行函数时所花费的。使用的燃气量为 33684,但这是以 gwei 计算的,而不是以以太计算的。1 以太等于 1,000,000,000 gwei,因此大约为 0.00003 以太。在这种情况下,燃气是自动计算的,但以后,您可以设置在以太坊区块链中执行任何函数时要分配多少燃气。如果您没有贡献太多以太,并且分配的燃气量很小,那么您的执行很有可能会被赋予较低的优先级。交易确认所需的时间会更长(意味着变量的值已经被更改)。如果网络遇到大量交通,它可能会被矿工拒绝。

花钱改变程序状态的概念是新的。从区块链中读取所有内容是免费的,因为所有数据都在您的计算机上(如果您有以太坊节点),但是要更改或添加区块链中的内容需要花钱。这是因为您更改了全球各地的所有以太坊节点中的数据,这是昂贵的!除了更改智能合约的状态外,内存中运行的计算也需要花钱。

向智能合约发送以太币

现在,让我们向智能合约发送一些以太币。让我们使用第二个帐户。第二个帐户希望使用智能合约捐赠 5 个以太币,如下所示:

truffle(development)> Donation.deployed().then(function(instance) { return instance.donate({ from: "0x6d3eBC3000d112B70aaCA8F770B06f961C852014", value: 5000000000000000000 }); });

您将获得以下输出:

除了from字段外,您还需要添加一个value字段。在这个value字段中,您输入要发送到智能合约的金额。您可能会想知道为什么这个数字有这么多个零。在以太坊区块链中转移资金时,您必须使用最低的货币单位(类似于从美元转换为美分),这称为wei。1 以太是 1,000,000,000,000,000,000 wei(18 个零)。您想发送 5 个以太,使其为 5,000,000,000,000,000,000。现在,如果您查看 Ganache 屏幕,您会注意到余额下降到 95 个以太。因此,5 个以太现在存储在智能合约中,如下截图所示:

让我们使用第一个帐户提取这笔款项:

truffle(development)> Donation.deployed().then(function(instance) { return instance.receive_donation({ from: "0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77" }); });

您将获得以下输出:

这个执行与以前的语法相同,减去了value字段。现在看一下 Ganache 屏幕。第一个帐户有 104 到 105 个以太币(例如 104.8 或 104.9 个以太币)。不是 105 个以太币,因为我们已经在使用第一个帐户启动智能合约时支出了一些钱,并且在执行智能合约中的一些功能时支付了交易费用:

为什么智能合约?

您现在已经看到了智能合约的运行。那么这一切是怎么回事?智能合约能做什么传统程序(普通 Web 应用程序)做不到的?当涉及到更改网络上的程序变量的值时,我们可以使用远程过程调用。更常见的是,我们可以将变量存储在数据库中,人们可以从 Web 应用程序中更改数据库中的值。在发送资金时,我们可以将我们的 Web 应用程序与 Stripe 或 PayPal 集成,从而使我们能够发送资金。或者,您可以创建一个用于存储数字货币的数据库表。以太坊中的以太币基本上是数字货币。实际上,普通的 Web 应用程序可以做任何智能合约可以做的事情,但速度更快,成本更低。区块链解决方案的关键区别在于可以无需信任。这意味着您可以信任程序本身,而不是操作员。

在正常的 Web 应用程序中,我们依靠对 Web 应用程序的运营者(开发者或系统管理员)的信任。我们希望他们诚实地开发和部署 Web 应用程序。作为 Web 应用程序的用户,我们无法确保 Web 应用程序是否真的做到了它告诉大家它正在尝试做的事情。

假设我们有一个托管视频的 Web 应用程序(例如 YouTube 或 Vimeo)。如果用户点击“喜欢”按钮,Web 应用程序可以增加视频的喜欢数。规则是用户只能给视频点赞一次。因此,你期望包含 400 个喜欢的视频有 400 个点赞的用户。如果我告诉你,在幕后,系统管理员可以人为地增加喜欢数呢?这意味着在 400 个喜欢中,可能只有 300 个来自真实用户,另外 100 个是系统管理员人为增加的。这并不一定是直接更新数据库中的表,例如通过UPDATE video_likes SET likes_amount = 400 WHERE video_id = 5;。增加喜欢数的方式可能被嵌入在系统内部。

普通用户可能不会注意到这一点。Web 应用程序的公司可以在 GitHub 上发布源代码。但是,如何确保 Web 应用程序确实是从 GitHub 托管的源代码构建的呢?如果在部署 Web 应用程序后,系统管理员或开发者对系统进行了修补怎么办?

有许多方法可以防止这种数字作弊。首先,我们可以引入 IT 审计员。他们可以来自政府或非政府组织。他们将审计系统的源代码,更重要的是,检查代码在生产系统中的运行情况。在这种情况下,你将把你的信任从开发者或系统管理员转移到第三方审计员。

作为一名 IT 顾问,我通过构建 Web 应用程序和移动应用程序谋生。我曾遇到一个潜在客户,他想要制作一份数字报纸(类似于《卫报》或《纽约时报》)。这位客户问我是否有办法将任何文章移动到最受欢迎的文章部分。这样做的动机是为了推广特定文章,即使这意味着系统会向用户撒谎关于最常被浏览的文章是什么。

Reddit 的 CEO 最近因在网站上修改关于他的评论而发表了公开道歉。更多信息请参阅以下链接:techcrunch.com/2016/11/23/reddit-huffman-trump/

即使是知名的网站也会玩数字作弊。例如,一些加密货币交易所伪造交易量。更多信息请参阅以下链接:medium.com/@sylvainartplayribes/chasing-fake-volume-a-crypto-plague-ea1a3c1e0b5e

智能合约是防止数字作弊的另一种方式。智能合约的开发者可以发布源代码并将智能合约部署到以太坊区块链中。人们可以验证发布的源代码和部署的智能合约是否一致。开发者可以在智能合约中创建欺骗的方法,但用户可以从区块链中智能合约的字节码重构智能合约的源代码,从而发现这种行为。开发者无法修补已经部署的智能合约。

还有其他智能合约属性,比如自治和抗审查。然而,没有任何属性能比得上它的透明性。或者说,你必须拥有比所有 10,000 个节点一起工作更强大的力量来保护以太坊系统的可信度。简单来说,你需要购买超过 5,000 台配备高端 GPU 的计算机。假设你有这么多资源来作弊。当你在以太坊系统中作弊时,诚实的矿工会在互联网上发出警报。因此,如果你有作弊的手段,你无法偷偷摸摸地做。此外,如果你有手段获得 5,000 台配备高端 GPU 的计算机,你可以成为以太坊平台上的矿工并获得丰厚的收入。因此,在以太坊平台上作弊是非常困难的。

总结

在这一章中,我们探讨了如何安装 Solidity 开发工具:Node.js,Truffle 或 Ganache。然后我们学习了如何编写智能合约并对其进行编译。之后,我们看了一下将智能合约部署到以太坊区块链的概念,然后使用 Truffle 控制台工具与已部署的智能合约进行交互。我们对向智能合约发送以太币以及在执行智能合约中的功能时的燃气使用概念有了了解。在最后一步,我们熟悉了为什么智能概念如此成功,具有创建一个无法绕过的、透明的程序的潜力。

在下一章中,我们将学习如何使用 Vyper 实现智能合约。

进一步阅读

以太坊黄皮书:ethereum.github.io/yellowpaper/paper.pdf

以太坊白皮书:github.com/ethereum/wiki/wiki/White-Paper

第三章:使用 Vyper 实现智能合约

许多学习如何编写智能合约的程序员会学习 Solidity 编程语言。有丰富的在线教程和书籍可以教你 Solidity。与 Truffle 框架结合使用,Solidity 形成了一个开发智能合约的绝佳组合。几乎所有存在于以太坊区块链上的智能合约都是用 Solidity 编程语言编写的。

在本章中,我们将探讨如何编写智能合约。但我们不会使用 Solidity 编程语言。相反,我们将使用 Vyper 编程语言。

本章将涵盖以下主题:

  • Vyper 背后的动机

  • 安装 Vyper

  • 使用 Vyper 创建智能合约

  • 将智能合约部署到 Ganache

  • 深入了解 Vyper

  • 与其他智能合约互动

  • 以编程方式编译代码

  • 其他技巧

Vyper 背后的动机

编写智能合约与开发普通的 Web 应用程序是不同的。在开发普通的 Web 应用程序时,座右铭是“快速移动,打破事物”。开发 Web 应用程序的速度至关重要。如果应用程序中有错误,你可以随后升级应用程序。或者,如果错误是灾难性的,你可以在线修补它或在引入修复之前将应用程序下线。有一个非常流行的词来描述开发普通 Web 应用程序的理想思维方式——敏捷。你需要灵活,以便随着需求的变化改变软件。

然而,编写智能合约需要不同的思维方式。智能合约的应用范围可以从编写金融应用程序到将火箭发射到太空。一旦部署了智能合约,要修复错误就非常困难。你不能替换智能合约,因为一旦部署了,就无法更改。如果编写一个函数来销毁智能合约,你可以销毁智能合约,但修复有问题的智能合约的唯一方法是部署一个新的智能合约,在新的地址中修复错误,然后将这种情况通知所有相关方。但你不能替换智能合约。

因此,理想的情况是在区块链上部署一个没有错误的智能合约,或者至少没有恶性错误。然而,在现实世界中发布的智能合约仍然会出现错误。

那么,智能合约中可能出现什么样的错误?第一种是会让你的资金消失的错误。比如,你正在为一个首次代币发行ICO)编写智能合约。ICO 是通过在以太坊区块链上出售你创建的代币来积累资本。基本上,人们用以太币购买你的代币。你可以自行设置价格,例如1 ETH = 100 YOURTOKEN。这意味着人们如果支付给你 1 个以太币,他们将获得 100 个你的代币。

你可能引入的第一个错误是人们可以向你的智能合约发送资金(以太币),但你无法提取它(要么你忘记实现提取方法,要么提取方法有问题)。这意味着你可以检查你的智能合约余额,以太币余额可能值得 100 万美元,但它将永远被困在那里,没有人能够索取它。

另一个错误可能是您忘记保护销毁智能合约的方法。在以太坊中,您有动机从区块链中删除东西,因为存储是昂贵的。因此,如果您部署了一个智能合约,您将支付燃气费,因为您的智能合约将被保留。您可以对其进行实验,然后如果您对您的智能合约感到厌倦,您可以销毁它。为此,以太坊将返还一些燃气到您的账户。这是为了阻止垃圾邮件攻击以太坊区块链。因此,回到我们智能合约错误的案例,想象一下您在智能合约中积累了价值 100 万美元的以太币,然后有人通过访问销毁它的功能销毁了您的智能合约账户。在这种情况下,您的以太币余额也将被销毁。

最后一种错误是允许黑客窃取您的以太坊余额并将其转移到他们的账户。这可能发生在许多不同的情况下。例如,也许您忘记在提款功能中设置正确的权限,或者提款功能中的权限太开放。

当然,所有这些错误都可以追溯到程序员的错误。为了避免这些错误,一个新的工作岗位诞生了——智能合约审计员,他审计您的智能合约,以确保它没有错误。然而,以太坊的发明者 Vitalik Buterin 随后审视了工具(在这种情况下是编程语言),并想知道是否可以通过改进工具本身来缓解这种情况。这种情况的罪魁祸首是 Solidity 编程语言。Vitalik 认为 Solidity 具有一些功能是强大的,但可能会产生错误。尽管 Solidity 的开发人员有改进 Solidity 安全性的计划,但 Vitalik 希望有一些自由来尝试新的视角。Vyper 就是从这里诞生的。

假设您创建了一个具有重要功能的父类。在当前或子类中,您使用此功能而不检查其定义。也许父类是由您团队中的其他人编写的。程序员有时懒得检查其他文件中的函数定义;他们会在源代码文件中上下滚动以阅读代码,但程序员通常不会检查继承功能启用的其他文件中的代码。

另一个可能使智能合约复杂且难以阅读的 Solidity 功能是修饰符,它类似于一个预备函数。以下代码显示了修饰符在 Solidity 中的使用方式:

modifier onlyBy(address _account)
{
  require(msg.sender == _account, "Sender not authorized.");
  _;
}
function withdraw() public onlyBy(owner)
{
  //withdraw money;
}

如果我们想使用withdraw()方法,智能合约将首先执行onlyBy()修饰符方法。使用require短语是为了确保调用此方法的msg.sender与作为参数发送的account变量相同。这个例子很简单。您可以一眼看完所有的代码。然而,请考虑这些函数是由许多行分隔开的,甚至在另一个文件中定义。程序员往往会忽视onlyBy()方法的定义。

函数重载是编程语言中最强大的功能之一。这是一个使您能够发送不同参数以获得不同函数的功能,如下面的代码所示:

    function flexible_function(uint _in) public {
        other_balance = _in;
    }

    function flexible_function(uint _in, uint _in2) public {
        other_balance = _in + _in2;
    }

    function flexible_function(uint _in, uint _in2, uint _in3) public {
        other_balance = _in + _in2 - _in3;
    }

然而,函数重载功能可能会误导程序员,导致他们执行不同意图的函数。程序员可能只记得flexible_function函数是这样做的,但可能会无意中执行与flexible_function不同类型的函数。

因此,一些聪明的人决定,虽然所有这些功能使得创建一个非常复杂的程序成为可能,但这些功能应该被限制在开发智能合约上。也许他们从太空飞船上编写程序的人那里得到了这个想法,在那里有规定禁止使用 C++的某些功能。或者,他们可能受到了 Java 为何被创建来取代 C++的启发。在 Java 中,不可能直接操纵内存。C++的创造者 Bjarne Stroustoup 说,C++是如此强大,以至于人们可以用 C++自己打自己的脚。

这些聪明的人决定创建一种比 Solidity 更简单的新编程语言。Python 是他们的主要灵感,因为这种编程语言的语法源自 Python。这种编程语言被称为Vyper。在 Vyper 中,诸如继承、函数重载、修饰符等功能都被移除了。Vyper 编程语言的创造者认为,移除这些功能可以使智能合约的开发更容易。重要的是,它还使代码更易于阅读。代码的阅读远远多于编写。考虑到所有这些因素,他们希望程序员在使用 Vyper 编程语言创建智能合约时能够减少错误。

安装 Vyper

默认情况下,Ubuntu Xenial 安装了 Python 3.5。Vyper 需要 Python 3.6 软件,因此如果您想使用 Ubuntu Xenial,您需要首先安装 Python 3.6。较新版本的 Ubuntu,如 Bionic Beaver,将已经安装了 Python 3.6。

因此,如果您没有安装 Python 3.6 软件,您必须首先使用以下命令安装它:

$ sudo apt-get install build-essential
$ sudo add-apt-repository ppa:deadsnakes/ppa
$ sudo apt-get update 
$ sudo apt-get install python3.6 python3.6-dev

Vyper 需要的不仅仅是 Python 3.6;您还需要安装开发文件python3.6-dev。然后,您可以通过以下步骤为 Python 3.6 创建一个虚拟环境:

  1. 首先,您必须使用以下代码安装virtualenv工具:
$ sudo apt-get install virtualenv
  1. 然后,使用以下代码为 Python 3.6 创建一个虚拟环境:
$ virtualenv -p python3.6 vyper-venv
  1. 现在,按照以下方式执行虚拟环境脚本:
$ source vyper-venv/bin/activate
  1. 接下来,使用以下命令安装 Vyper:
(vyper-venv) $ pip install vyper
  1. 如果这里没有任何错误,您就可以开始了。您可以按照以下方式测试 Vyper 编译器:
(vyper-venv) $ vyper --version
0.1.0b6

然后,您就准备好迈向下一个步骤了。

使用 Vyper 创建智能合约

现在让我们使用 Vyper 创建一个智能合约。首先,我们将创建一个扩展名为.vy的文件,并将其命名为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)

如果您来自 Solidity 或 Python 背景,您会注意到一个奇特之处:在使用 Vyper 编程语言编写的智能合约中,没有类(如 Python 编程语言中)和合约(如 Solidity 编程语言中)的概念。但是,有一个initializer函数。initializer函数的名称与 Python 编程语言中的__init__相同。

在 Python 中,您可以在一个文件中创建任意多的类。在 Vyper 中,规则是一个文件对应一个智能合约。这里也没有类或合约;文件本身就是一个类。

这是如何编译这个vyper文件的:

(vyper-venv) $ vyper hello.vy

从中,您将得到以下输出:

这是智能合约的字节码。请记住,要部署智能合约,您需要字节码,但要访问智能合约,您需要abi。那么如何获得abi?您可以通过运行以下命令来实现:

(vyper-venv) $ vyper -f json hello.vy

从中,您将得到以下输出:

如果您想在单个编译过程中同时获得abibytecode,您可以在编译过程中结合这两个标志,如下所示:

(vyper-venv) $ vyper -f json,bytecode hello.vy

这将给你以下输出:

将智能合约部署到 Ganache

那么,如何将这个智能合约部署到以太坊区块链上呢?有几种方法可以做到这一点,但让我们使用 Truffle 的一种熟悉的方式:

  1. 创建一个目录,并使用truffle init进行初始化,如下所示:
$ mkdir hello_project
$ cd hello_project
$ truffle init
  1. 就像您在上一章中所做的那样,将truffle-config.js设置为以下内容:
module.exports = {
  networks: {
    "development": {
      network_id: 5777,
      host: "localhost",
      port: 7545
    },
  }
};
  1. 创建一个build目录,如下所示:
$ mkdir -p build/contracts
$ cd build/contracts
  1. 然后在那里创建一个Hello.json文件,如下所示:
{
  "abi":
  "bytecode":
}
  1. 然后将abi字段填充为编译过程的abijson输出,并将bytecode字段填充为编译过程的bytecode输出。您需要用双引号引用bytecode值。不要忘记在abi字段和bytecode字段之间放置逗号。这将给您类似于以下内容:
{
  "abi": [{"name": "__init__", "outputs": [], "inputs": [], "constant": false, "payable": false, "type": "constructor"}, {"name": "change_name", "outputs": [], "inputs": [{"type": "bytes", "name": "new_name"}], "constant": false, "payable": false, "type": "function", "gas": 70954}, {"name": "say_hello", "outputs": [{"type": "bytes", "name": "out"}], "inputs": [], "constant": false, "payable": false, "type": "function", "gas": 8020}, {"name": "name", "outputs": [{"type": "bytes", "name": "out"}], "inputs": [], "constant": true, "payable": false, "type": "function", "gas": 5112}],
  "bytecode": "0x600035601c52740100000000000000000000000000000000000000006020526f7fffffffffffffffffffffffffffffff6040527fffffffffffffffffffffffffffffffff8000000000000000000000000000000060605274012a05f1fffffffffffffffff...
...
1600101808352811415610319575b50506020610160526040610180510160206001820306601f8201039050610160f3005b60006000fd5b61012861049703610128600039610128610497036000f3"
}
  1. 然后,您可以创建一个迁移文件来部署这个智能合约,方法是在migrations/2_deploy_hello.js中创建一个新文件,如下所示:
var Hello = artifacts.require("Hello");
module.exports = function(deployer) {
  deployer.deploy(Hello);
};

一切设置好之后,启动 Ganache!

  1. 然后,在hello_project目录中,您可以运行迁移过程,如下所示:
$ truffle migrate

您将看到类似于以下内容:

您使用 Vyper 编写的智能合约已经部署到了 Ganache。您的智能合约地址如下:

0x3E9417399786347B6Ab38f59d3f00829d6bba7b8

与智能合约交互

就像之前一样,您可以使用 Truffle 控制台与您的智能合约交互,如下所示:

$ truffle console

您的智能合约始终被赋予名称Contract。我们可以使用以下语句访问智能合约:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8")

您将得到一个长输出,其中您可以看到abibytecode等,如下面的截图所示:

让我们使用以下语句查看智能合约的name变量的值:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.name.call(); });
'0x5361746f736869204e616b616d6f746f'

您可能注意到,神秘的输出看起来并不像 Satoshi Nakamoto。但实际上,这确实是 Satoshi Nakamoto,只是用十六进制写的。让我们丢弃0x,这只是一个指示器,表明这个字符串是十六进制形式。现在您有了5361746f736869204e616b616d6f746f字符串。取前两个数字,即53,并将其转换为十进制数。在 Python 中,您可以这样做:

>>> int(0x53)
83

因此,十进制数为83。您还记得 ASCII 表吗?这是一个数据表,保存了十进制数和字符之间的关系。因此,十进制数65代表字符 A(大写 A),十进制数66代表字符 B(大写 B)。

那么十进制数83的字符是什么?您可以使用 Python 来找出,如下所示:

>>> chr(83)
'S'

如果您对每个十六进制字符都这样做,其中每个十六进制字符占两个数字字符,它将拼写出 Satoshi Nakamoto。

让我们使用以下代码在这个智能合约中执行另一个方法:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.say_hello.call(); })
'0x48656c6c6f2c205361746f736869204e616b616d6f746f'

那个神秘的输出只是Hello, Satoshi Nakamoto

让我们按照以下方式更改名称:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.change_name(web3.utils.fromAscii("Vitalik Buterin"), { from: "0x6d3eBC3000d112B70aaCA8F770B06f961C852014" }); });

您将得到以下输出:

from字段中的值来自 Ganache 中的一个账户。您可以查看 Ganache 窗口并选择任何您喜欢的账户。

我们不能直接将字符串发送到change_name方法;我们必须首先使用web3.utils.fromAscii方法将其转换为十六进制字符串。

现在名称已经更改了吗?让我们找出来。运行以下命令:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.name.call(); });
'0x566974616c696b204275746572696e'

是的,名称已经更改了。如果您将该十六进制字符串转换为 ASCII 字符串,您将得到 Vitalik Buterin。

深入了解 Vyper

让我们来看看我们的智能合约:

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)

看一下第一行:

name: public(bytes[24])

字节数组基本上就是一个字符串。名为name的变量具有bytesstring数组类型。它的可见性是public。如果要将其设置为private,只需省略 public 关键字,如下所示:

name: bytes[24]

现在,看一下接下来的几行:

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

如果您来自 Python 背景,那么您将认识到 Python 装饰器函数。Vyper 中有四个这样的函数:

  • @public表示您可以像用户一样执行此方法(就像在上一章中的 Truffle 控制台中一样)。

  • @private表示只有相同智能合约内的其他方法才能访问此方法。您不能作为用户(在 Truffle 控制台中)调用该方法。

  • @payable表示您可以向此方法发送一些以太币。

  • @const表示此方法不应更改智能合约的状态。这意味着执行此方法不会花费以太币。这就像读取公共变量的值。

回到__init__()方法,您可以像这样向该方法传递参数:

i: public(uint256)

@public
def __init__(int_param: uint256):
    self.i = int_param

在部署智能合约时不要忘记发送参数。在我们的情况下,我们在 Truffle 软件中使用迁移,因此修改您的迁移文件2_deploy_hello.js如下:

var Hello = artifacts.require("Hello");
module.exports = function(deployer) {
    deployer.deploy(Hello, 4);
};

让我们继续阅读智能合约的以下行,以了解public方法:

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

此方法修改了智能合约的状态,即name变量。这将产生燃气。

让我们继续阅读智能合约的下一行,以了解在public方法中返回值的情况:

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

concat是一个内置函数,用于组合字符串。请参考vyper.readthedocs.io/en/latest/built-in-functions.html获取完整的内置函数列表。

您必须小心方法的返回值,该值由右箭头(→)指示。例如,看一下以下代码:

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

在这种情况下,尽管“Hello,Satoshi Nakamoto”肯定少于 28 个字符,但它在编译时会失败。该字符串的长度为 23 个字符;但是,您必须记住self.name被定义为bytes[24],而Hello,的长度为 7 个字符。因为 24 + 7 是 31 个字符,所以您必须将其设置为更大的数组。

由于此方法不会更改此智能合约的状态,因此您可以在此方法的顶部添加@const,如下所示:

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

数据类型

让我们创建一个更复杂的智能合约,并将其命名为donation.vy,如下所示。您可以参考以下 GitLab 链接获取完整代码:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_03/donation.vy

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

donatur_details: public(map(address, DonaturDetail))

...
...

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

    send(self.donatee, self.balance)

像以前一样编译和部署智能合约。不要忘记删除build/contracts目录中的所有文件,并且如果您重用项目目录,则重新启动您的 Ganache。

请看以下行:

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

让我们逐一讨论 Vyper 数据类型:

  • 结构:第一个称为结构。Vyper 中的结构就像另一种编程语言中的结构一样;它是不同数据类型的容器。您可以按以下方式访问其成员:
DonaturDetail.name = "marie curie"
  • Wei:我们要了解的第二个数据类型是uint256(wei)。这指的是可以持有的特定以太币金额。正如您所知,1 个以太币等于 1,000,000,000,000,000,000 wei(18 个零)。为了持有如此大的金额,需要特定的数据类型。

  • 时间戳:第三个数据类型是timestamp数据类型。这是设计用来保存时间值的。

  • 地址:第四个是地址数据类型。这是设计用来保存地址值(例如0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF)。这可以是帐户或智能合约的地址。如果您想知道地址数据类型是什么样子,可以查看下面的 Ganache 截图。帐户地址是地址数据类型的示例。您可以向具有此数据类型的变量发送以太币:

  • 映射:第五个是map数据类型。这就像一个字典。一个简单的映射看起来是这样的:
simple_map: map(address, uint256)

在这里,键是address,值是uint256。这是如何向这个映射填充值的:

self.simple_map[0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5] = 10

如果你习惯了 Python 中的字典数据类型,那么对于这种映射数据类型,有一个小技巧:你不能迭代这个映射。所以,不要期望像在 Python 中使用dictionary数据类型的变量那样在 Vyper 中迭代映射数据类型的变量。你可以通过查看以下代码来了解它是如何工作的:

for key in self.simple_map:
    // do something with self.simple_map[key]

以太坊虚拟机EVM)不会跟踪具有映射数据类型的变量的所有键。在 Python 中,你可以从具有字典数据类型的变量中获取所有键,如下面的代码所示:

self.simple_map.keys()

但是在 Vyper 中你不能这样做。

如果你访问一个不存在的键,它会返回值数据类型的默认值。在我们的例子中,如果我们做类似这样的操作,我们会得到0,如下面的代码所示:

self.simple_map[0x1111111111111111111111111111111111111111] => 0

如果你从来没有为0x1111111111111111111111111111111111111111键设置值,或者设置为0,都没有关系。如果你想要跟踪这些键,你需要将它们保存在一个单独的数组中。映射数据类型就像 Python 中的默认字典,如下面的代码所示:

>>> from collections import defaultdict
>>> d = defaultdict(lambda: 0, {})
>>> d['a']
0
>>> d['a'] = 0
>>> d['a']
0

所以,回到我们第二个定义的变量,让我们看一下下面的代码:

donatur_details: public(map(address, DonaturDetail))

这段代码展示了一个地址到包含weistringtimestamp数据类型的结构体的映射。我们想要用这种数据类型记录捐赠者的姓名、捐赠金额和捐赠时间。

  • 数组:第五种数据类型是数组数据类型,它没有无限大小。数组的大小必须在开始时设置。

看一下这些行:

donaturs: public(address[10])

这是一个包含 10 个地址的数组。

让我们看一下下面的行,学习如何在智能合约中保留所有者的账户:

donatee: public(address)
  • 整数:第六种数据类型是整数。它类似于uint256int128。请注意,uint256uint256(wei)是不同的。int128uint256之间的区别在于int128数据类型可以保存零、正数和负数。uint256数据类型只能保存零和正数,但它的上限比int128更高。

下面的代码将保存启动这个智能合约的人的地址:

index: int128

这是为了跟踪有多少捐赠者捐赠了。请注意,它没有一个公共修饰符。这意味着你不能从 Truffle 控制台访问这个变量。

让我们来看一下__init__()方法:

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

在每个方法中,都有一些特殊的对象。其中之一是msg。你可以通过msg.sender访问访问该方法的账户。你还可以通过msg.value找到以wei为单位的以太币的数量。在下面的代码中,我们想要保存这个智能合约的启动者的地址:

@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

在这里,@payable表示这个方法接受以太币支付。assert短语类似于 Python 编程语言中的assert。如果条件为false,那么方法的执行将被中止。在assert行之后,我们只是将self.donatur_details映射设置为一个DonaturDetail结构体,键为msg.sender。在结构体内部,你可以使用block.timestamp设置时间属性,表示当前时间。as_wei_value短语是一个内置函数。由于在这个智能合约中我们必须使用 wei 单位处理以太支付,使用这个内置函数是一个好主意。否则,你必须使用很多零,如下所示:

assert msg.value >= 1000000000000000000

提取以太币

智能合约的最后几行将是一个提取捐款到donatee账户的方法,如下面的代码所示:

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

    send(self.donatee, self.balance)

在这里,self.balance代表了在这个智能合约中累积的所有以太币。send短语是一个内置函数,用于将钱转移到第一个参数,也就是donatee

所以让我们在 Truffle 控制台中测试这个智能合约。确保你将方法中的地址更改为你的智能合约的地址。你可以使用 truffle migrate 命令获取它,如下所示:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donatee.call(); });
'0xb105f01ce341ef9282dc2201bdfda2c26903da77'

这是 Ganache 中的第一个账户,如下面的截图所示:

让我们从 Ganache 中的第二个账户捐赠 2 个以太币,如下所示:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donate(web3.utils.fromAscii("lionel messi"), {from: "0x6d3eBC3000d112B70aaCA8F770B06f961C852014", value: 2000000000000000000}); });

现在从 Ganache 中的第三个账户捐赠 3.5 个以太币,如下所示:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donate(web3.utils.fromAscii("taylor swift"), {from: "0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5", value: 3500000000000000000}); });

现在用以下代码查看捐赠者的捐赠情况:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.donatur_details__sum.call("0x9049386D4d5808e0Cd9e294F2aA3d70F01Fbf0C5"); });
<BN: 30927f74c9de0000>

你访问结构的属性的方式是在 donatur_details 结构后使用两个下划线。你将映射的键放在 call 函数中。如果你想知道 <BN: 30927f74c9de0000> 中的 30927f74c9de0000 是什么意思,它不是内存的位置,而是一个十六进制格式的数字。因为这个数字非常大(BN 是大数的缩写),EVM 必须以十六进制格式显示这个数字,如下所示:

truffle(development)> web3.utils.toBN(15);
<BN: f>
truffle(development)> web3.utils.toBN(9);
<BN: 9>
truffle(development)> web3.utils.toBN(100);
<BN: 64>
truffle(development)> web3.utils.toBN(3500000000000000000);
<BN: 30927f74c9de0000>

如果你看一下 Ganache,第二和第三个账户已经失去了一些钱,如下面的截图所示:

所以,让我们使用以下代码撤回捐赠:

truffle(development)> Contract.at("0x3E9417399786347B6Ab38f59d3f00829d6bba7b8").then(function(instance) { return instance.withdraw_donation({from: "0xb105F01Ce341Ef9282dc2201BDfdA2c26903da77"}); });

看一下你的 Ganache。在我的情况下,第一个账户有 105.48 ETH,如下面的截图所示:

其他数据类型

Vyper 还有其他数据类型,这些数据类型在捐赠智能合约中没有使用,如下面的列表所示:

  • bool:这种数据类型类似于普通的布尔值。它包含 true 或 false 值,如下面的代码所示:
bull_or_bear: bool = True
  • decimal:这种数据类型类似于 Python 中的 floatdouble,如下面的代码所示:
half_of_my_heart: decimal = 0.5
  • bytes32:这种数据类型类似于 bytes32,但有一个特点。如果值的长度小于 32 字节,它将用零字节填充。所以,如果你将 messi 值(5 个字符/字节)设置为 bytes32 数据类型变量(如下面的代码所示),它将变成 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
goat: bytes32 = convert('messi', bytes32)
  • Constant:这种数据类型在声明后不能被改变:
GOAT: constant(bytes[6]) = 'messi'

与 C++ 编程语言不同,未初始化的变量可以有垃圾值,Vyper 编程语言中所有未初始化的变量都有默认值。默认的整数数据类型值是 0。默认的布尔数据类型值是 false

有用的内置函数

你已经使用了内置函数,比如 sendassertas_wei_valueconcatconvert。然而,还有其他有用的函数,比如以下的函数:

  • sliceslice 短语是字节数据类型。它用于从字符串中获取子字符串等任务,如下面的代码所示:
first_name: bytes[10] = slice(name, start=0, len=10)
  • len:此函数用于获取值的长度,如下面的代码所示:
length_of_name: int128 = len(name)
  • selfdestruct:此函数用于销毁智能合约,如下面的代码所示。参数是这个智能合约发送其以太币的地址:
selfdestruct(self.donatee)
  • ceil:这个函数用于将整数四舍五入到上限,如下面的代码所示:
round_heart: int128 = ceil(half_of_my_heart)
  • floor:这个函数用于将整数四舍五入到下限,如下面的代码所示:
round_heart: int128 = floor(half_of_my_heart)
  • sha3:这是一个内置的哈希函数,如下面的代码所示:
secret_hash: bytes32 = sha3('messi')

事件

Vyper 支持事件。你可以在你的方法中向任何订阅者广播事件。例如,当人们用智能合约捐赠以太币时,你可以广播一个捐赠事件。要声明一个事件,你可以使用以下语句:

Donate: event({_from: indexed(address),  _value: uint256(wei)})

然后,在我们的 donate 方法中,你可以在捐赠交易发生后广播事件,如下面的代码所示:

@public
def donate(name: bytes[100]):
    log.Donate(msg.sender, msg.value)

我们将在后面的章节中更多地讨论事件。

与其他智能合约交互

你知道你的智能合约不必在那里孤独吗?你的智能合约可以与区块链上的其他智能合约进行交互。

地址数据类型不仅用于普通账户,还可以用于智能合约账户。因此,智能合约可以通过捐赠智能合约向我们的受赠人捐赠以太币!

重新启动您的 Ganache;我们将重新启动我们的区块链。还记得您的hello.vy Vyper 文件吗?我们想要部署我们的Hello智能合约并自定义名称。

我们的迁移文件migrations/2_deploy_hello.js仍然是相同的,如下所示:

var Hello = artifacts.require("Hello");
module.exports = function(deployer) {
  deployer.deploy(Hello);
};

再次编译您的hello.vy文件以获取接口和字节码。打开我们的合同 JSON 文件,build/contracts/Hello.json文件。清除所有内容并用以下代码替换它:

{
  "contractName": "Hello",
  "abi": <your Hello smart contract's interface>,
  "bytecode": "<your Hello smart contract's bytecode>"
}

您必须为您的智能合约命名,因为这次,您将部署两个智能合约。如果您不为您的智能合约命名,它将具有默认名称Contract。如果您只想部署一个智能合约,这不是问题。

然后,对于您的donation.vy,编辑它,并将以下代码行(加粗显示)添加到代码文件中(请参考以下 GitLab 链接中donation.vy的完整代码文件:gitlab.com/arjunaskykok/hands-on-blockchain-for-python-developers/blob/master/chapter_03/donation.vy):

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

contract Hello():
 def say_hello() -> bytes[32]: constant

donatur_details: public(map(address, DonaturDetail))

...
...

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

    send(self.donatee, self.balance)

@public
@constant
def donation_smart_contract_call_hello_smart_contract_method(smart_contract_address: address) -> bytes[32]:
 return Hello(smart_contract_address).say_hello()

请注意加粗的更改。这些更改是您要与之交互的智能合约接口的声明方式;您声明合约对象和要与之交互的方法。您不需要知道say_hello方法的实现,只需要知道接口(即它期望的参数和返回值)。

然后调用外部智能合约的donation_smart_contract_call_hello_smart_contract_method方法。将地址作为参数发送给合约对象并像往常一样调用该方法。如果您已经知道要与之交互的智能合约的地址,可以硬编码它。但我使用参数是因为我还不知道Hello智能合约的地址。

使用以下代码,为我们升级的Donation智能合约创建另一个迁移文件,migrations/3_deploy_donation.js

var Donation = artifacts.require("Donation");
module.exports = function(deployer) {
  deployer.deploy(Donation);
};

编译您的donation.vy并获取智能合约的接口和字节码。

然后,使用以下代码,为我们的Donation智能合约创建另一个合同 JSON 文件,build/contracts/Donation.json

{
  "contractName": "Donation",
  "abi": <your Donation smart contract's interface>,
  "bytecode": "<your Donation smart contract's bytecode>"
}

运行迁移。您可能需要使用--reset标志,如下所示:

$ truffle migrate --reset

您将获得以下输出:

注意Donation智能合约的地址和Hello智能合约的地址。Donation智能合约的地址是0x98Db4235158831BF9133faC1c4e1829021ecEB67Hello智能合约的地址是0xBc932d934cfE859F9Dc903fdd5DE135F32EbC20E。您的地址可能不同。

按以下方式运行 Truffle 控制台:

$ truffle console

现在我们的智能合约不再孤单,如下所示:

truffle(development)> Donation.at("0x98Db4235158831BF9133faC1c4e1829021ecEB67").then(function(instance) { return instance.donation_smart_contract_call_hello_smart_contract_method.call("0xBc932d934cfE859F9Dc903fdd5DE135F32EbC20E"); } );
'0x48656c6c6f2c205361746f736869204e616b616d6f746f'

智能合约之间交互的用例之一是创建一个去中心化交易智能合约。假设您的祖母启动了一个名为电网代币的代币智能合约,您的叔叔启动了一个名为 Wi-Fi 接入代币的代币智能合约。您可以创建一个智能合约,与电网代币和 Wi-Fi 接入代币进行交互。在您的智能合约中,您可以创建一个方法来启用这两个代币之间的交易;您只需获取它们的智能合约地址和接口。当然,您还需要编写交易的逻辑。

以编程方式编译代码

您可以创建一个脚本来编译 Vyper 代码,而不是使用命令行实用程序。确保您在包含hello.vydonation.vy的相同目录中。创建一个名为compiler.vy的脚本,如下所示:

import vyper
import os, json

filename = 'hello.vy'
contract_name = 'Hello'
contract_json_file = open('Hello.json', 'w')

with open(filename, 'r') as f:
    content = f.read()

current_directory = os.curdir

smart_contract = {}
smart_contract[current_directory] = content

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

smart_contract_json = {
    'contractName': contract_name,
    'abi': compiled_code[current_directory]['abi'],
    'bytecode': compiled_code[current_directory]['bytecode']
}

json.dump(smart_contract_json, contract_json_file)

contract_json_file.close()

如果你使用以下命令执行这个脚本,你将得到一个Hello.json文件,你可以在 Truffle 中使用,如下代码所示:

(vyper-venv) $ python compiler.py

现在,让我们逐步学习这个脚本。首先,导入Vyper库和一些 Python 标准库,这样我们就可以写一个 JSON 文件,如下所示:

import vyper
import os, json

你需要一个 Vyper 文件,你想要给你的智能合约的名称,以及输出的 JSON 文件。以下代码将完成这个任务:

filename = 'hello.vy'
contract_name = 'Hello'
contract_json_file = open('Hello.json', 'w')

使用以下代码行获取 Vyper 文件的内容:

with open(filename, 'r') as f:
    content = f.read()

然后创建一个字典对象,其中键是指向你的 Vyper 文件的路径,值是 Vyper 文件的内容,如下所示:

current_directory = os.curdir

smart_contract = {}
smart_contract[current_directory] = content

要编译 Vyper 代码,你只需要使用vyper模块的compile_codes方法,如下所示:

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

compile_codes方法的第一个参数是一个字典,其中键指向路径,值表示字符串中的 Vyper 代码。第二个参数是format,包括接口和字节码。第三个参数是可选的。如果你使用'dict',那么你将得到一个字典。如果你不给出第三个参数,那么你将得到一个数组。让我们看一下以下代码:

smart_contract_json = {
    'contractName': contract_name,
    'abi': compiled_code[current_directory]['abi'],
    'bytecode': compiled_code[current_directory]['bytecode']
}

因为我们使用了'dict'作为我们的第三个参数,我们得到了一个字典对象的结果。结果的键是我们的 Vyper 文件的路径。从技术上讲,你可以将它设置为任何你喜欢的字符串。一些开发人员使用文件路径来区分他们在项目目录中散布的 Vyper 文件。

最后的代码用于将结果写入输出的 JSON 文件:

json.dump(smart_contract_json, contract_json_file)

contract_json_file.close()

通过以编程方式编译 Vyper 代码,你可以在 Vyper 之上构建一个框架。在本书的后面章节中,你将使用一个名为 Populus 的框架来编译和部署 Vyper 文件。但你可能想要构建一个更好的框架,或者你可以构建一个 Vyper 集成开发环境(IDE),比如 JetBrains IDE,但用于 Vyper 编程语言。

其他技巧

Vyper 不像 Python 那样自由;有一些限制是你必须接受的。要克服这些限制,你需要与它们和解,或者你需要释放你的创造力。以下是一些关于如何做到这一点的提示。

第一个限制是数组必须有固定的大小。在 Python 中,你可能非常习惯于拥有一个可以根据需要扩展的列表,如下代码所示:

>>> flexible_list = []
>>> flexible_list.append('bitcoin')
>>> flexible_list.append('ethereum')
>>> flexible_list
['bitcoin', 'ethereum']

在 Vyper 中没有这样的东西。你必须声明你的数组有多大。然后你必须使用一个整数变量来跟踪你已经插入到这个固定大小数组中的项目数量。你在Donation智能合约中使用了这种策略。

如果你渴望拥有一个无限大小的数组,有一种方法可以实现这一点。你可以使用整数作为键的映射数据类型。你仍然使用一个整数变量来跟踪你已经插入到这个映射数据类型变量中的项目数量,如下代码所示:

infinite_array_of_strings: map(uint256, bytes[100])
index: int128

但由于infinite_array_of_strings是一个映射数据类型,你有责任保护这个变量免受非整数键的影响。

第二个限制是映射数据类型不能接受复合数据类型作为键。因此,你不能将映射数据类型或结构数据类型作为键。但它可以接受映射数据类型或结构数据类型作为值,如下代码所示:

mapping_of_mapping_of_mapping: map(uint256, map(uint256, map(uint256, bytes[10])))

如果你想将结构体作为映射数据类型变量的键,你可以先对它们进行序列化。例如,如果你想将两个字符串作为映射数据类型变量的键,你可以将这些字符串连接起来,作为你的映射数据类型变量的键,如下代码所示:

friend1_str: bytes32 = convert(friend1, bytes32)
friend2_str: bytes32 = convert(friend2, bytes32)
key: bytes[100] = concat(friend1_str, friend2_str)

dating[key] = True

或者你可以使用嵌套数组,如下所示:

dating[friend1_address][friend2_address] = True

哪种方法更好取决于情况和你的偏好。

第三个限制是 Vyper 编程语言无法访问现实世界。因此,在你的智能合约中不要想象以下内容:

nba_final_winner = nba.get_json_winner('2019/2020')

摘要

在本章中,我们学习了如何使用 Vyper 编程语言编写智能合约。首先,我们安装了 Vyper 编译器。然后我们开发了一个智能合约。通过这样做,我们了解了 Vyper 编程语言的大部分特性,包括函数修饰符、初始化函数和函数权限修改器。还有一些数据类型,如地址、整数、时间戳、映射、数组和字节数组(字符串)。我们学会了如何将 Vyper 源代码编译成智能合约,然后使用 Truffle 工具将其部署到 Ganache 上。我们还通过 Truffle 控制台与该智能合约进行了交互。

在下一章中,我们将学习关于web3.py。这是构建去中心化应用的第一步。