By George Z. September 6, 2021
6. 如何写 Plutus 交易
本教程概述了什么是 Plutus 事务以及如何编写它。 这是按以下顺序完成的:
- 在链码上写你的 Plutus。
- 将 Plutus on chain code 序列化为文本信封格式(cardano-cli 需要这种格式)。
- 使用随附的 Plutus 脚本创建您的交易。
- 提交交易以执行 Plutus 脚本。
6.1 什么是 Plutus 交易
交易是包含输入和输出的一段数据,从 Alonzo 时代开始,它们还可以包含 Plutus 脚本。 输入是来自先前交易 (UTxO) 的未花费输出。 一旦 UTxO 被用作交易中的输入,它就会被花费并且永远无法再次使用。 输出由地址(公钥或公钥哈希)和值(由 ADA 金额和可选的附加本机代币金额组成)指定。 这个流程图让我们更好地了解交易的组成部分在技术层面上是什么:
figure-4
简而言之,输入包含对先前交易引入的 UTXO 的引用,输出是本次交易将产生的新 UTXO。此外,如果我们考虑一下,这允许我们更改智能合约的状态,因为新数据可以包含在生成的输出中。定义 Plutus Tx 是什么也很重要。 Plutus Tx 是 Haskell 程序的特殊分隔部分的名称,用于将合约应用程序的链上部分编译到 Plutus Core(此编译后的代码然后用于验证交易,因此称为“Tx”) .生成的 Plutus Core 表达式可以是交易数据的一部分,也可以是存储在账本上的数据。这些代码段需要在区块链上进行特殊处理,称为 Plutus 脚本。
6.1.1. 为什么
从 Plutus 开发人员的角度来看,通过使用事务,我们可以控制 Plutus 脚本的执行流程。因此,交易也可以被认为是用于与智能合约交互的消息。理解交易是掌握智能合约开发的关键概念。
6.1.2. 什么时候
交易应该由钱包在评估链外代码时创建。现在,我们必须使用 cardano-cli 组装交易并将编译后的 Plutus 脚本放入其中。不过,在后期阶段,这将由用户的钱包软件自动执行。交易一旦提交,将被验证,因此 Plutus 代码将由验证器节点评估。如果脚本评估成功,交易将被视为有效。如果没有,交易将被拒绝。
6.1.3. 设置环境
如果您已经设置好 Haskell 开发环境,请跳过本节,否则,我们将设置一个合适的环境来使用 Nix 编译 plutus 脚本,或者您可以按照本指南进行操作。我们将使用 Nix 来提供 Haskell 和 Cabal,但如果您愿意,您也可以依靠 ghcup 工具来管理这些依赖项。但是,我们不会涵盖这一点。您可以参考官方 ghcup 站点以获取有关该方法的说明。Nix 是一个了不起的工具,除其他外,它允许我们创建隔离的环境,我们可以在其中嵌入应用程序所需的所有依赖项。这些依赖项甚至可以是系统级依赖项。因此,我们可以创建一个隔离的环境来确保应用程序能够运行,因为所有必需的依赖项都可用。通过推荐的多用户安装在任何 Linux 发行版、MacOS 或 Windows(通过 WSL)上安装 Nix。简而言之,您需要在终端上运行它:
sh <(curl -L https://nixos.org/nix/install) --daemon
为了提高构建速度,强烈建议通过执行以下操作来设置由 IOHK 维护的二进制缓存:
sudo mkdir -p /etc/nix
cat <<EOF | sudo tee /etc/nix/nix.conf
substituters = https://cache.nixos.org https://hydra.iohk.io
trusted-public-keys = iohk.cachix.org-1:DpRUyj7h7V830dp/i6Nti+NEO2/nhblbov/8MW7Rqoo= hydra.iohk.io:f/Ea+s+dFdN+3Y/G+FDgSq+a5NEWhJGzdjvKNGv0/EQ= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
EOF
在 Nix 在您现有的 shell 中工作之前,您需要关闭它们并再次打开它们。 除此之外,你应该准备好了。安装 Nix 后,注销然后重新登录,以便在您的 shell 中正确激活它。 克隆以下内容并查看最新版本的节点。 请参阅 cardano-node 发布页面以确保您使用的是最新版本。
git clone https://github.com/input-output-hk/cardano-node
cd cardano-node
git fetch --all --recurse-submodules --tags
git checkout tags/1.29.0
在我们刚刚克隆的 git 存储库的根目录中创建一个文件,并将其保存为 plutus-tutorial.nix:
{ version ? "mainnet", pkgs ? import <nixpkgs> { }}:
let
cardano-node-repo = import ./. { };
in pkgs.mkShell {
buildInputs = with pkgs; [
libsodium
cabal-install
zlib
haskell.compiler.ghc8104
haskellPackages.haskell-language-server
cardano-node-repo.scripts."${version}".node
cardano-node-repo.cardano-cli
];
CARDANO_NODE_SOCKET_PATH = "${builtins.toString ./.}/state-node-${version}/node.socket";
}
然后使用以下命令使用此文件加载带有 Nix 的 shell:
nix-shell plutus-tutorial.nix
第一次执行此操作大约需要五到十分钟,您应该会看到类似以下内容:
these paths will be fetched (445.08 MiB download, 5870.53 MiB unpacked):
/nix/store/04jc7s1006vhg3qj4fszg6bcljlyap1a-conduit-parse-0.2.1.0-doc
/nix/store/052kzx9p5fl52pk436i2jcsqkz3ni0r2-reflection-2.1.6-doc
.
.
.
/nix/store/7jq1vjy58nj8rjwa688l5x7dyzr55d9f-monad-memo-0.5.3... (34 KB left)
这将创建一个环境,其中包含“buildInputs”部分中列出的所有依赖项,其中包括 GHC 8.10.4 和 Cabal。一旦您拥有最新版本的 GHC 和 Cabal,请确保使用 GHC 8.10.2 或更高版本:
[nix-shell:~]$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 8.10.4
[nix-shell:~]$ cabal --version
cabal-install version 3.4.0.0
compiled using version 3.4.0.0 of the Cabal library
6.1.4. 运行 cardano-node
在 nix-shell 里面启动一个被动的 Cardano 节点,如果你还没有激活的话记得先激活 Nix 环境:
nix-shell plutus-tutorial.nix
[nix-shell:~]
cardano-node-mainnet
此时,节点将开始与网络同步,这将有助于稍后提交我们的交易。 我们现在准备开始构建 Plutus 交易。 保持节点在此 shell 中运行,并打开一个新终端以继续执行以下步骤。 请记住在这个新终端中进入 nix-shell 环境,以便您同时使用 GHC 和 Cabal。
- 在链码上写你的 Plutus 我们需要一个 Haskell 程序来编译我们想要的 Plutus 脚本。 在这个例子中,我们将使用 plutus-alwayssucceeds Plutus 脚本。 注意“Plutus 总是成功”脚本在本教程中仅用作示例,因为它使用非常简单的保护机制,不执行任何资金验证。 因此,我们建议您不要在主网上部署此合约。 如果你仍然想在主网上部署它,你应该使用更安全的数据。
git clone https://github.com/input-output-hk/Alonzo-testnet.git
cd Alonzo-testnet/resources/plutus-sources/plutus-alwayssucceeds
请注意,即使该程序是测试网示例的一部分,它也可以在主网上正常工作。
- 在链码上序列化你的 Plutus 通过构建项目,我们生成了一个编译此脚本的二进制文件。
cabal update
cabal build
执行 plutus-alwayssucceeds 项目 我们将选择一个随机数。 它将作为参数传递给 Plutus 脚本(脚本现在不使用它,但使用脚本的事务将需要它)。 第二个参数是我们想要编译的 Plutus 脚本的文件名。
cabal run plutus-alwayssucceeds -- 42 alwayssucceeds.plutus
您应该会看到如下内容:
Up to date
Writing output to: alwayssucceeds.plutus
"Log output"
[]
"Ex Budget"
ExBudget {exBudgetCPU = ExCPU 297830, exBudgetMemory = ExMemory 1100}
cat alwayssucceeds.plutus
您应该会看到如下内容:
{
"type": "PlutusScriptV1",
"description": "",
"cborHex": "4e4d01000033222220051200120011"
}
- 使用随附的 Plutus 脚本创建您的交易 然后我们将编译 Plutus 脚本。 现在,我们需要使用包含 Plutus 脚本的 cardano-cli 项目来构建交易。确保您拥有最新的标记版本时代。
cardano-cli query tip --mainnet
您应该会看到如下内容:
{
"epoch": 155,
"hash": "c8ae0bb7f06743cd95c35e19c866a811b7a3f104ad362c8667b9f0a1f0907ed2",
"slot": 36882965,
"block": 2899736,
"era": "Alonzo",
"syncProgress": "100.00"
}
注意:确保“era”对应于“Alonzo”。 如果您刚刚启动了节点,您可能需要等待您的节点同步才能看到这一点。 构建交易实际上不需要节点,但将交易提交到网络很有用。
6.1.5. 生成钱包
如果您还没有,我们必须首先创建一个支付密钥对和一个钱包地址。 对于这个例子,我们需要生成两个地址,如下所示。 对于这一步,在相应的地址中生成一个支付密钥:
cardano-cli address key-gen \
--verification-key-file payment.vkey \
--signing-key-file payment.skey
cardano-cli stake-address key-gen \
--verification-key-file stake.vkey \
--signing-key-file stake.skey
cardano-cli address build \
--payment-verification-key-file payment.vkey \
--stake-verification-key-file stake.vkey \
--out-file payment.addr \
--mainnet
cat payment.addr
确保使用上述相同步骤生成一个额外的钱包,以便您可以测试这些地址之间的交易。
6.1.6. 构建并提交一个简单的(非 Plutus)交易
在这个简单的交易中,我们将资金从一个个人地址发送到另一个地址。 假设我们在 payment.addr 和 payment2.addr 文件中有这些地址,我们想从第一个地址向第二个地址发送 500 个 ADA。首先,我们需要查询付款中的 UTXO。 地址:
cardano-cli query utxo --address $(cat payment.addr) --mainnet
考虑到您的地址有余额,您应该看到如下内容:
TxHash TxIx Amount
--------------------------------------------------------------------------------------
8c6f74370d823130847efe3d2e2e128f0e79c8e907fda692353d841dd0d6cb38 0 1000000000 lovelace + TxOutDatumHashNone
使用这些信息,我们可以建立一个交易:
cardano-cli transaction build \
--alonzo-era \
--mainnet \
--change-address $(cat payment.addr) \
--tx-in 8c6f74370d823130847efe3d2e2e128f0e79c8e907fda692353d841dd0d6cb38#0 \
--tx-out $(cat payment2.addr)+500000000 \
--out-file tx.build
在 –tx-in 参数中,我们设置了我们用作输入的 UTXO,其格式为 TxHash#TxIx。 –tx-out 参数决定了新 UTXO 的输出,其格式为地址+数量。如上图所示,我们可以有一个或多个输入和输出。接下来是签署并提交交易:
cardano-cli transaction sign \
--tx-body-file tx.build \
--mainnet \
--signing-key-file payment.skey \
--out-file tx.signed
cardano-cli transaction submit --tx-file tx.signed --mainnet
Transaction successfully submitted.
现在,如果我们查询 payment2.addr,我们将有一个包含 30,000 个 ADA 的新 UTxO:
cardano-cli query utxo --address $(cat payment2.addr) --mainnet
TxHash TxIx Amount
--------------------------------------------------------------------------------------
d7d207438c90fe611c1a14be29974b1662f8563331bf6fba4b6569e089ffa561 1 500000000 lovelace + TxOutDatumHashNone
cardano-cli query utxo --address $(cat payment.addr) --mainnet
TxHash TxIx Amount
--------------------------------------------------------------------------------------
d7d207438c90fe611c1a14be29974b1662f8563331bf6fba4b6569e089ffa561 0 499831815 lovelace + TxOutDatumHashNone
我们现在已经发送了一个简单的交易。
6.1.7. 交易锁定资金
锁定资金的交易与简单的交易非常相似。 但是,它有两个关键区别:我们将资金锁定到脚本地址而不是公共地址,并且我们需要为每个输出指定一个数据哈希。我们使用我们之前编译的 plutus-alwayssucceeds Plutus 验证器脚本。 无论数据和赎回者的值如何,此脚本都不会检查任何内容并且总是会成功。
{-# INLINABLE mkValidator #-}
mkValidator :: Data -> Data -> Data -> ()
mkValidator _ _ _ = ()
首先,计算脚本地址
cardano-cli address build \
--payment-script-file alwayssucceeds.plutus \
--mainnet \
--out-file script.addr
在 script.addr文件中找到地址
cat script.addr
我们不直接将数据附加到 UTXO,而是使用它的哈希。 要获取数据的哈希值,请运行以下 cardano-cli 命令:
cardano-cli transaction hash-script-data --script-data-value 42
export scriptdatumhash=7c7c0bf83e0ed45faf3976a5ee19b4ef8bd069baab4275425161ac89d492bf82
接下来,获取协议参数并将它们保存到名为 pparams.json 的文件中:
cardano-cli query protocol-parameters \
--mainnet \
--out-file pparams.json
现在,我们应该构建将 ADA 发送到我们的 plutus-alwayssucceeds 脚本的脚本地址的交易。 我们将交易写入名为 tx-script.build 的文件中:
cardano-cli transaction build \
--alonzo-era \
--mainnet \
--change-address $(cat payment.addr) \
--tx-in d7d207438c90fe611c1a14be29974b1662f8563331bf6fba4b6569e089ffa561#0 \
--tx-out $(cat script.addr)+1379280 \
--tx-out-datum-hash ${scriptdatumhash} \
--protocol-params-file pparams.json \
--out-file tx-script.build
继续使用签名密钥 payment.skey 对交易进行签名,并将这个签名的交易保存在一个文件 tx-script.signed 中:
cardano-cli transaction sign \
--tx-body-file tx-script.build \
--signing-key-file payment.skey \
--mainnet \
--out-file tx-script.signed
最后,提交交易
cardano-cli transaction submit --mainnet --tx-file tx-script.signed
Transaction successfully submitted.
我们可以查询个人地址和脚本地址:
cardano-cli query utxo --address $(cat payment.addr) --mainnet
TxHash TxIx Amount
--------------------------------------------------------------------------------------
f5a618d579bc66e6199ae2a1ab4a73e2d8a73cba61a324c939346e9cf32bb33a 0 498284086 lovelace + TxOutDatumHashNone
cardano-cli query utxo --address $(cat script.addr) --mainnet
TxHash TxIx Amount
--------------------------------------------------------------------------------------
f5a618d579bc66e6199ae2a1ab4a73e2d8a73cba61a324c939346e9cf32bb33a 1 1379280 lovelace + TxOutDatumHash ScriptDataInAlonzoEra "7c7c0bf83e0ed45faf3976a5ee19b4ef8bd069baab4275425161ac89d492bf82"
.
.
export plutusutxotxin=f5a618d579bc66e6199ae2a1ab4a73e2d8a73cba61a324c939346e9cf32bb33a#1
现在,我们已经向脚本发送了资金。
- 提交交易以执行 Plutus 脚本 要从脚本中解锁资金,我们需要赎回者。 让我们记住,无论赎回者的价值如何,只要我们提供正确的数据,这个脚本总是会成功。 所以我们可以使用任何值作为赎回者。 我们还需要一个输入作为抵押:如果交易失败,它会承担费用。 然后,我们需要一个有足够资金的 UTXO。 我们将使用payment2.addr 帐户作为示例创建一个简单的交易。它产生了两个新的 UTXO。
检查余额:
cardano-cli query utxo --address $(cat payment2.addr) --mainnet
TxHash TxIx Amount
--------------------------------------------------------------------------------------
d7d207438c90fe611c1a14be29974b1662f8563331bf6fba4b6569e089ffa561 1 500000000 lovelace + TxOutDatumHashNone
export txCollateral="d7d207438c90fe611c1a14be29974b1662f8563331bf6fba4b6569e089ffa561#1"
构建、签署并提交新交易以解锁资金:
cardano-cli transaction build \
--alonzo-era \
--mainnet \
--tx-in ${plutusutxotxin} \
--tx-in-script-file alwayssucceeds.plutus \
--tx-in-datum-value 42 \
--tx-in-redeemer-value 42 \
--tx-in-collateral ${txCollateral} \
--change-address $(cat payment.addr) \
--protocol-params-file pparams.json \
--out-file test-alonzo.tx
如果我们使用属于脚本地址一部分的 UTXO 作为交易的输入,我们需要指定 –tx-in-script-file –tx-in datum-value –tx-in-redeemer-value 在包含该 UTXO 的 –tx-in 参数之后的 –tx-in-collateral 参数:
cardano-cli transaction sign \
--tx-body-file test-alonzo.tx \
--signing-key-file payment.skey \
--mainnet \
--out-file test-alonzo.signed
cardano-cli transaction submit --mainnet --tx-file test-alonzo.signed
Transaction successfully submitted.
现在,如果我们查询两个地址,我们可以看到我们已经解锁了资金:
cardano-cli query utxo --address $(cat payment2.addr) --mainnet
cardano-cli query utxo --address $(cat script.addr) --mainnet
至此,您已经成功提交了您的第一笔 Plutus 交易!