智能合约的合适类型是什么?

我想知道用类型化语言(如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
}

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

泛化:全局状态?

那么,让我们退一步,把保守解重写成这样:

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

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

  1. 偷看对方合同的状态看起来很“hack "实现通信的方式;

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

现在该做什么?

现在我被蒙在鼓里。我知道我对这个问题没有正确的抽象,但我不确定它会是什么。看起来问题的根源在于我不能正确地捕捉交叉契约通信的现象。哪种具体类型更适合定义任意智能合约?

4536 次浏览

两个建议的解决方案:

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

该映射用于跟踪相同区块链上其他契约的状态。这个解决方案有两个优点:它非常简单,不需要任何特殊的语言功能,它可以与不知道这个解决方案的现有合同进行交互。或者是:

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

无论哪种方法,都使用不知道区块链的货币的类型。这种类型捕获一个契约可以执行的所有可能的操作,以及它如何改变自己的状态以及相同区块链上其他契约的状态。它还捕获用户提交操作时发生的情况。最后一部分对于实现智能合约之间的通信协议至关重要。例如,如果我们想实现一个Exchange合约,那么我们需要知道当一个交易所提交一个订单,而另一个交易所接受或拒绝它时会发生什么。这些信息需要从第二个合同传达给第一个合同,以便在提交/接受/拒绝事件发生后,双方都同意各自的状态。因此,这种类型将捕获关于交叉合约通信的所有必要信息,此外还能够定义任何任意的智能合约逻辑,而无需硬编码任何关于以太坊货币的具体信息(甚至不知道是否有一些货币)。

在回答主要问题之前,我将尝试更精确地定义用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是一个有状态的、命令式的平台,所以单子是一个合适的选择。

我们将把这个基础单元命名为Eth(尽管它并不是严格地特定于以太坊),并为它配备一组适当的原语,以安全和实用的方式利用底层VM的全部功能。

我们将在稍后讨论需要哪些基本操作,但现在,有两种方法来定义这个单子:

  • 作为具有一组操作的内置基元数据类型

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

    type Eth = IO
    

    在编译器和运行时的适当支持下,这将允许现有的基于__abc0的功能(例如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上执行代码-在Eth单子中返回一个动作
  • 呼叫其他合同。稍后我们将定义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(其智能合约功能在撰写本文时处于后期测试阶段)完全是用Haskell变体Plutus编程的。

与其使用严格的基于库或后端方法来生成EVM字节码,还可以设计其他更用户友好的方法。路托斯, Cardano区块链语言,使用模板Haskell拼接将链上Haskell代码嵌入到普通Haskell中,后者在链下执行。然后这些代码由GHC插件处理。

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

进一步的阅读

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

其他有趣的资源:

  • < p > 通过类型驱动开发更安全的智能合约

    一篇论文描述了在Idris中指定智能合约的更完整的方案,利用依赖编程特性来强制执行重要的不变量

  • < p > 马洛:区块链上的金融合同

    一篇描述使用函数式语言指定区块链智能合约的论文,它是Cardano Plutus技术的先驱

  • < p > fp-ethereum

    为那些对函数式编程和以太坊智能合约感兴趣的人提供了一个引用列表和gitter社区。

Kindelia中,契约只是一个函数,它返回一个IO,在网络上执行侧向有效的操作,包括保存/加载持久状态,获取调用方名称和块高度,以及调用其他IOs。因此,只需通过输入调用契约就可以调用它们,然后它们就可以像正常的程序一样做任何需要的事情。5年后,我不认为一定会有不同的或花哨的方式来对待合同。下面是一个“反击”;例子:

// Creates a Counter function with 2 actions:
ctr {Inc} // action that increments the counter
ctr {Get} // action that returns the counter
fun (Counter action) {


// increments the counter
(Counter {Inc}) =
!take x        // loads the state and assigns it to 'x'
!save (+ x #1) // overwrites the state as 'x + 1'
!done #0       // returns 0


// returns the counter
(Counter {Get}) =
!load x // loads the state
!done x // returns it


// initial state is 0
} with {
#0
}

它的类型,如果写入,将是Counter : CounterAction -> IO<U128>。注意,契约必须具有依赖类型,因为它们返回的类型可能取决于调用者所采取的操作的值。

当然,不同的网络可能有或多或少的限制,但它们至少必须满足两个基本需求:持久状态和与其他契约通信。