Tezos 和基于 SmartPy 的智能合约开发
SmartPy和Tezos编程入门
本课程介绍了Tezos区块链及其智能合约语言SmartPy。我们编写、学习并测试了自己的第一个SmartPy合约。这只是一个开始,关于SmartPy和Tezos,还有很多需要学习和探索的东西。
区块链、Tezos和SmartPy介绍
1.1 区块链基础
在全面了解Tezos和SmartPy之前,我们必须先了解支撑所有这些技术的底层技术——区块链。区块链是由众多区块组成的链式结构,每个区块包含一个交易列表。区块链技术提供了一个去中心化的交易数据库,即“数字分类账”,对所有网络参与者可见。它的架构保证每笔交易都是唯一的,一旦记录在数据库中,便无法更改。
1.2 Tezos介绍
Tezos是一个典型的区块链平台,它与众多其他区块链平台(如比特币或以太坊)的区别在于其强调“自我修正”,允许协议在不进行硬分叉的情况下进行升级。这是一个重大优势,使协议具有适应性且不会过时。
Tezos还提供了一个智能合约平台。智能合约是一种自动执行的合约,买卖双方之间的协议直接通过代码编写。这种管理和验证数字协议的能力使Tezos在诸多领域具有潜在应用,包括金融服务、供应链、去中心化应用(DApp)等。
1.3 SmartPy:Tezos的智能合约语言
要在Tezos上创建智能合约,我们使用一种叫做SmartPy的语言。SmartPy是一个用于开发Tezos区块链智能合约的Python库。它是一种直观有效的语言,用于体现合约及相关的测试场景。
SmartPy最显著的特点是它与Python的整合,Python是世界上使用最广且发展最快的编程语言之一。如果你对Python已经有一定了解,那么学习SmartPy会相对容易。
用SmartPy创建你的第一份合约
访问SmartPy IDE
SmartPy包含一个功能齐全的集成开发环境(IDE),可通过浏览器访问。进入SmartPy IDE后,便可以开始编写你的第一个智能合约了。

编写智能合约
在SmartPy IDE中,找到编辑窗口,输入合约代码。我们首先来编写一个基本的合约。复制以下代码并粘贴到SmartPy编辑器中:
Python
import smartpy as sp
# A SmartPy module
@sp.module
def main():
# A class of contracts
class MyContract(sp.Contract):
def __init__(self, myParameter1, myParameter2):
self.data.myParameter1 = myParameter1
self.data.myParameter2 = myParameter2
# An entrypoint, i.e., a message receiver
# (contracts react to messages)
@sp.entrypoint
def myEntryPoint(self, params):
assert self.data.myParameter1 <= 123
self.data.myParameter1 += params
合约解析
该合约代码包含以下几个重要部分:
import smartpy as sp- 此代码将导入SmartPy库,我们可以用它来编写合约。
@sp.module- 此装饰器告知SmartPy解释器,这个函数将包含一个SmartPy合约。
class MyContract(sp.Contract):- 在这里,我们为合约定义一个新类(用于创建新对象的蓝图)。它继承自sp.Contract超类,该超类提供了许多有用的功能,用于处理合约的状态并定义其行为。
self.data.myParameter1 = myParameter1和self.data.myParameter2 = myParameter2- 这里我们定义了合约的初始状态。这两个参数将在部署到区块链时传递给合约。
@sp.entrypoint- 此装饰器告知解释器,函数myEntryPoint是合约的入口点。入口点是我们部署合约后与合约交互的方式。
self.data.myParameter1 += params- 此行代码将myParameter1的值增加一定数量,此数量是传给myEntryPoint的量。
合约测试
编写合约的一个重要内容是对其进行彻底的测试。在SmartPy中,测试与开发过程相融合。使用以下代码将测试添加到合约中:
Python
# Tests
@sp.add_test(name="Welcome")
def test():
# We define a test scenario, together with some outputs and checks
# The scenario takes the module as a parameter
scenario = sp.test_scenario(main)
scenario.h1("Welcome")
# We first define a contract and add it to the scenario
c1 = main.MyContract(12, 123)
scenario += c1
# And call some of its entrypoints
c1.myEntryPoint(12)
c1.myEntryPoint(13)
c1.myEntryPoint(14)
c1.myEntryPoint(50)
c1.myEntryPoint(50)
c1.myEntryPoint(50).run(valid=False) # this is expected to fail
# Finally, we check its final storage
scenario.verify(c1.data.myParameter1 == 151)
# We can define another contract using the current state of c1
c2 = main.MyContract(1, c1.data.myParameter1)
scenario += c2
scenario.verify(c2.data.myParameter2 == 151)
合约运行
编写好的完整合约代码应如下所示:
Python
import smartpy as sp
# This is the SmartPy editor.
# You can experiment with SmartPy by loading a template.
# (in the Commands menu above this editor)
#
# A typical SmartPy program has the following form:
# A SmartPy module
@sp.module
def main():
# A class of contracts
class MyContract(sp.Contract):
def __init__(self, myParameter1, myParameter2):
self.data.myParameter1 = myParameter1
self.data.myParameter2 = myParameter2
# An entrypoint, i.e., a message receiver
# (contracts react to messages)
@sp.entrypoint
def myEntryPoint(self, params):
assert self.data.myParameter1 <= 123
self.data.myParameter1 += params
# Tests
@sp.add_test(name="Welcome")
def test():
# We define a test scenario, together with some outputs and checks
# The scenario takes the module as a parameter
scenario = sp.test_scenario(main)
scenario.h1("Welcome")
# We first define a contract and add it to the scenario
c1 = main.MyContract(12, 123)
scenario += c1
# And call some of its entrypoints
c1.myEntryPoint(12)
c1.myEntryPoint(13)
c1.myEntryPoint(14)
c1.myEntryPoint(50)
c1.myEntryPoint(50)
c1.myEntryPoint(50).run(valid=False) # this is expected to fail
# Finally, we check its final storage
scenario.verify(c1.data.myParameter1 == 151)
# We can define another contract using the current state of c1
c2 = main.MyContract(1, c1.data.myParameter1)
scenario += c2
scenario.verify(c2.data.myParameter2 == 151)
编写好合约和测试代码后,可以直接在IDE中运行它们。单击IDE右上角的“Run”按钮。IDE将对合约进行编译,运行测试,并显示输出结果以及每个操作和状态变化的详细说明。
我们编写、学习并测试了自己的第一个SmartPy合约。这只是一个开始,关于SmartPy和Tezos,还有很多需要学习和探索的东西。不要走开,继续跟我们进行这场探索之旅吧!
合约存储
什么是存储?
在Tezos智能合约中,存储就像合约的内存,是保存与合约相关的所有数据的地方。从本质上说,它充当我们合约的状态,存储在多笔交易中持续存在的值,并使智能合约能够“记住”信息。正是这种能力使我们能够在Tezos区块链上构建复杂且有趣的去中心化应用。
深入了解存储
在分析本节课的代码之前,我们将进一步了解存储的概念。合约的存储是在函数调用之间持续存在的状态。如果你具有编程背景,可以将其视为合约的“全局状态”。它允许用户与合约进行持续的互动。
我们来看看本节课的合约代码:
Python
import smartpy as sp
@sp.module
def main():
class StoreValue(sp.Contract):
def __init__(self, value):
self.data.storedValue = value
@sp.entrypoint
def replace(self, params):
self.data.storedValue = params.value
@sp.entrypoint
def double(self):
self.data.storedValue *= 2
@sp.entrypoint
def divide(self, params):
assert params.divisor > 5
self.data.storedValue /= params.divisor
if "templates" not in __name__:
@sp.add_test(name="StoreValue")
def test():
c1 = main.StoreValue(12)
scenario = sp.test_scenario(main)
scenario.h1("Store Value")
scenario += c1
c1.replace(value=15)
scenario.p("Some computation").show(c1.data.storedValue * 12)
c1.replace(value=25)
c1.double()
c1.divide(divisor=2).run(
valid=False, exception="WrongCondition: params.divisor > 5"
)
scenario.verify(c1.data.storedValue == 50)
c1.divide(divisor=6)
scenario.verify(c1.data.storedValue == 8)
代码解析与合约运行
在我们的存储合约中,有多个入口点——replace、double和divide。部署合约后,用户可以调用这些入口点与合约进行交互。
对于replace和divide入口点,用户必须在交易中提供参数。对于replace,需要参数value,对于divide,需要参数divisor。
在SmartPy IDE上运行此合约时,你可以在右侧看到合约运算和存储的可视化表示。你可以在这里尝试运行合约,具体步骤如下:
点击Deploy按钮,部署你的合约。
部署后,你将在Contracts下看到该合约。点击该合约。
现在可以看到所列合约的入口点。
要调用replace,请在字段中输入params.value的值,然后单击replace按钮。
要调用double,只需单击double按钮。
要调用divide,请在字段中输入params.divisor的值,然后单击divide按钮。
每一次运行都会创建一个新运算,可以在每次运算后看到更新的合约存储状态。

在这个合约中,我们通过self.data.storedValue = value命令行强调了存储的概念。其中,self.data指的是我们合约的存储。这是我们保存合约状态的地方:一个名为storedValue的单一参数。
本合约也包含多个入口点。入口点本质上是允许外部方与合约交互的公共函数。这里的入口点允许以各种方式修改storedValue。我们可以用一个新值替换它,或者将其翻倍,也可以用给定的除数除以它。
最后,我们可以试验一下如下场景:为storedValue创建一个初始值为12的合约实例,然后调用入口点以各种方式修改storedValue的值并验证结果。
存储的重要性
在构建Tezos智能合同时,存储和更新值的能力非常重要。它允许数据在与合约的不同交互中保持持久性。无论是维护代币合约中的余额,在去中心化应用中存储用户信息,还是在区块链上保存游戏状态,存储都是实现这些功能的核心所在。
智能合约中的存储可以包含简单的值,如整数、字符串和布尔值,也可以包含更复杂的数据结构,如列表、映射和自定义对象。这使我们能够在合约中构建复杂的逻辑和状态转换。
下面我们将继续探讨这些核心概念,在智能合约中引入更复杂的计算,并在我们的存储合约中使用更高级的数据类型。请继续关注。俗话说,熟能生巧。所以,不要犹豫,动手编写你的代码吧,尝试多多修改并观察结果,你一定能取得进步!
构建智能合约计算器
理论
Tezos上的智能合约可以有多个入口点,入口点可以看作是面向对象编程中的方法或函数。每个入口点都可以有自己的参数,并且可以与合约的存储进行交互。在我们的计算器合约中,每个数学运算都将是一个入口点。
需要注意的是,对存储数据的任何修改都将记录在区块链上。因此,我们执行的运算不像常规计算器中那样只能短暂存储。在Tezos区块链上,这些运算是不可篡改且可审计的。
此外,需要注意的是,由于Tezos区块链是去中心化的,所有计算都应该是确定性的。因此,像除法这样的运算可能与你所熟悉的稍有不同。在Tezos合约中,除法是整数除法,因此3除以2将得到1,而不是1.5。
实操
下面是该计算器合约的代码。Calculator合约将运算结果保存在其存储空间中。每个入口点都将接收一个参数,并使用存储结果和输入参数执行运算。
Python
import smartpy as sp
@sp.module
def main():
class Calculator(sp.Contract):
def __init__(self):
self.data.result = 0
@sp.entrypoint
def multiply(self, x, y):
self.data.result = x * y
@sp.entrypoint
def add(self, x, y):
self.data.result = x + y
@sp.entrypoint
def square(self, x):
self.data.result = x * x
@sp.entrypoint
def squareRoot(self, x):
assert x >= 0
y = x
while y * y > x:
y = (x / y + y) / 2
assert y * y <= x and x < (y + 1) * (y + 1)
self.data.result = y
@sp.entrypoint
def factorial(self, x):
self.data.result = 1
for y in range(1, x + 1):
self.data.result *= y
@sp.entrypoint
def log2(self, x):
self.data.result = 0
y = x
while y > 1:
self.data.result += 1
y /= 2
if "templates" not in __name__:
@sp.add_test(name="Calculator")
def test():
c1 = main.Calculator()
scenario = sp.test_scenario(main)
scenario.h1("Calculator")
scenario += c1
c1.multiply(x=2, y=5)
c1.add(x=2, y=5)
c1.add(x=2, y=5)
c1.square(12)
c1.squareRoot(0)
c1.squareRoot(1234)
c1.factorial(100)
c1.log2(c1.data.result)
scenario.verify(c1.data.result == 524)
我们来实际操作一下如何实现这份合约!
第一步:将合约代码粘贴到SmartPy IDE中。
第二步:点击右上角的Run按钮来编译并模拟合约。
第三步:观察IDE右侧的模拟结果。你可以看到每次运算(如乘法、加法、平方根等)后合约存储的状态。
第4步:任意修改运算参数并观察合约存储的变化!

现在,你已经学会了构建和运行计算器智能合约,该合约能够执行基本的运算!我们将学习更高级的概念,如FIFO合约创建。最后,再次勉励大家,继续探索,开启愉快的编程之旅!
FIFO合约创建
理论
在FIFO的数据结构中,第一个加入队列的元素也将是第一个被移除的元素。也就是说,一旦添加了新元素,必须先移除之前添加的所有元素,才能移除新元素。
在智能合约中,实现FIFO队列可以用于许多场景,例如公平的排队系统,每个人都按照他们到达的顺序得到服务(或处理)。
实操
我们直接开始编写一个FIFO合约。合约的两个主要运算将是push(用于向队列添加元素)和pop(用于从队列中移除元素)。
合约将队列存储在其存储空间中的列表中,每次push运算都会将一个元素追加到列表的末尾,而每次pop运算都会从列表的开头移除一个元素。
合约代码如下:
Python
import smartpy as sp
@sp.module
def main():
# The Fifo class defines a simple contract that handles push and pop instructions
# on a first-in first-out basis.
class SimpleFifo(sp.Contract):
def __init__(self):
self.data.first = 0
self.data.last = -1
self.data.saved = {}
@sp.entrypoint
def pop(self):
assert self.data.first < self.data.last
del self.data.saved[self.data.first]
self.data.first += 1
@sp.entrypoint
def push(self, element):
self.data.last += 1
self.data.saved[self.data.last] = element
@sp.onchain_view
def head(self):
return self.data.saved[self.data.first]
if "templates" not in __name__:
@sp.add_test(name="Fifo")
def test():
scenario = sp.test_scenario(main)
scenario.h1("Simple Fifo Contract")
c1 = main.SimpleFifo()
scenario += c1
c1.push(4)
c1.push(5)
c1.push(6)
c1.push(7)
c1.pop()
scenario.verify(sp.View(c1, "head")() == 5)
测试FIFO合约:
第一步:复制合约代码并粘贴至SmartPy IDE中。
第二步:点击右上角的Run按钮来编译和模拟合约。
第三步:查看IDE右侧的模拟结果。你将看到每次运算后合同存储的状态。
第四步:尝试更改运算的顺序或添加新运算。
递归视图与斐波那契数列
在计算机科学领域,递归指问题的解决方案取决于相同问题的较小实例。它是一个函数作为子程序调用自身的过程。这使得函数可以用较少的参数被调用,减少了函数需要维护的状态信息的数量,并提供了一种简洁的方式来表达解决某些类型问题的方法。对于初学者来说,递归通常被认为是一个具有挑战性的概念,但如果学习得当,它可以成为程序员的一个强大工具,有助于创建更简洁的代码,并且通常可以用来用简单的方案解决复杂问题。 在本课中,我们将把递归应用于斐波那契数列。斐波那契数列是一串数字,从第三个数字开始,每个数字都是前两个数字之和,如0、1、1、2、3、5、8、13、21、34,以此类推。该数列从0开始,第n个数字是第(n-1)个和第(n-2)个数字的和。
SmartPy中的递归视图
在SmartPy中,递归是通过允许函数在其自己的定义中调用自身来实现的。在处理可以分解为更小的、相同的子问题时,这种方法非常有用。视图(view)是一个SmartPy函数,它不会修改合约存储,但可以读取合约内容。我们所说的递归视图是指在执行过程中调用自身的视图函数。
斐波那契数列
斐波那契数列是一组数字,前两项为0和1,从第三项开始,每个数字都是前两个数字的和。这个数列虽然简单,但由于它是递归性的,可以很好地帮助我们理解递归。
斐波那契数列由递归关系定义:
SCSS
F(n) = F(n-1) + F(n-2)
该数列中,初始条件是F(0) = 0和F(1) = 1。要得到斐波那契数列中的第n个数字,我们只需将第(n-1)个和第(n-2)个数字相加。这种递归性正是斐波那契数列非常适合理解递归的原因。了解了递归及其在斐波那契数列中的应用之后,我们将深入研究实现递归视图以计算斐波那契数列的SmartPy代码。
代码解析
以下SmartPy代码定义了一个合约FibonacciView,它使用递归视图计算斐波那契数列。这个例子可以很好地帮助我们理解如何在SmartPy中创建和使用递归函数。
Python
import smartpy as sp
@sp.module
def main():
class FibonacciView(sp.Contract):
"""Contract with a recursing view to compute the sum of fibonacci numbers."""
@sp.onchain_view()
def fibonacci(self, n):
"""Return the sum of fibonacci numbers until n.
Args:
n (sp.int): number of fibonacci numbers to sum.
Return:
(sp.int): the sum of fibonacci numbers
"""
sp.cast(n, int)
if n < 2:
return n
else:
n1 = sp.view("fibonacci", sp.self_address(), n - 1, int).unwrap_some()
n2 = sp.view("fibonacci", sp.self_address(), n - 2, int).unwrap_some()
return n1 + n2
if "templates" not in __name__:
@sp.add_test(name="FibonacciView basic scenario", is_default=True)
def basic_scenario():
sc = sp.test_scenario(main)
sc.h1("Basic scenario.")
sc.h2("Origination.")
c1 = main.FibonacciView()
sc += c1
sc.verify(c1.fibonacci(8) == 21)
这个FibonacciView合约包含一个递归视图函数fibonacci,它返回第n个斐波那契数。
该函数用@sp.onchain_view()装饰器,表示这是对合约存储的只读操作。该函数将整数n作为参数,代表我们要检索的斐波那契数列中的位置。
在函数内部,为了安全起见,我们首先将n转换为整数。接下来是函数的递归部分。如果n小于2,我们只返回n,因为斐波那契数列的前两个数字是0和1。如果n大于或等于2,我们用递归的方式调用n-1和n-2的fibonacci函数,然后将结果相加来计算第n个斐波那契数。这符合定义斐波那契数列的递归关系。sp.view调用创建了对fibonacci函数本身的递归调用。
我们继续看看测试部分。
在测试函数basic_scenario中,创建一个新的FibonacciView合约实例,将其添加到本场景中,然后使用sc.verify检查第8个斐波那契数是否为21,若是,则斐波那契数列是正确的。
在SmartPy IDE中运行代码
运行代码:
打开SmartPy IDE。
将我们提供的代码复制并粘贴到编辑器中。
单击“Run”按钮,在IDE右侧会显示正在执行的测试场景。你可以看到正在执行的操作和验证的检查。
在这节课中,我们介绍了很多高级概念,包括递归的基础知识、它在编程中的使用、SmartPy中的递归视图,以及递归在斐波那契数列中的应用。我们还探讨了SmartPy中的一个工作代码示例,还学习了如何在SmartPy IDE中运行和验证此代码。
课程总结
课程回顾
SmartPy和Tezos编程入门:在课程开始,我们首先介绍了区块链技术,并重点关注Tezos。我们讨论了智能合约和SmartPy语言,学习了如何设置和使用SmartPy IDE。
合约存储:我们深入探讨了合约存储的概念,学习了如何创建一个存储合约。
构建智能合约计算器:我们构建了一个基本的智能合约计算器,进一步了解了合约存储的工作原理,并介绍了入口点(合约中的一种函数)。
FIFO合约创建:我们构建了先进先出(FIFO)合约,探讨了如何在合约中实现更复杂的逻辑。本节课演示了合约存储和入口点在创建有状态合约方面的强大功能。
递归视图与斐波那契数列:我们介绍了计算机科学中的一个基本概念——递归,用递归视图来计算斐波那契的数字。递归视图是可以调用自身的链上合约查询函数,为复杂的合约交互和计算提供了强大的工具。
学习建议
我们的编程之旅才刚刚开始!从本部分课程中学到的基础知识和技能将助力你进一步探究Tezos和SmartPy相关概念。Tezos上的智能合约编程是一个不断发展的领域,为我们带来了众多新兴话题。完成第一部分的学习后,你就迈出了成为经验丰富的Tezos和SmartPy开发人员的一大步。