201

我想知道用 Haskell 或 Idris 等类型语言表达智能合约的最佳方式是什么(例如,你可以编译它以在以太坊网络上运行)。我主要关心的是:捕获合约可以做的所有事情的类型是什么?

天真的解决方案:EthIO

一个天真的解决方案是将合同定义为EthIO类型的成员。这种类型类似于 Haskell 的IO,但不是启用系统调用,而是包括区块链调用,即,它可以读取和写入区块链的状态,调用其他合约,获取块数据等。

-- incrementer.contract

main: EthIO
main = do
   x <- SREAD 0x123456789ABCDEF
   SSTORE (x + 1) 0x123456789ABCDEF

这显然足以执行任何合同,但是:

  1. 会太强大。

  2. 特别是与以太坊区块链非常耦合。

保守的解决方案:事件溯源模式

在这个想法下,合约将被定义为折叠一系列动作:

type Contract action state = {
    act  : UserID -> action -> state -> state,
    init : state
}

所以,一个程序看起来像:

incrementer.contract

main : Contract
main = {
    act _ _ state = state + 1,
    init          = 0
}

也就是说,您定义初始状态、操作类型以及当用户提交操作时该状态如何更改。这将允许人们定义任何不涉及发送/接收资金的任意合同。大多数区块链都有某种货币,最有用的合约以某种方式涉及货币,所以这种类型的限制太强了。

不太保守的解决方案:事件+货币

我们可以通过将货币逻辑硬编码到上面的类型中来使上面的类型知道货币。因此,我们会得到类似的东西:

type Contract action state = {
    act        : UserID -> action -> state -> state,
    init       : state,
    deposit    : UserID -> Amount -> state -> state,
    withdrawal : UserID -> Amount -> state -> Maybe state
}

即,合约开发者需要明确定义如何处理货币存款和取款。这种类型足以定义任何可以与主机区块链货币交互的自包含合约。遗憾的是,这样的合约无法与其他合约交互。在实践中,合同经常相互影响。例如,交易所需要与其交换的 Token 合约进行通信以查询余额等。

概括:全局状态?

因此,让我们退后一步,将保守的解决方案重写为:

type Contract = {
    act  : UserID -> Action -> Map ContractID State -> State,
    init : State
}

根据这个定义,该act函数不仅可以访问合约自身的状态,还可以访问同一区块链上所有其他合约的状态。由于每个合约都可以读取彼此的状态,因此可以轻松地在此之上实现通信协议,因此,这种类型足以实现任意交互的合约。此外,如果区块链的货币本身是作为合同实现的(可能使用包装器),那么该类型也足以处理货币,尽管它没有在类型上进行硬编码。但该解决方案有两个问题:

  1. 查看另一个合约的状态看起来像是一种非常“hacky”的通信方式;

  2. 以这种方式定义的合约将无法与不了解该解决方案的现有合约进行交互。

现在怎么办?

现在我在黑暗中。我知道我对这个问题没有正确的抽象,但我不确定它会是什么。看起来问题的根源在于我无法正确捕捉跨合约通信的现象。哪种具体类型更适合定义任意智能合约?

4

2 回答 2

22

在回答主要问题之前,我将尝试更准确地定义在 Haskell 或 Idris 中编写代码并编译它以在类似以太坊的区块链上运行的含义。Idris 可能更适合于此,但我将使用 Haskell,因为这是我熟悉的。

编程模型

概括地说,我可以设想两种使用 Haskell 代码为区块链虚拟机生成字节码的方法:

  • 将EVM 字节码构建为 Haskell 数据的

  • 从 Haskell 代码生成字节码的GHC 后端

使用方法,将为每个 EVM 字节码声明构造函数,并在其上分层库代码以创建对程序员友好的构造。这可能会被构建成一元结构,从而为定义这些字节码提供类似编程的感觉。然后将提供一个函数来将此数据类型编译成适当的 EVM 字节码,以适当地部署到区块链。

这种方法的优点是不需要额外的基础设施——编写 Haskell 代码,用股票 GHC 编译它,然后运行它来生成字节码。

最大的缺点是,从库中重用现有的 Haskell 代码并不容易。所有代码都必须针对 EVM 库从头开始编写。

这就是GHC 后端变得相关的地方。一个编译器插件(目前它可能必须是一个 GHC 分支,就像 GHCJS 一样)会将 Haskell 编译成 EVM 字节码。这将对程序员隐藏单个操作码,因为它们确实太强大而无法直接使用,而是将它们降级为由编译器基于代码级构造发出。您可以将 EVM 视为不纯、不安全、有状态的平台,类似于 CPU,语言的工作就是将其抽象出来。相反,您可以使用常规的 Haskell 函数样式来编写它,并且在后端和您自定义编写的运行时的限制内,现有的 Haskell 库将编译并可用。

还有混合方法的可能性,我将在本文末尾讨论其中的一些方法。

在这篇文章的其余部分,我将使用 GHC 后端方法,我认为这是最有趣和最相关的。我确信核心思想会延续到图书馆方法中,也许会进行一些修改。

编程模式

然后,您需要决定如何针对 EVM 编写程序。当然,可以编写常规的纯代码并进行编译和计算,但也需要与区块链进行交互。EVM 是一个有状态的命令式平台,因此 monad 将是一个合适的选择。

我们将调用这个基础 monad Eth(尽管它不一定是以太坊特定的),并为其配备一组适当的原语,以安全和实用的方式利用底层 VM 的全部功能。

稍后我们将讨论需要哪些原始操作,但目前,有两种方法来定义这个 monad:

  • 作为具有一组操作的内置原始数据类型

    -- Not really a declaration but a compiler builtin
    -- data Eth = ...
    
  • 由于大部分 EVM 类似于一台普通计算机,主要是它的内存模型,一个更狡猾的方法是将其别名为IO

    type Eth = IO
    

    在编译器和运行时的适当支持下,这将允许现有IO的基于 的功能,例如IORefs,无需修改即可运行。当然,很多IO功能(例如文件系统交互)将不受支持,并且base必须在没有这些功能的情况下提供自定义包,以确保使用它们的代码不会编译。

原语

需要定义一些内置值以支持区块链编程:

-- | Information about arbitrary accounts
balance  :: Address -> Eth Wei
contract :: Address -> Eth (Maybe [Word8])
codeHash :: Address -> Eth Hash

-- | Manipulate memory; subsumed by 'IORef' if the 'IO' monad is used
newEthVar   :: a -> Eth (EthVar a)
readEthVar  :: EthVar a -> Eth a
writeEthVar :: EthVar -> a -> Eth ()

-- | Transfer Wei to a regular account
transfer :: Address -> Wei -> Eth ()

selfDestruct :: Eth ()

gasAvailable :: Eth Gas

其他基本功能,包括函数调用,包括决定调用是常规(内部)函数调用、消息调用还是委托消息调用,将由编译器和运行时处理。

智能合约的类型

我们现在要回答最初的问题:智能合约的合适类型是什么?

type Contract = ???

合同需要:

  • 在 EVM 上执行代码 - 在Ethmonad中返回一个动作
  • 调用其他合约。稍后我们将定义一个Eth操作来执行此操作。
  • 取值和返回值,类型inout
  • 访问有关其环境的信息,包括:
    • 当前交易中转移的金额
    • 当前帐户、发送帐户和发起帐户
    • 区块信息

因此,适当的类型可能是:

newtype Contract in out = Contract (Wei -> Env -> in -> Eth out)

Wei参数仅供参考;实际转账发生在合约被调用时,不能被合约修改。

Eth关于环境信息,决定什么应该作为参数传递以及什么应该作为原始操作提供,这有点像判断调用。

可以使用合约调用原语来调用合约:

call :: Contract in out -> Wei -> in -> Eth out

当然,这是一种简化;例如,它不会 curry 输入类型。据推测,编译器将为每个可见合约生成独特的操作,类似于 Solidity。使这个原语可用甚至可能不合适。

另一个细节:EVM 支持构造函数,即在创建合约时执行的 EVM 代码,以允许使用环境信息。因此,由程序员编写的合约类型将是:

main :: Eth (Contract in out)
main = return . Contract $ \wei env a -> do
  ...

结论

我省略了许多细节,例如错误处理、日志记录/事件、Solidity 互操作/FFI 和部署。尽管如此,我希望我已经对针对区块链智能合约环境的函数式语言的编程模型进行了有用的概述。

这些想法并不是严格意义上的以太坊特有的;但是,请注意以太坊使用基于账户的模型,而比特币和卡尔达诺都使用未使用的交易输出(UTxO)模型,因此许多细节会有所不同。比特币并没有真正可用的智能合约平台,而 Cardano(其智能合约功能在撰写本文时处于后期测试阶段)完全在 Plutus 中编程,Plutus 是 Haskell 的变体。

除了严格的基于库或后端的方法来生成 EVM 字节码,还可以设计其他更用户友好的方法。卡尔达诺区块链语言Plutus使用模板 Haskell 拼接将链上 Haskell 代码嵌入到链下执行的普通 Haskell 中。然后,此代码由 GHC 插件处理。

另一个有趣的想法是使用 Conal Eliot 的编译类别来提取和编译区块链的 Haskell 代码。这也使用了编译器插件,但必要的插件已经存在。所有这些都是定义相关类别理论类型类的实例所必需的,并且您可以免费获得 -Haskell-to-arbitrary-backend 编译。

进一步阅读

在写这篇文章时,我大量参考了以下参考资料:

其他有趣的资源:

于 2021-09-09T15:35:29.783 回答
0

两个建议的解决方案:

type Contract action state = {
    act  : UserID -> action -> state -> (state, Map ContractID State)
}

该地图用于跟踪同一区块链上其他合约的状态。该解决方案有两个优点:非常简单,不需要任何特殊的语言特性,并且可以与不了解该解决方案的现有合约进行交互。要不就:

type Contract action state = {
    act  : UserID -> action -> state -> (state, List State)
}

无论哪种方式,都使用不知道区块链货币的类型。这种类型捕获了一个合约可以执行的所有可能的操作,以及它如何改变自己的状态以及同一区块链上其他合约的状态。它还捕获用户提交操作时发生的情况。最后一部分对于实现智能合约之间的通信协议至关重要。例如,如果我们想实现一个交易所合约,那么我们需要知道当一个交易所提交订单而另一个交易所接受或拒绝它时会发生什么。该信息需要从第二个合同传回给第一个合同,以便在此类提交/接受/拒绝事件发生后他们都同意各自的状态。所以,

于 2021-08-30T03:38:39.770 回答