
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 script, datum, 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.
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.
Blockchain Cardano được tổ chức thành các block (khối), mỗi block gồm hai phần chính:
- 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.
- 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 (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 (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ỉ Cardano bao gồm ba phần:
- 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).
- 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.
- 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:
- 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.
-
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.
-
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ề
truehoặcfalse).
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.
Cardano hỗ trợ các ngữ cảnh chính cho eUTxO:
- Spend: Tiêu UTXO từ ví hoặc hợp đồng để chuyển tài sản.
- Mint: Tạo hoặc hủy tài sản (dùng policy ID và forging script).
- Vote/Proposal: Quản trị on-chain (governance), như bỏ phiếu hoặc đề xuất.
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”.
-
Cài Đặt Qua npm:
- Chạy lệnh sau để cài đặt Aiken (sử dụng
bunthaynpmnế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.
- Chạy lệnh sau để cài đặt Aiken (sử dụng
-
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.
- Thư mục
- Tạo một dự án mới:
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.
- Trong thư mục
validators, tạo filehello_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
datumcó khớp với “HelloWorld”. - Kiểm tra
redeemercó 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).
- Kiểm tra
- Kết Quả: Trả về
truenếu tất cả điều kiện đều thỏa mãn, cho phép tiêu UTXO.
- Build Hợp Đồng:
- Chạy lệnh để biên dịch hợp đồng:
aiken build
- Kết quả tạo file
plutus.jsontrong thư mụcbuild, 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.
- Chạy lệnh để biên dịch hợp đồng:
Để 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:
- 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.jsonhoặ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.
- Aiken Documentation: Hướng dẫn cài đặt và viết hợp đồng.
- MeshJS Documentation: Hướng dẫn tích hợp hợp đồng với dApp.
- Blockfrost API Documentation: Endpoint để lấy UTXO.
- Cardano Developer Portal: Công cụ và tài liệu phát triển Cardano.
- Cardano Testnet Explorer: Kiểm tra giao dịch.
Bài viết đã giải thích mô hình eUTxO trên Cardano, vai trò của script, datum, 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
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.
- 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
- 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.
- Đầu vào: Alice sử dụng UTxO chứa 200 ADA.
- Đầ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).
- 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.
- 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.
- Đầ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.
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.
- 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
- 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). - 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.
- 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.
- Đầu vào: UTxO chứa 100 ADA và datum
- 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.
- 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.
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.
- 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
- Trong Aiken, một validator được định nghĩa để kiểm tra logic giao dịch.
- 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).
- Datum: Chuỗi
- Logic: So sánh redeemer với datum, trả về
truenếu chúng khớp. - Mã Aiken được viết trong file trong thư mục
validators.
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 buildsẽ biên dịch mã này thành fileplutus.jsonđể sử dụng trong giao dịch.
- Datum được lưu là
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.
- 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
- Để chuyển 70 ADA, Bob phải sử dụng cả hai UTxO vì không UTxO nào đủ 70 ADA.
- Đầu vào:
- UTxO 1: 60 ADA.
- UTxO 2: 20 ADA.
- Tổng: 60 + 20 = 80 ADA.
- Đầu ra:
- UTxO mới cho Charlie: 70 ADA.
- UTxO mới trả lại cho Bob: 80 – 70 = 10 ADA.
- Giao dịch được ký bởi Bob và đưa vào blockchain.
- Đầ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.
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.
- 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
- Đị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.
- 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.
- Nhận datum (
- 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.
- Đầu vào: UTxO chứa 30 ADA và datum
- 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.
- Validator so sánh datum (
- Để thực hành, bạn có thể cài đặt Aiken qua lệnh
npm install -g aikenhoặcbun 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
