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

感谢Robert Kornacki对草稿的润色。
介绍
ErgoScript是Ergo区块链使用的智能合约语言。虽然它采用了来自Scala/Kotlin的简洁语法,但由于ErgoScript在概念上与我们熟知和喜爱的传统语言有很大不同,因此一开始可能会让人感到困惑。这是因为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是交易的输入集合中的相应盒子的位置。!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外还涉及代币。该交易从卖方钱包(指定为pk(seller)合约)支出E: ERG和T: TID代币。两个输出是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




















