Xây dựng dapp trên cardano từ con số không
About Lesson

Introduction eUTxO model (datums, redeemers, etc.) & Aiken syntax

Bài viết này hướng dẫn cách hiểu mô hình eUTxO (Extended Unspent Transaction Output) trên blockchain Cardano, vai trò của scriptdatum, và redeemer trong hợp đồng thông minh, cùng với cách sử dụng ngôn ngữ Aiken để viết hợp đồng thông minh. Nội dung được thiết kế cho các nhà phát triển xây dựng ứng dụng phi tập trung (dApps) trên Cardano, cung cấp các khái niệm cốt lõi và hướng dẫn thực hành để triển khai hợp đồng thông minh đơn giản.

Yêu Cầu Chuẩn Bị

Trước khi bắt đầu, hãy đảm bảo bạn có:

  • Node.js và npm: Cài đặt Node.js (khuyến nghị phiên bản 16 trở lên) và npm để quản lý phụ thuộc JavaScript.
  • Trình Soạn Thảo Mã: Sử dụng Visual Studio Code hoặc tương tự.
  • Aiken: Cài đặt Aiken để viết hợp đồng thông minh (hướng dẫn chi tiết bên dưới).
  • Ví Cardano: Thiết lập ví Cardano (như Eternl) với test ADA (tADA) trên mạng thử nghiệm Cardano testnet.
  • Kiến Thức Cơ Bản: Hiểu biết về JavaScript, blockchain, và các khái niệm cơ bản về hợp đồng thông minh.

Tổng Quan Về Mô Hình eUTxO

Blockchain Cardano và Cấu Trúc Block

Blockchain Cardano được tổ chức thành các block (khối), mỗi block gồm hai phần chính:

  1. Header: Chứa thông tin như mã hash của block hiện tại, mã hash của block trước (đảm bảo tính liên kết), và chi tiết block.
  2. Body: Lưu trữ danh sách các giao dịch (transactions) xảy ra trong khoảng thời gian block được tạo.

UTXO Là Gì?

UTXO (Unspent Transaction Output) là đầu ra chưa được chi tiêu của một giao dịch, đại diện cho tài sản mà một ví sở hữu. Ví dụ:

  • Alice có 100 ADA từ một giao dịch trước (UTXO).
  • Alice muốn chuyển 10 ADA cho Bob:
    • Đầu vào: UTXO 100 ADA của Alice.
    • Đầu ra: 10 ADA cho Bob và 90 ADA trả lại cho Alice.
    • Giao dịch tiêu thụ UTXO 100 ADA và tạo hai UTXO mới: 10 ADA (Bob) và 90 ADA (Alice).

Mỗi ví trên Cardano lưu trữ danh sách UTXO, và để thực hiện giao dịch, bạn phải sử dụng toàn bộ UTXO làm đầu vào, sau đó phân phối lại thành các UTXO mới.

eUTxO Là Gì?

eUTxO (Extended UTXO) là phiên bản mở rộng của mô hình UTXO, được Cardano sử dụng để hỗ trợ hợp đồng thông minh. Điểm khác biệt chính:

  • Ngoài ví người dùng, hợp đồng thông minh (smart contracts) cũng có thể sở hữu UTXO.
  • UTXO trong hợp đồng thông minh được gắn với datum (dữ liệu) và được kiểm soát bởi script (logic hợp đồng).
  • Để tiêu UTXO từ hợp đồng, giao dịch phải cung cấp redeemer và thỏa mãn logic của script.

Địa Chỉ Trên Cardano

Địa chỉ Cardano bao gồm ba phần:

  1. Header: Xác định loại địa chỉ (ví dụ: ví người dùng hoặc hợp đồng thông minh) và mạng (mainnet/testnet).
  2. Payment: Liên kết với khóa công khai của ví hoặc script của hợp đồng thông minh.
  3. Delegation (tùy chọn): Liên quan đến staking, không bắt buộc.

Ví dụ địa chỉ (Bech32): addr_test1.... Khi chuyển sang hex (Bech16), bạn có thể tách rõ các phần header, payment, và delegation.

Script, Datum, và Redeemer

  1. Script:

    • Là logic của hợp đồng thông minh, xác định điều kiện để tiêu UTXO.
    • Script hoạt động như một ví nhưng không có private key, thay vào đó sử dụng logic để xác thực giao dịch.
    • Ví dụ: Script yêu cầu chữ ký từ một ví cụ thể hoặc kiểm tra điều kiện logic.
  2. Datum:

    • Là dữ liệu được gắn vào UTXO trong hợp đồng thông minh, lưu trữ thông tin như trạng thái hợp đồng (ví dụ: “ABC”).
    • Datum được lưu on-chain và có thể truy vấn để đọc dữ liệu.
  3. Redeemer:

    • Là dữ liệu được cung cấp trong giao dịch để tương tác với script.
    • Script sử dụng redeemer và datum để kiểm tra xem giao dịch có thỏa mãn logic hay không (trả về true hoặc false).

Ví dụ:

  • Alice gửi 250 ADA vào một hợp đồng thông minh với datum “ABC”.
  • Để rút 250 ADA từ hợp đồng, Alice tạo giao dịch:
    • Đầu vào: UTXO 250 ADA từ hợp đồng.
    • Redeemer: Dữ liệu (ví dụ: “withdraw”) để thỏa mãn logic script.
    • Script: Kiểm tra xem redeemer và datum có khớp với logic không (ví dụ: redeemer là “withdraw” và giao dịch được ký bởi Alice).
  • Nếu logic trả về true, giao dịch được chấp nhận, và UTXO được chuyển về ví Alice.

Ngữ Cảnh (Purposes) của eUTxO

Cardano hỗ trợ các ngữ cảnh chính cho eUTxO:

  1. Spend: Tiêu UTXO từ ví hoặc hợp đồng để chuyển tài sản.
  2. Mint: Tạo hoặc hủy tài sản (dùng policy ID và forging script).
  3. Vote/Proposal: Quản trị on-chain (governance), như bỏ phiếu hoặc đề xuất.

Sử Dụng Aiken Để Viết Hợp Đồng Thông Minh

Aiken là một ngôn ngữ lập trình được thiết kế để viết hợp đồng thông minh trên Cardano, cung cấp cú pháp đơn giản và hiệu quả hơn so với Haskell/Plutus. Dưới đây là hướng dẫn cài đặt Aiken và viết một hợp đồng “Hello World”.

Bước 1: Cài Đặt Aiken

  1. Cài Đặt Qua npm:

    • Chạy lệnh sau để cài đặt Aiken (sử dụng bun thay npm nếu bạn dùng Bun):
      npm install -g @aiken-lang/aiken
       
    • Kiểm tra phiên bản:
      aiken --version
       

      Phiên bản mới nhất tại thời điểm viết là 1.0.19.

  2. Tạo Dự Án Aiken:

    • Tạo một dự án mới:
      aiken new hello-world
      cd hello-world
       
    • Cấu trúc dự án:
      • Thư mục validators: Chứa các file script hợp đồng thông minh.
      • File aiken.toml: Cấu hình dự án.

Bước 2: Viết Hợp Đồng Hello World

Tạo một hợp đồng đơn giản kiểm tra xem giao dịch có được ký bởi một ví cụ thể và redeemer khớp với một giá trị nhất định.

  1. Trong thư mục validators, tạo file hello_world.ak:
validator {
  fn hello_world(datum: String, redeemer: String, ctx: ScriptContext) -> Bool {
    let owner = "HelloWorld" // Giá trị datum mong muốn
    let expected_redeemer = "HelloWorld" // Giá trị redeemer mong muốn
    let is_signed = ctx.tx.signatories.contains(ctx.script_address)

    datum == owner && redeemer == expected_redeemer && is_signed
  }
}
 

Giải Thích Mã:

  • Validator: Định nghĩa logic hợp đồng thông minh.
  • Parameters:
    • datum: String: Dữ liệu gắn với UTXO (ví dụ: “HelloWorld”).
    • redeemer: String: Dữ liệu cung cấp trong giao dịch (ví dụ: “HelloWorld”).
    • ctx: ScriptContext: Ngữ cảnh giao dịch, chứa thông tin như chữ ký và địa chỉ script.
  • Logic:
    • Kiểm tra datum có khớp với “HelloWorld”.
    • Kiểm tra redeemer có khớp với “HelloWorld”.
    • Kiểm tra giao dịch có được ký bởi ví liên quan đến script (ctx.tx.signatories.contains).
  • Kết Quả: Trả về true nếu tất cả điều kiện đều thỏa mãn, cho phép tiêu UTXO.
  1. Build Hợp Đồng:
    • Chạy lệnh để biên dịch hợp đồng:
      aiken build
       
    • Kết quả tạo file plutus.json trong thư mục build, chứa mã hợp đồng đã biên dịch (CBOR format).
    • File này có thể được sử dụng với MeshJS để tích hợp vào dApp.

Bước 3: Tích Hợp Hợp Đồng Với MeshJS

Để sử dụng hợp đồng trong một dự án Next.js, bạn cần tích hợp file plutus.json với MeshJS. Dưới đây là cách thực hiện giao dịch sử dụng hợp đồng:

  1. Tạo Trang Giao Dịch: Tạo tệp app/contract/page.jsx để gọi hợp đồng:
'use client';
import { useState, useEffect } from 'react';
import { Transaction, BrowserWallet } from '@meshsdk/core';
import WalletConnect from '../components/WalletConnect';

export default function Contract() {
  const [wallet, setWallet] = useState(null);
  const [balance, setBalance] = useState(0);
  const [datum, setDatum] = useState('HelloWorld');
  const [redeemer, setRedeemer] = useState('HelloWorld');

  useEffect(() => {
    const getBalance = async () => {
      if (wallet) {
        try {
          const balance = await wallet.getBalance();
          setBalance(balance.find(asset => asset.unit === 'lovelace').quantity / 1000000);
        } catch (error) {
          console.error('Error fetching balance:', error);
        }
      } else {
        setBalance(0);
      }
    };
    getBalance();
  }, [wallet]);

  const lockFunds = async () => {
    if (!wallet) {
      alert('Please connect a wallet first.');
      return;
    }

    try {
      const addresses = await wallet.getUsedAddresses();
      const senderAddress = addresses[0];

      // Địa chỉ hợp đồng (lấy từ plutus.json hoặc tạo từ script)
      const scriptAddress = 'addr_test1...script_address...'; // Thay bằng địa chỉ script thực tế
      const tx = new Transaction({ initiator: wallet });
      tx.sendValue(
        { lovelace: '250000000' }, // 250 ADA
        scriptAddress,
        { datum: { value: datum, inline: true } } // Gắn datum
      );

      const unsignedTx = await tx.build();
      const signedTx = await wallet.signTx(unsignedTx);
      const txHash = await wallet.submitTx(signedTx);
      alert(`Funds locked: ${txHash}`);
      console.log('Transaction Hash:', txHash);
    } catch (error) {
      console.error('Error locking funds:', error);
      alert('Failed to lock funds.');
    }
  };

  const unlockFunds = async () => {
    if (!wallet) {
      alert('Please connect a wallet first.');
      return;
    }

    try {
      const addresses = await wallet.getUsedAddresses();
      const senderAddress = addresses[0];
      const scriptAddress = 'addr_test1...script_address...'; // Thay bằng địa chỉ script thực tế

      // Lấy UTXO từ script (giả sử dùng Blockfrost)
      const projectId = 'preprodYourProjectIdHere'; // Thay bằng Project ID
      const response = await fetch(`https://cardano-preprod.blockfrost.io/api/v0/addresses/${scriptAddress}/utxos`, {
        headers: { project_id: projectId },
      });
      const utxos = await response.json();
      if (utxos.length === 0) {
        throw new Error('No UTXOs found in script');
      }

      const formattedUtxos = utxos.map(utxo => ({
        input: { outputIndex: utxo.output_index, txHash: utxo.tx_hash },
        output: {
          address: scriptAddress,
          amount: utxo.amount.map(asset => ({ unit: asset.unit, quantity: asset.quantity })),
          datum: utxo.data_hash ? { value: datum, inline: true } : undefined,
        },
      }));

      // Tạo giao dịch unlock
      const tx = new Transaction({ initiator: wallet });
      tx.setTxInputs(formattedUtxos);
      tx.sendValue({ lovelace: '250000000' }, senderAddress); // Gửi về ví người gửi
      tx.setRedeemer({ data: redeemer }); // Gắn redeemer
      tx.setScript({ code: 'script_code_from_plutus.json' }); // Thay bằng mã script từ plutus.json

      const unsignedTx = await tx.build();
      const signedTx = await wallet.signTx(unsignedTx, true);
      const txHash = await wallet.submitTx(signedTx);
      alert(`Funds unlocked: ${txHash}`);
      console.log('Transaction Hash:', txHash);
    } catch (error) {
      console.error('Error unlocking funds:', error);
      alert('Failed to unlock funds.');
    }
  };

  return (
    <main>
      <h1>Interact with Smart Contract</h1>
      <WalletConnect setWallet={setWallet} />
      <div>
        <h2>Lock Funds</h2>
        <button onClick={lockFunds}>Lock 250 ADA</button>
      </div>
      <div>
        <h2>Unlock Funds</h2>
        <button onClick={unlockFunds}>Unlock 250 ADA</button>
      </div>
      <p>Balance: {balance} ADA</p>
    </main>
  );
}
 

Giải Thích Mã:

  • Lock Funds: Gửi 250 ADA vào hợp đồng với datum “HelloWorld”.
  • Unlock Funds:
    • Lấy UTXO từ địa chỉ hợp đồng qua Blockfrost.
    • Gắn redeemer “HelloWorld” và mã script từ plutus.json.
    • Gửi 250 ADA về ví người gửi nếu logic script trả về true.
  • Script Address: Cần lấy từ plutus.json hoặc tạo từ Aiken (xem tài liệu Aiken để lấy địa chỉ script).

Kiểm Tra Giao Dịch:

  • Chạy dự án: npm run dev.
  • Truy cập http://localhost:3000/contract.
  • Kết nối ví Eternl, nhấn “Lock 250 ADA” để gửi tiền vào hợp đồng, sau đó nhấn “Unlock 250 ADA” để rút.
  • Kiểm tra hash giao dịch trên Cardano Testnet Explorer.

Tài Liệu Tham Khảo

Kết Luận

Bài viết đã giải thích mô hình eUTxO trên Cardano, vai trò của scriptdatum, và redeemer, cùng cách sử dụng Aiken để viết hợp đồng thông minh. Bạn đã học cách cài đặt Aiken, tạo hợp đồng “Hello World”, và tích hợp với MeshJS để tương tác trong một dApp. Để mở rộng, bạn có thể viết các hợp đồng phức tạp hơn hoặc tích hợp với các ngữ cảnh như minting hoặc governance, tham khảo tài liệu Aiken và Cardano.

Bài Tập

Bài tập 1: Hiểu về UTxO và giao dịch cơ bản

Đề bài

Alice có 200 ADA trong một UTxO. Cô ấy muốn chuyển 50 ADA cho Bob và giữ lại phần còn lại. Hãy mô tả cách giao dịch này hoạt động trong mô hình UTxO.

Yêu cầu

  • Mô tả các thành phần đầu vào và đầu ra của giao dịch.
  • Xác định số dư còn lại của Alice và Bob sau giao dịch.
Cách giải
  1. Trong mô hình UTxO, một giao dịch sử dụng UTxO làm đầu vào và tạo ra các UTxO mới làm đầu ra.
  2. Đầu vào: Alice sử dụng UTxO chứa 200 ADA.
  3. Đầu ra:
    • 50 ADA được chuyển đến ví của Bob (tạo UTxO mới cho Bob).
    • Phần còn lại (200 – 50 = 150 ADA) được trả về ví của Alice (tạo UTxO mới cho Alice).
  4. Giao dịch phải được ký bởi Alice để xác nhận rằng cô ấy có quyền chi tiêu UTxO này.
  5. Sau khi giao dịch được xác nhận và đưa vào blockchain, trạng thái UTxO được cập nhật.

Đáp án

  • Đầu vào: UTxO của Alice chứa 200 ADA.
  • Đầu ra:
    • UTxO mới cho Bob: 50 ADA.
    • UTxO mới cho Alice: 150 ADA.
  • Số dư sau giao dịch:
    • Alice: 150 ADA.
    • Bob: 50 ADA.

Bài tập 2: Hiểu về eUTxO và Smart Contract

Đề bài

Một smart contract trên Cardano chứa một UTxO với 100 ADA và một datum lưu giá trị "LockUntil:2025-12-31". Để mở khóa UTxO này, cần cung cấp một redeemer. Hãy giải thích vai trò của datum và redeemer trong giao dịch này.

Yêu cầu

  • Giải thích datum và redeemer là gì.
  • Mô tả cách giao dịch sử dụng chúng để mở khóa UTxO.
Cách giải
  1. Datum: Là dữ liệu được đính kèm vào UTxO trong smart contract, ở đây là "LockUntil:2025-12-31", biểu thị điều kiện khóa (ví dụ: thời gian khóa đến 31/12/2025).
  2. Redeemer: Là dữ liệu được cung cấp trong giao dịch để tương tác với script, ví dụ: thời gian hiện tại hoặc chữ ký xác nhận.
  3. Trong giao dịch:
    • Đầu vào: UTxO chứa 100 ADA và datum "LockUntil:2025-12-31".
    • Redeemer: Người thực hiện giao dịch cung cấp giá trị (ví dụ: thời gian hiện tại) để so sánh với datum.
    • Validator script: Kiểm tra xem redeemer (thời gian hiện tại) có thỏa mãn điều kiện trong datum (sau 31/12/2025) không. Nếu đúng, giao dịch hợp lệ và UTxO được chi tiêu.
  4. Kết quả: Nếu validator trả về true, 100 ADA được chuyển đến ví của người thực hiện giao dịch.

Đáp án

  • Datum: Lưu trữ thông tin điều kiện khóa, ở đây là "LockUntil:2025-12-31".
  • Redeemer: Cung cấp dữ liệu (ví dụ: thời gian hiện tại) để validator kiểm tra.
  • Quy trình:
    • Giao dịch lấy UTxO với 100 ADA làm đầu vào.
    • Redeemer được so sánh với datum qua validator script.
    • Nếu thời gian hiện tại > 31/12/2025, validator trả về true, UTxO được mở khóa và 100 ADA được chuyển.

Bài tập 3: Viết hợp đồng Aiken cơ bản

Đề bài

Viết một hợp đồng Aiken đơn giản cho phép khóa 50 ADA vào một smart contract với datum là "Hello". Để mở khóa, redeemer phải khớp với datum.

Yêu cầu

  • Viết mã Aiken cho validator của hợp đồng.
  • Giải thích cách validator kiểm tra điều kiện.
Cách giải
  1. Trong Aiken, một validator được định nghĩa để kiểm tra logic giao dịch.
  2. Validator nhận 3 tham số:
    • Datum: Chuỗi "Hello".
    • Redeemer: Chuỗi do người dùng cung cấp.
    • Script context: Thông tin giao dịch (ở đây không sử dụng).
  3. Logic: So sánh redeemer với datum, trả về true nếu chúng khớp.
  4. Mã Aiken được viết trong file trong thư mục validators.

Đáp án

validator {
  fn spend(datum: String, redeemer: String, _ctx: ScriptContext) -> Bool {
    datum == redeemer
  }
}
 
  • Giải thích:
    • Datum được lưu là "Hello".
    • Redeemer là chuỗi do người dùng cung cấp khi giao dịch.
    • Validator kiểm tra datum == redeemer. Nếu redeemer là "Hello", trả về true, cho phép chi tiêu UTxO chứa 50 ADA.
    • Lệnh aiken build sẽ biên dịch mã này thành file plutus.json để sử dụng trong giao dịch.

Bài tập 4: Giao dịch với nhiều UTxO

Đề bài

Bob có hai UTxO: một chứa 60 ADA và một chứa 20 ADA. Anh ta muốn chuyển 70 ADA cho Charlie. Hãy mô tả cách giao dịch này được thực hiện trong mô hình eUTxO.

Yêu cầu

  • Mô tả các UTxO đầu vào và đầu ra.
  • Tính toán số dư còn lại của Bob sau giao dịch.
Cách giải
  1. Để chuyển 70 ADA, Bob phải sử dụng cả hai UTxO vì không UTxO nào đủ 70 ADA.
  2. Đầu vào:
    • UTxO 1: 60 ADA.
    • UTxO 2: 20 ADA.
    • Tổng: 60 + 20 = 80 ADA.
  3. Đầu ra:
    • UTxO mới cho Charlie: 70 ADA.
    • UTxO mới trả lại cho Bob: 80 – 70 = 10 ADA.
  4. Giao dịch được ký bởi Bob và đưa vào blockchain.

Đáp án

  • Đầu vào:
    • UTxO 1: 60 ADA.
    • UTxO 2: 20 ADA.
  • Đầu ra:
    • UTxO cho Charlie: 70 ADA.
    • UTxO trả lại cho Bob: 10 ADA.
  • Số dư còn lại của Bob: 10 ADA.

Bài tập 5: Hiểu về địa chỉ và script trong Cardano

Đề bài

Một địa chỉ smart contract trên Cardano chứa một UTxO với 30 ADA và datum "Vote:Yes". Một giao dịch được gửi để chi tiêu UTxO này với redeemer "Vote:Yes". Hãy giải thích cách địa chỉ script và validator hoạt động trong trường hợp này.

Yêu cầu

  • Mô tả cấu trúc địa chỉ script.
  • Giải thích cách validator sử dụng datum và redeemer để xác thực giao dịch.
Cách giải
  1. Địa chỉ script:
    • Gồm 3 phần: header (loại địa chỉ và mạng), payment (liên kết với script hash), delegation (tùy chọn, thường không có trong script).
    • Địa chỉ này đại diện cho smart contract, không có private key.
  2. Validator:
    • Nhận datum ("Vote:Yes"), redeemer ("Vote:Yes"), và script context.
    • Kiểm tra xem redeemer có khớp với datum không.
    • Nếu khớp, validator trả về true, cho phép chi tiêu UTxO.
  3. Giao dịch:
    • Đầu vào: UTxO chứa 30 ADA và datum "Vote:Yes".
    • Redeemer: "Vote:Yes".
    • Nếu validator trả về true, 30 ADA được chuyển đến địa chỉ người gửi giao dịch.

Đáp án

  • Cấu trúc địa chỉ script:
    • Header: Xác định mạng (mainnet/testnet) và loại địa chỉ (script).
    • Payment: Hash của script (thay vì public key như ví người dùng).
    • Delegation: Thường trống.
  • Quy trình xác thực:
    • Validator so sánh datum ("Vote:Yes") với redeemer ("Vote:Yes").
    • Nếu chúng khớp, validator trả về true, giao dịch hợp lệ.
    • UTxO chứa 30 ADA được chi tiêu và chuyển đến ví người gửi giao dịch.

Hướng dẫn thêm

  • Để thực hành, bạn có thể cài đặt Aiken qua lệnh npm install -g aiken hoặc bun install -g aiken.
  • Sử dụng lệnh aiken new để tạo dự án mới và aiken build để biên dịch hợp đồng.
  • Tham khảo tài liệu Aiken tại https://aiken-lang.org để biết thêm chi tiết.

Link Source Code: https://github.com/htlabs-xyz/Cardano-App-Development-Course/tree/main/Code/Video_08

Link Bài Tập: https://github.com/htlabs-xyz/Cardano-App-Development-Course/blob/main/Exercises/Video_08.md