
Bài 8: Viết Code Off-chain (PyCardano + Blockfrost)
Chào mừng các bạn đến với Bài 8 — bài viết mang tính chất “bản lề” và quan trọng nhất trong chuỗi series DApp AI + Blockchain này!
Ở các bài trước, chúng ta đã có Smart Contract tĩnh lặng nằm trên mạng (Bài 6), có vector khuôn mặt và IPFS CID đại diện cho danh tính (Bài 7). Hôm nay, chúng ta sẽ là người “nhạc trưởng”, viết mã Off-chain bằng Python (PyCardano) để kết nối tất cả lại, tương tác trực tiếp với Cardano Blockchain.
🎯 Mục Tiêu Bài Học
Kết thúc bài viết này, bạn sẽ nắm được:
-
Cách ánh xạ chính xác kiểu dữ liệu (PlutusData) giữa Aiken và Python.
-
Đọc file
plutus.jsonvà thiết lập Giao dịch Khóa (Lock TX) để gửi ADA kèm Datum lên Smart Contract. -
Kích hoạt mô hình CKV (Continuing Key Validation) bằng cách Mở khóa (Spend/Unlock) UTxO thông qua các hành động (Redeemer).
-
Nhận diện và vượt qua 2 cạm bẫy cực kỳ nguy hiểm khi code off-chain: Lỗi phí giao dịch và lỗi giải mã RawCBOR.
-
Chạy toàn bộ vòng đời của DID trên mạng thử nghiệm Preprod.
PlutusData Mapping: Ánh xạ Aiken ↔ Python
Điều đầu tiên và mang tính sống còn khi giao tiếp giữa off-chain và on-chain là: Cấu trúc dữ liệu Python phải khớp chính xác 100% với Aiken. Khi Python gửi một giao dịch, dữ liệu Datum sẽ được mã hóa sang chuẩn nhị phân CBOR. Nếu có bất kỳ sai lệch nào, Validator (Smart Contract) sẽ từ chối giao dịch ngay lập tức.
Hãy xem cách chúng ta định nghĩa DIDDatum bằng thư viện PyCardano:
from dataclasses import dataclass
from pycardano import PlutusData
@dataclass
class DIDDatum(PlutusData):
CONSTR_ID = 0
did_id: bytes
face_ipfs_hash: bytes
owner: bytes
created_at: int
verified: int # CỰC KỲ QUAN TRỌNG: 0 = chưa, 1 = đã verify
Cạm bẫy kiểu dữ liệu (Int vs Bool): Trong Aiken (Bài 6), chúng ta khai báo verified là kiểu Int. Ở Python, bạn BẮT BUỘC phải dùng kiểu int. Nếu bạn quen tay dùng bool (True/False), thư viện PyCardano sẽ mã hóa nó thành cấu trúc CBOR boolean – hoàn toàn khác biệt với số nguyên (Integer) trong mắt Aiken, dẫn đến lỗi từ chối giao dịch cực kỳ khó fix.
Tương tự, định nghĩa các Redeemer (Hành động) phải khớp vị trí CONSTR_ID đã quy định trong Aiken:
@dataclass
class Register(PlutusData): CONSTR_ID = 0 # Khớp vị trí 0
@dataclass
class Update(PlutusData): CONSTR_ID = 1 # Khớp vị trí 1
@dataclass
class Verify(PlutusData): CONSTR_ID = 2 # Khớp vị trí 2
@dataclass
class Revoke(PlutusData): CONSTR_ID = 3 # Khớp vị trí 3
Lock TX: Khởi tạo danh tính trên Blockchain
Bước đầu tiên trong vòng đời DID là Khóa (Lock): gửi 2 ADA kèm theo DIDDatum vào địa chỉ của Smart Contract.
Quy trình trong file lock_did.py gồm 3 bước:
-
Load Contract: Đọc file
plutus.json(sinh ra từ Bài 6), chuyển mã hex thành đối tượngPlutusV3Scriptvà lấy địa chỉ hợp đồng. -
Setup Wallet: Sử dụng 24 từ khóa (mnemonic) để khôi phục ví HD Wallet và tạo các cặp Signing Key, Verification Key.
-
Build & Submit:
# Tạo DID ID duy nhất bằng cách băm SHA256 mã IPFS CID
did_id = f"did:cardano:{hashlib.sha256(ipfs_hash.encode()).hexdigest()[:16]}"
datum = DIDDatum(
did_id=did_id.encode("utf-8"),
face_ipfs_hash=ipfs_hash.encode("utf-8"),
owner=bytes(payment_vkey.hash()), # Băm public key (28 bytes)
created_at=int(time.time() * 1000), # Thời gian hiện tại
verified=0, # Trạng thái mặc định: Chưa xác minh
)
builder = TransactionBuilder(context)
builder.add_input_address(sender_address) # Thêm UTxO từ ví để trả phí
builder.add_output(TransactionOutput(
address=script_address,
amount=Value(2_000_000), # Khóa 2 ADA
datum=datum, # Đính kèm Inline Datum
))
# Ký và gửi giao dịch...
Spend TX: Thao tác với Cỗ máy trạng thái (CKV)
Sau khi DID đã được khóa lên mạng, chúng ta cần thay đổi trạng thái của nó (Register, Verify, Revoke). Các logic này được nhóm vào class DIDManager.
Hãy cùng phân tích hành động Register – một ví dụ CKV kinh điển:
def register(self, lock_tx_hash: str) -> str:
target = self._find_utxo(lock_tx_hash) # Tìm UTxO cần thao tác
builder = TransactionBuilder(self.context)
# BẪY SỐ 1: PHẢI CÓ DÒNG NÀY ĐỂ TRẢ PHÍ GIAO DỊCH
builder.add_input_address(self.address)
# Chi tiêu UTxO từ Script kèm theo Redeemer "Register"
builder.add_script_input(target, self.script, Redeemer(Register()))
builder.required_signers = [self.pay_vkey.hash()]
# CKV: Tạo một Output quay lại Script, giữ nguyên Datum cũ
builder.add_output(TransactionOutput(
self.script_address,
Value(target.output.amount.coin),
datum=target.output.datum,
))
Cạm bẫy số 1 – Phí giao dịch (Fees): Khi bạn lấy tiền từ Smart Contract, lượng ADA đó (2 ADA) chỉ đủ để nạp lại vào hợp đồng (Continuing Output). Nhưng mạng lưới Cardano cần thu phí giao dịch (khoảng 0.3 – 0.5 ADA). Phí này phải lấy từ ví của bạn! Nếu quên hàm builder.add_input_address(self.address), TransactionBuilder sẽ không có tiền trả phí và báo lỗi “All UTxO selectors failed” – một lỗi cực kỳ mông lung nếu bạn không biết nguyên nhân này.
Cạm bẫy số 2: Giải mã RawCBOR
Khi Blockfrost trả về dữ liệu UTxO từ blockchain, thư viện PyCardano chưa biết định dạng thực sự của Datum là gì. Nó chỉ trả về những byte mã hóa thô (RawCBOR).
Nếu bạn truy cập thẳng vào trường dữ liệu, code sẽ sập ngay lập tức:
# SAI: Crash ngay!
input_datum = target.output.datum
print(input_datum.did_id) # Lỗi: 'RawCBOR' object has no attribute 'did_id'
Để giải quyết, khi thực hiện hành động Verify hoặc Update (những hành động cần đọc và thay đổi dữ liệu cũ để tạo Datum mới), bạn PHẢI giải mã nó:
# ĐÚNG: Giải mã RawCBOR thành cấu trúc DIDDatum
raw_datum = target.output.datum
input_datum = DIDDatum.from_cbor(raw_datum.cbor) # Giải mã tại đây
# Tạo Datum mới cho hành động Verify (0 -> 1)
output_datum = DIDDatum(
did_id=input_datum.did_id,
face_ipfs_hash=input_datum.face_ipfs_hash,
owner=input_datum.owner,
created_at=input_datum.created_at,
verified=1, # Thay đổi trạng thái xác minh
)
Lưu ý: Với hành động Register (chỉ copy nguyên xi) hoặc Revoke (xóa bỏ, không tạo output), bạn có thể bỏ qua bước giải mã này.
Demo Thực Tế: Vòng Đời Hoàn Chỉnh
Bây giờ chúng ta sẽ chạy full vòng đời trên Terminal.
Bước 1: Khóa (Lock) một DID mới bằng mã IPFS CID (từ bài 7)
python lock_did.py --ipfs-hash QmXLaBYop7bGLQ2uWtDUo5tk7niVDLdKpLTRfULAAwp6gz
Kết quả: Script sẽ tạo 1 giao dịch khóa 2 ADA lên mạng. Lấy mã TX_HASH nhận được để dùng cho bước sau.
Bước 2-4: Thực hiện Register, Verify và Revoke lần lượt:
# Register (Bắt đầu sử dụng)
python did_operations.py --action register --tx-hash <TX_HASH_TỪ_BƯỚC_1>
# Verify (Thay đổi verified: 0 -> 1)
python did_operations.py --action verify --tx-hash <TX_HASH_TỪ_BƯỚC_REGISTER>
# Revoke (Hủy bỏ DID, lấy lại 2 ADA)
python did_operations.py --action revoke --tx-hash <TX_HASH_TỪ_BƯỚC_VERIFY>
Tất cả 4 giao dịch (Lock → Register → Verify → Revoke) sẽ được ghi nhận thành công trên mạng Cardano Preprod Testnet!
Tổng Kết Bài 8
Chúc mừng bạn đã hoàn thành bài viết kỹ thuật nặng nhất của chương trình. Hãy ghi nhớ 3 cạm bẫy tử thần khi code Off-chain:
-
Int vs Bool: Phải giữ tính nhất quán về kiểu dữ liệu khi thiết kế Datum.
-
add_input_address(): Luôn cung cấp UTxO của ví cá nhân để trả phí mạng khi chi tiêu từ hợp đồng. -
from_cbor(): Luôn giải mã RawCBOR trước khi muốn thay đổi trường dữ liệu.
Đến đây, ứng dụng của chúng ta đã vận hành hoàn hảo, nhưng… chỉ dưới dạng lệnh dòng (CLI). Ở bài tiếp theo – Bài 9, chúng ta sẽ bọc tất cả những đoạn code Python này thành một REST API bằng FastAPI để frontend có thể giao tiếp dễ dàng!
Hẹn gặp lại các bạn trong bài viết tới!
Hẹn gặp lại các bạn trong bài viết tiếp theo! Chúc các bạn code vui vẻ!
Chi tiết về source code toàn bộ bài học các bạn có thể tham khảo tại!!!
Pycardano integration with AI Implementation Example
