链接 Algorand 无状态和有状态智能合约
文章速览
Algorand 有两种第 1 层智能合约类型
- 无状态智能合约用于批准支出或资产转移交易,常管理着特定账户的所有交易。
- 有状态智能合约实现应用逻辑,可存储和更新链值。
如果您的去中心化应用将应用逻辑和用户状态与资金或资产的处理捆绑在一起,那就两种合约类型都采用。例子包括
- 在批准托管付款前需要全局值时。例如,在区块链上存储位置的追踪应用。
- 需指定日期或特定轮次以限制交易次数时,例如投票应用。
- 需要从账本抽取资产或余额信息以批准交易时,例如需要特定通证参与的贸易应用。
- 需全部或部分填写订单时,例如数字资产交易所。
- 等等……
实现步骤
- 创建并部署一个有状态智能合约,其中存储无状态合约地址的全局变量占位符。
- 向需要同时提交两笔交易(原子交易)的无状态智能合约添加逻辑,并验证第一笔是对上一步中部署的有状态智能合约的应用调用。
- 编译并检索无状态合约的地址。
- 更新有状态智能合约,将无状态合约的地址存储为全局变量。
Algorand 目前有两类第 1 层智能合约:有状态的和无状态的。在 Algorand 区块链上构建应用时,这两种合约执行的功能迥然不同。尽管每个功能独立执行让开发人员可以构建出一些很棒的应用,但要解决某类问题常常需要这两类合约协同工作。如果您想要存储关于用户的某些持久数据或某些全局数据,同时还要执行某种类型的支出交易,通常就会发生此类问题。以众筹应用为例,用户向基金捐款(支出交易)时,您也应递增基金总额。您可能还想要存储每个人捐赠了多少,以备日后需要将资金退还给用户。本文将描述在应用中连接这两类智能合约的基础概念,帮助实现上述功能。
开始组合这两类合约之前,我们先分别简要概述一下每种合约,如果您已经很熟悉 Algorand 合约类型,可以直接跳转至连接合约示例。
无状态智能合约
无状态智能合约在开发文档中已有详尽介绍。此类合约的主要功能是通过逻辑批准支出交易。可以使用用户签名、多重签名或无状态智能合约的逻辑签名 Algorand 交易。
所需签名的类型取决于交易的“from”属性。普通 Algorand 账户的“from”属性是个简单的地址。这种情况下需要普通签名。如果“from”属性是多重签名账户,交易就需要由组成此多重签名的一个或多个 Algorand 账户签名。
无状态智能合约提供另外两种用逻辑签名交易的方式。第一种方式常被称为托管或合约账户,无状态智能合约就采用这种方式编写和编译。编译出的合约产生一个 Algorand 地址。可通过“to”属性设为托管地址的简单交易,向此地址转入 Algo 币或资产。一旦资金注入,任何人都可以提交“from”属性设置为托管地址的交易,并签名无状态智能合约。此交易会令 Algorand 区块链开始执行托管账户中的逻辑,根据逻辑的执行结果批准或拒绝交易。
使用无状态智能合约签名交易的第二种方式,是以委托方法使用无状态智能合约。简单讲就是编写无状态智能合约,个人账户或多重签名账户签名逻辑。日后就可以使用这个经签名的逻辑代表此账户或多重签名账户提交交易,从原始签名账户取得 Algo 币或资产了。一旦交易提交,即开始检查逻辑。例如,您可以签名逻辑,申明您的抵押贷款公司可以每月从您账户拿走 X 个 Algo 币。然后抵押贷款公司就可以每月提交一笔交易,交易的“from”属性设为您的账户,并用之前经过签名的逻辑签名此交易。这个经签名的逻辑称为 Algorand LogicSig,可在应用的内存中驻留。明智的做法是设置智能合约代码可以使用 LogicSig 的最大轮次,使得 LogicSig 到期失效。也应该执行许多其他检查,包括交易费用、接收方的地址、验证未发生 Rekeying 等等。若需深入了解智能合约逻辑中的关键检查,可参阅开发文档。
有状态智能合约
有状态智能合约主要作为状态持有合约使用。此类合约在链上全局存储合约或选择加入智能合约的个人账户的值。从这个意义上讲,这些合约存活在区块链上,可由任何人发起。个人通过有状态智能合约交易调用这些合约。这些交易启动有状态智能合约中存储的逻辑。合约处理逻辑,根据传给合约的值和合约中的逻辑,要么成功,要么失败。以众筹为例,合约筹集到的总额可能全局存储,而个人账户的捐款可能(本地)存储在用户的账本余额中。如果逻辑通过,无论本地还是全局,逻辑更改的所有状态将在账本中最终确定。如果逻辑未通过,账本不会记录任何状态更改。举个例子,假设您的众筹应用设置了最小捐款额。逻辑可能检查账户试图捐赠的数额。如果数额过低,逻辑将拒绝此有状态智能合约交易。如果数额高于此最小值,逻辑就会通过此交易并确认状态更改。
连接有状态智能合约与付款交易
有状态智能合约本身并不批准支出交易,仅批准有状态智能合约交易,但可以连接支出交易,从而有效批准或拒绝支出交易。采用 Algorand 的原子交易功能就能做到这一点。该功能允许同时提交多达 16 笔交易,且这些交易必须全部成功或失败。只要其中任何一笔交易失败,所有交易全部失败。这一强大功能使有状态智能合约能够有效拒绝支出交易。事实上,使用原子功能,无论有状态智能合约还是无状态智能合约,都能查询组内任意交易的所有交易属性。以众筹为例,假定设置了一个接收所有捐款的地址。若要捐款,用户只需向此地址发送付款交易即可。如果配合有状态智能合约使用,该合约可以查询付款交易值,检查支出交易的数额,验证该数额高于最小捐款额。如果逻辑失败,由于原子交易的关系,付款交易也将失败。
实现此类检查的 TEAL 代码可能类似下面这段代码。
gtxn 1 Amount
int 10000
>=
return
连接有状态智能合约与无状态智能合约
如上节所示,连接有状态智能合约与付款交易能获得很好的效果。采用原子交易,很多不同类型的交易都能组合到一起,包括有状态智能合约和无状态智能合约。例如,假设在众筹案例中,上个例子中的基金账户是个托管无状态智能合约。筹款期结束之前,捐款都保管在这个托管账户中。基金结束日期存储在有状态合约的全局状态中。这意味着您不希望资金在基金结束日期过去前流出此托管。理想情况下您还应执行其他检查,但出于简化考虑,我们不妨假设只有这一项检查。托管无权访问存储在有状态智能合约全局状态中的结束日期。这一点可以通过向无状态托管添加逻辑加以规避:逻辑验证有状态智能合约在有人试图将资金转移出托管时被调用。然后有状态智能合约就可以检查这个日期了。
为此,有状态智能合约需知道托管无状态合约,而托管需知道有状态智能合约。可以通过多种方式实现这一点。部署有状态智能合约时会生成该合约的唯一 id。可以采用类似下面的逻辑在托管无状态合约中检查这个应用 id。
// Portion of escrow stateless contract logic
// This contract only spends out
// if two transactions are grouped
global GroupSize
int 2
==
// The first transaction must be
// an ApplicationCall (ie call stateful smart contract)
gtxn 0 TypeEnum
int appl
==
&&
// The specific App ID must be called
// This should be changed after creation
gtxn 0 ApplicationID
int 12345 //App id of the stateful smart contract
==
&&
// The second transaction must be a payment transaction
gtxn 1 TypeEnum
int pay
==
&&
从上面的代码可以看出,除非托管的任一交易与有状态智能合约调用组合,且应用 id 必须是我们预期的那个有状态智能合约,否则逻辑将会失败。此外,第二个交易还必须是个付款交易。也就是说,必须在托管代码完成前部署有状态智能合约。
要将托管连接至有状态智能合约,有状态合约代码中需加入类似的代码。正如前面探讨的,无状态托管账户可在编译合约后由其地址标识。此地址可在有状态智能合约中加以检查。由于直到有状态智能合约部署之后才能知道托管合约地址,这就引发了一个问题。规避此问题的一个办法是编译托管并将地址传递给有状态智能合约,以存储在全局状态中。然后有状态智能合约中的逻辑就可以检查这个值了,如下图所示。
// This stateful contract will only approve stateful transaction
// if two transactions are grouped
global GroupSize
int 2
==
// The first transaction must be
// an ApplicationCall (ie call stateful smart contract)
gtxn 0 TypeEnum
int appl
==
&&
// The second transaction must be a payment transaction
gtxn 1 TypeEnum
int pay
==
&&
// verify the sender
// of the payment transaction
// is the escrow account
gtxn 1 Sender
byte "Escrow"
app_global_get
==
&&
.
.
// check that we are past fund close date
// assume the fund close date was stored earlier as well
global LatestTimestamp
byte "FundCloseDate"
app_global_get
>=
&&
要将托管地址存入有状态智能合约的全局状态中,您可以向有状态智能合约添加逻辑以存储托管地址。事实上,您只需更新有状态智能合约交易即可。托管会作为参数传递给交易。
// check if this is update ---
int UpdateApplication
txn OnCompletion
==
bz not_update
// the call should pass the escrow
// address
txn NumAppArgs
int 1
==
bz failed
// store the address in global state
// this parameter should be addr:
byte "Escrow"
txna ApplicationArgs 0
app_global_put
int 1
return
这么做可以有效连接两种合约,交易提交时只要其中之一不存在,另一个即会失败。