FlowCards:用于开发Ergo dApps的声明式框架
2020年4月29日

感谢Robert Kornacki对草稿的润色。
介绍
ErgoScript是Ergo区块链使用的智能合约语言。虽然它采用了来自Scala/Kotlin的简洁语法,但由于概念上与我们熟知和喜爱的传统语言有很大不同,最初可能会让人感到困惑。这是因为Ergo是基于UTXO的区块链,而智能合约传统上与以太坊等基于账户的系统相关联。然而,Ergo的交易模型在许多方面优于基于账户的模型,采用正确的方法开发Ergo合约甚至可能比编写和调试Solidity代码要简单得多。
下面我们将介绍Ergo合约模型的关键方面,使其与众不同:
范式
以太坊的账户模型是命令式的。这意味着将硬币从Alice发送到Bob的典型任务需要通过一系列操作来更改存储中的余额。另一方面,Ergo的基于UTXO的编程模型是声明式的。ErgoScript合约指定了交易被区块链接受的条件(而不是合约执行结果所需的存储状态更改)。
可扩展性
在以太坊的账户模型中,存储更改和有效性检查是在代码执行期间在链上进行的。相比之下,Ergo交易是在链下创建的,只有验证检查是在链上进行,从而减少了网络中每个节点执行的操作数量。此外,由于交易图的不可变性,可能采用各种优化策略来提高网络中每秒的交易吞吐量。轻量验证节点也是可能的,从而进一步促进网络的可扩展性和可访问性。
共享状态
基于账户的模型依赖于共享可变状态,这在并发/分布式计算的背景下已知会导致复杂的语义(和微妙的百万美元错误)。Ergo的模型基于不可变的交易图。这种方法,继承自比特币,适合区块链的并发和分布式特性,并促进了轻量无信任客户端的实现。
表达能力
以太坊提倡在区块链上执行图灵完备的语言。理论上,它承诺了无限的潜力,但在实践中,由于过度的区块链膨胀、微妙的百万美元错误、限制合约复杂性的燃气费用以及其他问题,严重的限制浮出水面。相反,Ergo扩展了UTXO以实现图灵完备,同时限制了ErgoScript语言本身的复杂性。以不同且更具语义合理性的方式实现了相同的表达能力。
综上所述,Ergo所使用的模型显然有很多好处。在本文的其余部分,我将向您介绍FlowCards的概念——一个dApp开发者组件,允许以声明式和可视化的方式设计复杂的Ergo合约。
从命令式到声明式
在以太坊的命令式编程模型中,交易是一系列由以太坊虚拟机执行的操作。以下Solidity函数实现了从sender到receiver的代币转移。交易在sender调用此函数时开始,并在函数返回时结束。
// 将现有硬币的数量从任何调用者发送到一个地址
function send(address receiver, uint amount) public {
require(amount <= balances[msg.sender], "余额不足。");
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Sent(msg.sender, receiver, amount);
}
该函数首先检查前置条件,然后更新存储(即余额),最后将后置条件作为Sent事件发布。交易消耗的燃气费用作为执行此交易的奖励发送给矿工。
与以太坊不同,Ergo中的交易是一个数据结构,包含一系列输入硬币和一系列输出硬币,保持ERGs和代币的总余额(在这方面Ergo与比特币相似)。
回到上面的例子,由于Ergo原生支持代币,因此在发送代币的这个特定示例中,我们不需要在ErgoScript中编写任何代码。相反,我们需要创建下图所示的“send”交易,它以声明式的方式描述了相同的代币转移。

该图形直观地描述了网络用户需要执行的以下步骤:
- 选择未花费的发送者箱,包含总计
tB >= amount的代币和B >= txFee + minErg的ERGs。 - 创建一个输出
target箱,该箱由receiver公钥保护,包含minErgERGs和amount的T代币。 - 创建一个由
minerFee合约保护的费用输出,包含txFeeERGs。 - 创建一个由
sender公钥保护的找零输出,包含B - minErg - txFeeERGs和tB - amount的T代币。 - 创建一个新交易,使用发送者的私钥对其进行签名并发送到Ergo网络。
在这里需要理解的重要一点是,所有这些步骤都是在链下执行的(例如使用Appkit交易API),由用户的应用程序完成。Ergo网络节点不需要重复此交易创建过程,他们只需验证已形成的交易。ErgoScript合约存储在交易的输入中并检查支出条件。节点在交易被验证时在链上执行合约。如果所有条件都满足,则交易有效。
因此,在以太坊中,当我们“从发送者发送金额到接收者”时,我们实际上是在编辑余额并使用一组具体的命令更新存储。这发生在链上,因此作为此过程结果的新交易也在链上创建。
在Ergo(如同比特币)中,交易是在链下创建的,网络节点只需验证它们。交易对区块链状态的影响是输入硬币(或在Ergo术语中称为箱)被移除,输出箱被添加到UTXO集合中。
在上面的例子中,我们没有使用ErgoScript合约,而是假设使用签名检查作为支出前置条件。然而,在更复杂的应用场景中,我们当然需要使用ErgoScript,这正是我们接下来要讨论的内容。
从更改状态到检查上下文
在send函数示例中,我们首先检查前置条件(require(amount <= balances[msg.sender],...)),然后更改状态(即更新余额balances[msg.sender] -= amount)。这在以太坊交易中是典型的。在我们更改任何内容之前,我们需要检查这样做是否有效。
在Ergo中,正如我们之前讨论的,当有效交易被包含在区块中时,状态(即UTXO箱的集合)是隐式更改的。因此,在交易可以添加到区块之前,我们只需检查前置条件。这就是ErgoScript合约所做的。
在ErgoScript中不可能“更改状态”,因为它是一种检查硬币支出前置条件的语言。ErgoScript是一种纯函数语言,没有副作用,操作在不可变数据值上。这意味着脚本中可用的所有输入、输出和其他交易参数都是不可变的。这使得ErgoScript成为一种非常简单的语言,易于学习和安全使用。与比特币类似,每个输入箱都包含一个脚本,该脚本应返回true值,以便1)允许支出该箱(即从UTXO集合中移除)和2)将交易添加到区块中。
如果我们要严格说法,因此将ErgoScript视为Ergo合约的语言是不正确的,因为它是保护箱免受“非法”支出的命题语言(逻辑谓词、公式等)。与比特币不同,在Ergo中,整个交易和当前区块链上下文的一部分对每个脚本都是可用的。因此,每个脚本可以检查交易创建的输出、它们的ERG和代币数量(我们将在我们的示例DEX合约中使用此功能)、当前区块号等。
在ErgoScript中,您定义了在给定上下文中是否允许发生更改(即硬币支出)的条件。这与在合约代码中命令式地编程更改形成对比。
虽然Ergo的交易模型解锁了一系列应用(如DEX、DeFi应用、LETS等),但直接将合约设计为硬币支出的前置条件(或保护脚本)并不直观。在接下来的部分中,我们将考虑一种有用的图形符号,以使用_FlowCard图_声明性地设计合约,这是一种可执行组件(FlowCards)的可视化表示。
FlowCards旨在通过提供高级声明性语言、执行运行时、存储格式和图形符号,彻底简化Ergo平台上的dApp开发。
我们将从高层次的图表开始,然后深入到FlowCard规范。
FlowCard图
FlowCard图的想法基于以下观察:1)Ergo箱是不可变的,只能在使用它作为输入的交易中支出。2)因此,我们可以通过交易绘制箱的流动,以便流入交易的箱被支出,而流出交易的箱被创建并添加到UTXO中。3)从这个角度来看,交易只是将旧箱转换为新箱的变换器,保持涉及的ERGs和代币的余额。
下图显示了我们之前已经看到的Ergo交易的主要元素(现在称为FlowCard图)。

每个_图表_元素背后都有严格定义的含义(语义),因此该图表是底层可执行组件(称为FlowCard)的可视化表示(或视图)。
FlowCard可以作为Ergo dApp的可重用组件,用于在Ergo区块链上创建和发起交易。我们将在接下来的部分中讨论这一点。
现在让我们逐一查看FlowCard图的各个部分。
1. 名称和参数
每个流卡都有一个名称和一个类型参数列表。这类似于带参数的模板。在上面的图中,我们可以看到Send流卡,它有五个参数。参数在规范中使用。
2. 合约钱包
这是流卡的一个关键元素。每个箱都有一个保护脚本。通常,它是检查签名与公钥的脚本。这个脚本在ErgoScript中是微不足道的,定义如下:def pk(pubkey: Address) = { pubkey },其中pubkey是类型为Address的参数。在图中,脚本模板应用于参数pk(sender),因此获得了一个具体的钱包合约。因此,pk(sender)和pk(receiver)产生不同的脚本,并在图中表示_不同_的钱包,尽管它们使用相同的模板。
_合约钱包_包含一组所有具有给定脚本的UTXO箱,这些脚本是通过使用流卡参数从给定脚本模板派生的。例如,在图中,模板是pk,参数pubkey被替换为sender流卡参数。
3. 合约
尽管合约是箱的属性,但在图中我们按合约对箱进行分组,因此看起来箱属于合约,而不是合约属于箱。在示例中,我们有三个实例化的合约pk(sender)、pk(receiver)和minerFee。注意,pk(sender)是用具体参数sender实例化的pk模板,而minerFee是保护矿工奖励箱的预定义合约的实例化。
4. 箱名称
在图中,我们可以给每个箱一个名称。除了提高图表的可读性外,我们还使用名称作为对合约中箱的更复杂索引访问的同义词。例如,change是箱的名称,也可以在ErgoScript条件中使用,而不是OUTPUTS(2)。我们还使用箱名称将支出条件与箱关联。
5. 钱包中的箱
在图中,我们将箱(较深的矩形)显示为属于合约钱包(较浅的矩形)。每个这样的_箱矩形_通过橙色或绿色箭头或两者连接到灰色_交易矩形_。输出箱(带有传入绿色箭头)可能包含多行文本,其中每行指定应作为交易的一部分检查的条件。第一行指定应放入箱中的ERG数量的条件。其他行可以采用以下形式之一:
amount: TOKEN- 箱应包含给定amount的给定TOKENR == value- 箱应包含给定寄存器R的给定valueboxName ? condition- 名为boxName的箱应在其脚本中检查condition。
我们将在下面的部分讨论这些条件。
6. 箱中的ERGs数量
每个箱应存储最低数量的ERGs。这在创建交易时进行验证。在图中,ERGs的数量_始终_显示为第一行(例如B: ERG或B - minErg - txFee)。值类型说明B: ERG是可选的,可以用于可读性。当值以公式给出时,则该公式应被交易创建箱时遵循。
重要的是要理解,像amount和txFee这样的变量不是箱的命名属性。它们是整个图表的参数,表示某些数量。换句话说,它们是交易之间的共享参数(例如,下面DEX示例中的卖出订单和交换交易共享tAmt参数)。因此,相同的名称在整个图表中与相同的值相关联(这就是工具将大有帮助的地方)。然而,当涉及到这些值的_链上_验证时,只有标记为?的显式条件会转换为ErgoScript。同时,所有其他条件在交易构建期间(例如在使用Appkit API的应用程序中)和在添加到区块链时进行交易验证时确保_链下_。
7. T代币的数量
一个箱可以存储多种代币的值。图中的代币是命名的,并且可以使用value: T表达式将value变量与代币T关联。value可以通过公式给出。如果公式以箱名称为前缀,如boxName ? formula,则它也应在boxName箱的保护脚本中进行检查。这种额外的规范非常方便,因为1)它允许自动验证可视化设计,2)图表中箱中指定的条件足以合成必要的保护脚本。(更多关于这一点将在**“从图表到ErgoScript合约”**中讨论)
8. 交易输入
输入通过橙色箭头连接到相应的交易。输入箭头可以有以下形式的标签:
name@index- 可选名称和索引,即fee@0或@2。这是箭头目标端点的属性。名称用于相关箱的条件,index是交易的INPUTS集合中相应箱的位置。!action- 是箭头源的属性,并为箱的替代支出路径提供名称(我们将在DEX示例中看到这一点)。
由于替代支出路径,一个箱可能有多个传出的橙色箭头,在这种情况下,它们应标记为不同的操作。
9. 交易
交易支出输入箱并创建输出箱。输入箱由橙色箭头给出,标签应将输入放置在INPUTS集合中的正确索引处。输出箱由绿色箭头给出。每个交易应保持ERG值的严格平衡(输入总和==输出总和),并且对于每个代币,输入总和>=输出总和。设计图要求对所有输出箱的ERG和代币值进行显式规范,以避免隐式错误并确保更好的可读性。
10. 交易输出
输出通过绿色箭头连接到相应的交易。输出箭头可以有以下形式的标签name@index,其中可选名称伴随一个索引,即fee@0或@2。这是箭头源端点的属性。名称用于相关箱的条件,index是交易的OUTPUTS集合中相应箱的位置。
示例:去中心化交易所(DEX)
现在让我们使用上述描述的符号为DEX dApp设计一个FlowCard。它简单明了,同时也展示了我们在前一部分中介绍的FlowCard图的所有关键特性。
dApp场景如下图所示:
DEX dApp有三个参与者(买方、卖方和DEX)和五种不同的交易类型,由参与者创建。买方希望用ergAmt的ERGs交换tAmt的TID代币(或反之,卖方希望将TID代币出售给ERGs,谁先发送订单并不重要)。买方和卖方都可以随时取消他们的订单。DEX链下匹配服务可以找到匹配的订单并创建Swap交易以完成交换。
下图完全(且正式)指定了DEX dApp必须在链下创建的五个交易。它还指定了所有应在链上验证的支出条件。

让我们详细讨论FlowCard图和每个交易的逻辑:
买入订单交易
买方创建一个Buy Order交易。该交易从pk(buyer)钱包中的一个或多个箱支出E数量的ERGs(我们将写作E: ERG)。该交易创建一个bid箱,包含ergAmt: ERG,由buyOrder脚本保护。buyOrder脚本是从规范中合成的(见下文**“从图表到ErgoScript合约”**),可以手动或自动由工具生成。尽管在设计过程中我们不需要显式定义buyOrder脚本,但在运行时,bid箱应包含buyOrder脚本作为保护命题(检查箱支出条件),否则图中指定的条件将不会被检查。
创建change箱是为了使交易的输入和输出总和平衡。交易费用箱被省略,因为工具可以自动添加它。然而,在实践中,设计者可以显式地将费用箱添加到图表中。这涵盖了更复杂交易(如Swap)的情况,其中有多种支付交易费用的方式。
取消买入、取消卖出交易
在任何时候,buyer都可以通过发送CancelBuy交易来取消订单。该交易应满足保护bid箱的buyOrder合约。如您在图中所见,Cancel和Swap交易都可以支出bid箱。当一个箱有支出替代方案(或_支出路径_)时,每个替代方案由一个以!为前缀的唯一名称标识(!cancel和!swap用于bid箱)。每个替代路径都有特定的支出条件。在我们的示例中,当Cancel Buy交易支出bid箱时,?buyer条件应满足,我们可以理解为“交易中应提供buyer地址的签名”。因此,只有买方可以取消买入订单。这个“签名”条件仅在!cancel替代支出路径中是必需的,而在!swap中则不需要。
卖出订单交易
Sell Order交易与BuyOrder类似,因为它除了ERGs外还涉及代币。该交易支出E: ERG和T: TID代币(指定为pk(seller)合约)。两个输出是ask和change。找零是一个标准箱,用于平衡交易。ask箱保留tAmt: TID代币用于交换和minErg: ERG - 每个箱中所需的最低ERGs数量。
交换交易
这是DEX dApp场景中的关键交易。该交易对输入箱有多个支出条件,这些条件包含在buyOrder和sellOrder脚本中(在交易添加到区块链时进行验证)。然而,在图中,这些条件并未在bid和ask箱中指定,而是定义在交易的输出箱中。
这是为了提高可用性的约定,因为大多数条件与输出箱的属性相关。我们可以在bid箱中指定这些属性,但那样我们就必须使用更复杂的表达式。
让我们考虑由标记为buyerOut@0的箭头创建的输出。该标签告诉我们,输出在交易的OUTPUTS集合中的索引为0,并且在图中我们可以通过buyerOut名称引用此箱。因此,我们可以标记箱本身和箭头,以给箱一个名称。
在buyerOut箱中显示的条件形式为bid ? condition,这意味着它们应在链上进行验证,以支出bid箱。
这些条件的含义如下:
tAmt: TID要求箱中有tAmt数量的TID代币R4 == bid.id要求箱中的R4寄存器等于bid箱的id。script == buyer要求buyerOut箱具有图中所在钱包的脚本,即pk(buyer)。
类似的属性被添加到sellerOut箱,该箱被指定为索引1,并使用箱本身上的标签而不是箭头给它命名。
Swap交易支出两个箱bid和ask,在两个箱上使用!swap支出路径,然而与!cancel不同,路径上的条件未被指定。这就是bid ?和ask ?前缀发挥作用的地方。它们用于将buyerOut和sellerOut箱中列出的条件移动到bid和ask箱的!swap支出路径中。
如果您查看输出箱的条件,您会发现它们准确地指定了卖方和买方钱包之间的值交换。买方获得所需数量的TID代币,卖方获得相应数量的ERGs。当存在两个匹配的箱与buyOrder和sellOrder合约时,Swap交易被创建。
从图表到ErgoScript合约
FlowCard规范的有趣之处在于,我们可以使用它们自动生成所需的ErgoTree脚本。借助适当的工具支持,这可以自动完成,但如果缺乏工具,也可以手动完成。因此,FlowCard允许我们捕捉并可视化表示Ergo dApp的所有设计选择和语义细节。
接下来,我们将机械地从DEX流卡中的信息创建buyOrder合约。
回想一下,每个脚本都是一个命题(布尔值表达式),应评估为true以允许支出箱。当我们有许多条件需要同时满足时,我们可以使用AND二元操作将它们组合在一起,如果我们有替代方案(不一定是排他的),我们可以将它们放入OR操作中。
buyOrder箱有替代支出路径!cancel和!swap。因此,ErgoScript代码应具有OR操作,两个参数——每个支出路径一个。
/** buyOrder合约 */
{
val cancelCondition = {}
val swapCondition = {}
cancelCondition || swapCondition
}
cancelCondition表达式的公式在buyOrder箱的!cancel支出路径中给出。我们可以直接将其包含在脚本中。
/** buyOrder合约 */
{
val cancelCondition = { buyer }
val swapCondition = {}
cancelCondition || swapCondition
}
对于buyOrder箱的!swap支出路径,条件在Swap交易的buyerOut输出箱中指定。如果我们简单地将它们包含在swapCondition中,那么我们将得到一个语法不正确的脚本。
/** buyOrder合约 */
{
val cancelCondition = { buyer }
val swapCondition = {
tAmt: TID &&
R4 == bid.id &&
@contract
}
cancelCondition || swapCondition
}
然而,我们可以使用以下简单规则将图表语法中的条件转换为ErgoScript表达式:
buyerOut@0==>val buyerOut = OUTPUTS(0)tAmt: TID==>tid._2 == tAmt,其中tid = buyerOut.tokens(TID)R4 == bid.id==>R4 == SELF.id,其中R4 = buyerOut.R4[Coll[Byte]].getscript == buyer==>buyerOut.propositionBytes == buyer.propBytes
注意,在图中,TID表示代币id,但ErgoScript无法通过id访问代币,因此我们无法写tokens.getByKey(TID)。因此,当图表被转换为ErgoScript时,TID成为箱的tokens集合中的索引的命名常量。常量的具体值在创建带有buyOrder箱的BuyOrder交易时分配。实际的tokenId、TID常量和buyerOut箱的实际代币之间的对应关系和一致性由_链下_应用程序代码确保,这完全是可能的,因为所有交易都是由应用程序使用FlowCard作为指导规范创建的。这听起来可能太复杂,但这是从图表规范到实际可执行应用程序代码的转换过程,其中大部分可以自动化。
经过转换后,我们可以获得一个正确的脚本,检查支出buyOrder箱所需的所有前置条件。
/** buyOrder合约 */
def DEX(buyer: Addrss, seller: Address, TID: Int, ergAmt: Long, tAmt: Long)
{
val cancelCondition: SigmaProp = { buyer } // 验证买方的签名(ProveDlog)
val swapCondition = OUTPUTS.size > 0 && { // 确保OUTPUTS访问
val buyerOut = OUTPUTS(0) // 来自buyerOut@0
buyerOut.tokens.size > TID && { // 确保代币访问
val tid = buyerOut.tokens(TID)
val regR4 = buyerOut.R4[Coll[Byte]]
regR4.isDefined && { // 确保R4访问
val R4 = regR4.get
tid._2 == tAmt && // 来自tAmt: TID
R4 == SELF.id && // 来自R4 == bid.id
buyerOut.propositionBytes == buyer.propBytes // 来自script == buyer
}
}
}
cancelCondition || swapCondition
}
类似的脚本可以使用相同的转换规则为sellOrder箱获得。借助工具,合约的代码可以从图表规范中机械生成。
结论
声明式编程模型在许多应用领域(如大数据、流处理、深度学习、数据库等)中已经赢得了与命令式编程的斗争。Ergo正在开创dApp开发的声明式模型,作为当前流行的命令式智能合约模型的更好和更安全的替代方案。
FlowCard的概念将重点从编写ErgoScript合约转移到值的整体流动(因此得名),以这样的方式,使得ErgoScript始终可以从中生成。一旦工具到位,您将不再需要查看ErgoScript代码。
以下是未来工作的可能下一步:
-
FlowCard规范的存储格式及相应的EIP标准化文件格式(Json/XML/Protobuf)。这将允许各种工具(图表编辑器、运行时、dApps等)创建和使用
*.flowcard文件。 -
FlowCard查看器,可以从
*.flowcard文件生成图表。 -
FlowCard运行时,可以运行
*.flowcard文件,创建并发送交易到Ergo网络。 -
FlowCard设计工具,可以简化复杂图表的开发。这将使Ergo合约的设计和验证变得愉快,更像是绘图而不是编码。此外,整个dApp场景的正确性可以通过工具进行验证和控制。
参考文献
Share post




















