艾达币(Cardano)合约开发教程
前言
本文旨在提供一个关于在Cardano区块链上进行智能合约开发的详细且专业的教程。我们将深入探讨核心概念,例如 Plutus Core, Extended UTXO (EUTXO) 模型以及脚本验证,并提供一些实用的、可执行的例子,旨在帮助初学者和有经验的开发者快速入门。为了顺利进行后续的学习,请确保你已经正确安装并配置了所有必要的开发工具,包括 Plutus Application Framework (PAF)、必要的 Cardano 节点(例如`cardano-node` 和 `cardano-cli`),以及 Haskell 工具链 (GHC, Cabal)。 特别强调,正确配置 Cardano 节点,使其与测试网络(如 Preprod 或 Preview)同步,是成功部署和测试智能合约的关键步骤。
环境准备
在深入 Plutus 智能合约开发之前,务必确保您的开发环境已妥善配置。以下是详细的环境搭建步骤:
- 安装 Cardano 节点 : 这是与 Cardano 区块链进行交互的基础。您需要从 Cardano 官方网站下载最新版本的 Cardano 节点软件。下载完成后,按照官方指南完成安装和配置,并启动节点。请耐心等待节点同步完成,同步过程可能需要数小时甚至数天,具体取决于您的网络连接速度和区块链的大小。节点同步完成后,您才能与 Cardano 网络进行交互,查询链上数据、提交交易等。强烈建议使用SSD硬盘以加速同步过程。
- 安装 Plutus Application Framework (PAF) : PAF 是一个强大的 Plutus 智能合约开发和测试框架,它提供了一系列工具和库,简化了智能合约的开发流程。请参考 Cardano 官方文档或 Plutus 开发文档,下载并安装 PAF。安装过程中,请确保您的系统满足 PAF 的依赖要求,并按照说明进行配置。PAF 包括 Plutus Tx 和 PAB(Plutus Application Backend)等核心组件。Plutus Tx负责将Haskell代码编译为链上可执行的脚本,而PAB则负责处理链下交互,例如钱包交互、API调用等。
- 安装 GHC 和 Cabal : Plutus 智能合约使用 Haskell 编程语言编写,因此您需要安装 GHC (Glasgow Haskell Compiler),它是 Haskell 的标准编译器。同时,您还需要安装 Cabal,它是 Haskell 的包管理和构建工具。您可以从 Haskell 官方网站下载 GHC 和 Cabal 的最新版本。安装完成后,请确保 GHC 和 Cabal 的可执行文件已添加到您的系统环境变量中,以便您可以在命令行中直接使用它们。建议使用 GHCup 进行安装,方便管理不同版本的GHC。
- 配置 Nix 环境 (可选但强烈推荐) : Nix 是一个强大的包管理器和系统配置工具,它可以帮助您创建一个隔离的、可重现的开发环境。使用 Nix,您可以确保您的开发环境始终保持一致,避免因不同版本的依赖库而导致的问题。如果您选择使用 Nix,请按照 Nix 官方文档进行安装和配置。安装完成后,您可以使用 Nix 创建一个包含所有 Plutus 依赖项的环境,从而避免与系统中其他项目的依赖冲突。Nix 环境可以确保项目在不同机器上的可移植性和可重复性。 通过 `nix-shell` 命令,您可以方便地进入和退出 Nix 环境。
Plutus 核心概念
Plutus 平台采用 Haskell 作为智能合约的主要编程语言。Plutus 智能合约的设计围绕着链上和链下组件的协同工作,实现安全且高效的去中心化应用。
- 链上代码 (On-Chain Code) : 链上代码是指在 Cardano 区块链上直接执行的智能合约逻辑。其核心职责是验证交易,确保交易符合合约预设的规则和条件。验证通过后,交易才能被确认并记录到区块链上。链上代码必须高度安全和高效,以避免潜在的安全漏洞和性能问题。
- 链下代码 (Off-Chain Code) : 链下代码则在区块链之外运行,负责构建交易和与用户进行交互。链下代码允许更复杂的计算和数据处理,而无需消耗昂贵的链上资源。典型的链下操作包括构建交易、签名交易、以及将交易提交到区块链。链下代码可以方便地集成各种外部数据源和服务。
Plutus 采用基于 UTXO (Unspent Transaction Output) 模型的扩展智能合约模型,该模型提供了一种清晰的状态管理方式。合约状态存储在 UTXO 中,通过交易来更新状态,保证了合约状态的可追溯性和安全性。要深入理解 Plutus 合约,需要熟悉以下关键概念:
- UTXO (Unspent Transaction Output) : UTXO 代表未花费的交易输出,是 Cardano 区块链的最小基本单位。 每个 UTXO 都包含一定数量的加密货币,以及可能包含的合约状态数据 (Datum)。合约的状态信息持久化存储在 UTXO 中。当合约需要更新状态时,就会创建一个新的交易,消耗现有的 UTXO,并创建包含更新状态的新 UTXO。
- Validator : Validator 是一种用 Haskell 编写的函数,其作用是验证交易是否满足合约的执行条件。Validator 函数接收交易输入、交易输出和脚本上下文 (Script Context) 作为参数。通过分析这些参数,Validator 能够判断交易是否被授权执行,以及交易是否符合合约逻辑。Validator 的结果决定了 UTXO 是否可以被解锁和花费。
- Datum : Datum 是存储在 UTXO 中的任意数据,代表合约的状态或者任何需要存储在链上的信息。Datum 可以是简单的数值,也可以是复杂的数据结构。Datum 的值在 UTXO 创建时被写入,在 UTXO 被花费时被读取。通过 Datum,合约可以跟踪其状态,并根据状态的变化执行不同的操作。
- Redeemer : Redeemer 是解锁 UTXO 的输入参数,提供验证器函数所需的信息,用于判断交易是否有效。Redeemer 可以包含各种数据,例如签名、参数、或者其他证明交易有效性的信息。Validator 根据 Redeemer 的值来执行相应的验证逻辑。不同的 Redeemer 值可以触发合约的不同行为。
- Script Context : Script Context 包含当前交易的详细信息,例如输入、输出、签名、以及其他相关的交易元数据。Validator 函数可以利用 Script Context 来验证交易的各个方面,例如验证签名是否有效,验证输入和输出是否符合预期,以及验证交易是否满足时间锁定的条件。Script Context 提供了 Validator 所需的上下文信息,以做出正确的验证决策。
编写 Plutus 合约
我们将创建一个简单的“Hello World”合约,该合约允许用户锁定 Ada,并在满足特定条件后解锁。这个合约展示了 Plutus 合约的基本结构,包括数据类型定义、验证器逻辑以及链上和链下交互。
- 定义 Datum, Redeemer 和 Validator 参数类型
在 Plutus 合约中,Datum、Redeemer 和 Script Context 是三个核心要素。 Datum 存储在合约地址上的状态信息,Redeemer 用于解锁或操作合约的状态,而 Script Context 提供了关于当前交易的上下文信息。正确定义这些类型是编写健壮合约的关键。
HelloDatum
包含了一个字符串
helloMessage
,作为合约锁定的信息。
HelloRedeemer
包含
unlockMessage
,用于验证解锁交易。使用
makeLift
和
unstableMakeIsData
函数可以方便地将 Haskell 数据类型转换为 Plutus Core 可以理解的格式。
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
module Hello where
import PlutusTx
import PlutusTx.Prelude hiding (String)
import Ledger
import Ledger.Constraints as Constraints
import qualified Ledger.Typed.Scripts as Scripts
import qualified Plutus.V1.Ledger.Scripts as Plutus
import qualified Plutus.V1.Ledger.Value as Value
-- 定义 Datum 类型
data HelloDatum = HelloDatum
{ helloMessage :: BuiltinByteString
}
PlutusTx.makeLift ''HelloDatum
PlutusTx.unstableMakeIsData ''HelloDatum
-- 定义 Redeemer 类型
data HelloRedeemer = HelloRedeemer
{ unlockMessage :: BuiltinByteString
}
PlutusTx.makeLift ''HelloRedeemer
PlutusTx.unstableMakeIsData ''HelloRedeemer
- 编写 Validator 函数
Validator 函数是合约的核心逻辑。它决定了在什么条件下允许交易执行。该函数接收 Datum、Redeemer 和 Script Context 作为输入,并返回一个布尔值。如果返回
True
,则交易有效,否则无效。
mkValidator
函数检查 Redeemer 中的
unlockMessage
是否与 Datum 中的
helloMessage
匹配。如果匹配,则交易被认为是有效的,否则合约将拒绝该交易。使用
traceIfFalse
函数可以在 Plutus 模拟器中添加调试信息,方便开发者调试合约。
validator
使用
Plutus.mkValidatorScript
和
PlutusTx.compile
将 Haskell 代码编译成 Plutus Core,这是链上运行的实际代码。编译后的验证器脚本将被用于验证所有与该合约相关的交易。
-- 定义 Validator 函数
{-# INLINABLE mkValidator #-}
mkValidator :: HelloDatum -> HelloRedeemer -> ScriptContext -> Bool
mkValidator datum redeemer ctx = traceIfFalse "Incorrect unlock message" (unlockMessage redeemer == helloMessage datum)
validator :: Plutus.Validator
validator = Plutus.mkValidatorScript
$$(PlutusTx.compile [|| mkValidator ||])
- 创建 Typed Validator
Typed Validator 提供了一种更类型安全的方式来与 Plutus 合约交互。通过定义
ValidatorTypes
实例,可以将 Datum 和 Redeemer 类型与验证器脚本关联起来,从而在编译时捕获类型错误。
HelloType
是一个空类型,用于创建
ValidatorTypes
实例。
DatumType
和
RedeemerType
分别被设置为
HelloDatum
和
HelloRedeemer
。
typedValidator
使用
Scripts.mkTypedValidator
函数创建 Typed Validator。
wrap
函数用于将普通的验证器函数转换为 Typed Validator 可以使用的格式。
data HelloType
instance Scripts.ValidatorTypes HelloType where
type instance DatumType HelloType = HelloDatum
type instance RedeemerType HelloType = HelloRedeemer
typedValidator :: Scripts.TypedValidator HelloType
typedValidator = Scripts.mkTypedValidator @HelloType
$validator
$$(PlutusTx.compile [|| wrap ||])
where
wrap = Scripts.wrapValidator @HelloDatum @HelloRedeemer
- 链下代码 (Off-Chain Code)
链下代码负责与 Plutus 合约进行交互,构建并提交交易。 这部分代码运行在 Cardano 节点之外,通常使用 Cardano 客户端库和 Plutus Application Framework (PAF) 与区块链交互。链下代码需要处理交易的构建、签名和提交,以及监控合约状态等任务。
- 锁定 Ada :
lock
函数负责将 Ada 锁定到合约地址。它接收 Datum 和要锁定的 Value 作为输入。该函数首先获取 Typed Validator 的脚本地址,然后创建一个包含
mustPayToTheScript
约束的交易。
mustPayToTheScript
约束确保指定的 Value 被支付到合约地址,并将 Datum 存储在该地址上。使用
submitTxConstraints
函数提交交易。
-- 锁定 Ada
lock :: HelloDatum -> Value.Value -> IO ()
lock datum value = do
let script = Scripts.validatorScript typedValidator
tx = Constraints.mustPayToTheScript datum value
ledgerTx <- submitTxConstraints typedValidator tx
print $ "submitted lock transaction " ++ show ledgerTx
- 解锁 Ada :
unlock
函数负责从合约地址解锁 Ada。它接收 Datum、Redeemer、TxOutRef 和 TxOutTx 作为输入。TxOutRef 指向合约地址上的 UTXO (Unspent Transaction Output),TxOutTx 包含关于该 UTXO 的详细信息。
unlock
函数创建一个包含
mustSpendScriptOutput
约束的交易。
mustSpendScriptOutput
约束确保指定的 UTXO 被花费,并且使用正确的 Datum 和 Redeemer。该函数还添加了
mustBeSignedBy
和
mustPayToPubKey
约束,以确保交易由正确的用户签名,并将解锁的 Ada 支付给正确的地址。使用
submitTxConstraintsWith
函数提交交易。
-- 解锁 Ada
unlock :: HelloDatum -> HelloRedeemer -> Ledger.TxOutRef -> Ledger.TxOutTx -> IO ()
unlock datum redeemer oref odata = do
let script = Scripts.validatorScript typedValidator
val = Ledger.txOutValue $ Ledger.txOutTxOut odata
lookups = Constraints.typedValidatorLookups typedValidator
Constraints.otherScript script
Constraints.unspentOutputs (Map.singleton oref odata)
tx = Constraints.mustSpendScriptOutput oref (Scripts.datumHash datum) redeemer
Constraints.mustBeSignedBy (Ledger.txOutAddress $ Ledger.txOutTxOut odata)
Constraints.mustPayToPubKey (Ledger.txOutAddress $ Ledger.txOutTxOut odata) val
ledgerTx <- submitTxConstraintsWith @HelloType lookups tx
print $ "submitted unlock transaction " ++ show ledgerTx
- 编译合约
使用
cabal build
或
stack build
命令编译合约。这些构建工具会自动处理依赖关系,并将 Haskell 代码编译成 Plutus Core 脚本。编译成功后,可以在链上部署和执行合约。
测试 Plutus 合约
在 Cardano 区块链上部署 Plutus 合约之前,进行全面的测试至关重要。 可以使用 Plutus Application Framework (PAF) 或直接利用 Cardano 节点提供的命令行工具,甚至结合二者进行测试。 选择合适的测试方法取决于测试的需求,例如,对合约逻辑的快速验证,或是模拟真实链上环境的完整集成测试。
- 使用 Plutus Playground 进行快速原型验证
Plutus Playground 是一款基于浏览器的交互式集成开发环境 (IDE),它提供了一个沙盒环境,专门用于测试和实验 Plutus 合约。 无需设置本地开发环境,用户即可在线编写、编译和模拟执行 Plutus Core 代码。 该环境支持模拟交易,允许开发者检查合约的执行流程、验证链上逻辑,并评估合约在不同输入条件下的行为。 Plutus Playground 非常适合快速原型设计、初步测试和学习 Plutus 编程。 但请注意,Playground 模拟的是一个简化版的 Cardano 环境,某些复杂场景可能无法完全模拟。
- 使用 PAF (Plutus Application Framework) 进行本地测试与集成
PAF (Plutus Application Framework) 提供了一个更高级的本地测试环境,允许开发者模拟 Cardano 区块链的完整功能。 开发者可以使用 PAF 启动一个本地的 Cardano 节点,并部署和执行 Plutus 合约。 与 Plutus Playground 相比,PAF 提供了更大的灵活性和控制权,允许开发者模拟更复杂的交易场景,例如多方参与的合约、并发交易和链上数据访问。 PAF 还支持与其他工具和库的集成,例如单元测试框架和模拟数据生成器,从而实现更全面的测试。 使用 PAF 需要一定的技术知识和开发经验,但它可以提供更准确和可靠的测试结果。 开发者还可以使用 PAF 构建模拟的链下应用程序 (Off-chain code),与 Plutus 合约进行交互,从而进行端到端的集成测试。
部署合约
将智能合约部署到 Cardano 区块链是一个涉及多个步骤的过程,其核心是将高级语言编写的合约转化为区块链可执行的代码,并将其永久存储在链上。这一过程首先需要将合约代码编译为 Plutus Core 脚本,Plutus Core 是 Cardano 用于智能合约执行的低级函数式编程语言。编译后的 Plutus Core 脚本代表了合约的逻辑,并将在满足特定条件时在区块链上执行。将合约存储在区块链上,确保合约的不可篡改性和永久可用性。
- 获取合约脚本
从成功编译后的 Cardano 智能合约中提取 Plutus Core 脚本是部署流程的关键一步。Plutus Core 脚本包含了合约的完整逻辑,它是合约在 Cardano 区块链上执行的基础。提取过程通常涉及使用 Cardano 提供的命令行工具或 SDK,例如
cardano-cli
,它可以从编译后的合约文件(通常是
.plutus
扩展名)中提取出 Plutus Core 脚本。
- 创建合约地址
在 Cardano 区块链上,每个智能合约都与一个唯一的地址相关联。要创建一个合约地址,你需要使用 Cardano 节点提供的工具,例如
cardano-cli address build
命令。创建地址的过程涉及指定脚本哈希(从 Plutus Core 脚本计算得出)以及网络魔术(指示地址属于哪个 Cardano 网络,例如主网或测试网)。合约地址用于接收锁定在该合约中的资金和触发合约执行的交易。合约地址本质上是锁定资金的“保管人”,只有满足合约逻辑规定的条件才能释放这些资金。
- 将合约脚本注册到区块链
为了让其他用户能够与你的智能合约交互,你需要将合约脚本注册到 Cardano 区块链。这通常通过提交一个包含合约脚本的特殊交易来实现。这个交易会将合约脚本存储在区块链上,并将其与你创建的合约地址关联起来。一旦注册完成,其他用户就可以通过向合约地址发送交易并包含适当的脚本见证人(证明满足合约条件)来执行合约。注册合约脚本是使合约可供整个 Cardano 网络使用的关键步骤,允许任何人验证合约逻辑并与之交互。
最佳实践
- 编写清晰、简洁且可读性高的代码 : Plutus 合约作为智能合约,需要在 Cardano 区块链上执行,因此代码的效率和简洁性至关重要。 避免冗余代码,使用有意义的变量名,并添加适当的注释以提高代码的可维护性。优化代码逻辑,减少 gas 消耗,提高合约的执行效率。
- 使用全面的单元测试和集成测试 : 仅仅验证合约的逻辑是否正确是不够的。需要编写全面的单元测试,覆盖各种可能的输入和边界条件,确保合约在各种情况下都能正常运行。还需要进行集成测试,模拟合约与其他合约或外部系统的交互,验证合约的整体功能。 使用专门的 Plutus 测试框架来简化测试流程。
- 进行严格的安全审计和形式化验证 : 在部署合约之前,进行专业的安全审计是必不可少的,这包括代码审查、模糊测试等方法,以发现潜在的安全漏洞,例如重入攻击、溢出漏洞等。 考虑使用形式化验证工具,对合约的代码进行数学建模,证明合约的安全性。
- 持续关注 Cardano 社区和 Plutus 平台更新 : 积极参与 Cardano 和 Plutus 开发者社区,例如论坛、邮件列表等,了解最新的开发动态、安全漏洞报告和最佳实践分享。 及时关注 Plutus 平台的更新,包括新的特性、优化和安全补丁。根据社区的反馈和平台的更新,不断改进和优化合约。
这是一个关于如何在 Cardano 区块链上进行合约开发的入门教程。希望本文能帮助你开始你的 Plutus 合约开发之旅。