Skip to main content

Aiken dành cho người nghiệp dư

Sau vài tuần nghiên cứu về Cardano, tôi vẫn loay hoay hình dung những gì mình có thể xây dựng bằng hợp đồng thông minh. Mối quan hệ giữa chính sách đúc tiền và trình xác thực chi tiêu không rõ ràng, cầu nối giữa Aiken và Lucid có vẻ phức tạp và tệ nhất là tôi không thể tìm ra nơi để giữ trạng thái ứng dụng của mình! Đây là nỗ lực của tôi trong phần giới thiệu mà tôi rất muốn có.

Điều kiện tiên quyết

Bài viết này giả sử bạn đã hoàn thành hướng dẫn Aiken “Hello, World” và đã đọc tài liệu dành cho nhà phát triển Cardano. Cần có sự quen thuộc với token gốc, ví, địa chỉ và cơ sở hạ tầng off-chain (ví dụ: chỉ mục chuỗi). Tất cả các ví dụ bên trong là Deno + TypeScript + Lucid + Aiken. Môi trường của bạn phải được cấu hình để hỗ trợ điều đó.

Một số nội dung nhập và bản soạn sẵn đã bị xóa khỏi các ví dụ bên dưới. Các ví dụ đầy đủ, có thể chạy được có sẵn trong kho đi kèm với bài viết này.

Hợp đồng thông minh

Hợp đồng thông minh trên Cardano là trình xác thực. Hợp đồng thông minh không thể tự mình thực hiện bất kỳ hành động nào; nó chỉ có thể phê duyệt hoặc từ chối các giao dịch được đề xuất. Hợp đồng thông minh không thể “gửi token” hoặc “gọi một hợp đồng khác”. Hợp đồng phải xem xét nội tâm giao dịch đến và xác minh rằng nó thực hiện những điều đó. Có bốn loại hợp đồng, nhưng chúng ta sẽ tập trung vào hai loại hỗ trợ các ứng dụng phi tập trung nhất: chính sách đúc tiền và trình xác thực chi tiêu.

  • Chính sách đúc tiền : Nếu một giao dịch có ý định đúc token mới hoặc đốt token hiện có thì giao dịch đó cần có sự chấp thuận của chính sách đúc token đó để thực hiện điều đó.
  • Trình xác thực chi tiêu : Nếu một giao dịch có ý định chi tiêu token từ một địa chỉ hợp đồng, thì giao dịch đó cần có sự chấp thuận của trình xác thực chi tiêu của địa chỉ đó.

Một hợp đồng được xác định bằng hàm băm của chính nó. Trong trường hợp chính sách đúc, hàm băm của mã của hợp đồng là id chính sách của token gốc mà nó tạo ra. Nếu bạn có token có id chính sách cụ thể, nó được đảm bảo đã được tạo ra bởi chính sách đúc mà id chính sách được lấy từ đó. Nếu mã của chính sách đúc tiền đó là bất kỳ thứ gì khác thì id chính sách kết quả sẽ khác và nó không thể tạo ra token “giống nhau”. Theo cách tương tự, đối với trình xác thực chi tiêu, hàm băm của mã hợp đồng (thực tế) là địa chỉ mà hợp đồng cư trú. Trong hướng dẫn Aiken “Hello, World”, việc tra cứu giao dịch kết quả trên một dịch vụ lập chỉ mục cho thấy điều này; đã có lịch sử giao dịch lâu dài tại địa chỉ hợp đồng bạn vừa “thực hiện” rồi! Cùng mã? Hợp đồng tương tự.

Hợp đồng có thể hạn chế trong việc “tra cứu” dữ liệu on-chain. Khi gửi giao dịch, gần như toàn bộ dữ liệu phải được người gọi cung cấp. Đối với một giao dịch, điều này bao gồm

  • Đầu vào : Đầu ra giao dịch chưa chi tiêu nào đang được chi tiêu?
  • Kết quả đầu ra : Những UTXO mới nào đang được tạo?
  • Mint : Những token nào đã được đúc và đốt?
  • Người ký : Ai đã ký nó?
  • Mốc thời gian : Dữ liệu được lưu trữ cùng với UTXO on-chain.
  • Redeemer : Các đối số cho chính sách đúc tiền hoặc Trình xác thực chi tiêu.

Lưu ý rằng cũng có metadata. Metadata là dữ liệu được lưu trữ on-chain không có sẵn trong hợp đồng. Một giao dịch có thể đặt metadata vào bất kỳ UTXO nào, nhưng hợp đồng không thể sử dụng metadata đó.

Mọi chính sách đúc tiền và trình xác thực chi tiêu đều có quyền truy cập vào các trường này. Một số có thể tự giải thích được, nhưng có hai điều đáng được quan tâm đặc biệt, mốc thời gianRedeemer.

Chính sách đúc tiền & Redeemers

Hãy giới thiệu các chính sách đúc tiền và Redeemer bằng một ví dụ. Đây là chính sách đúc tiền đơn giản nhất có thể có ở Aiken. chúng ta biết đó là chính sách đúc tiền vì nó chỉ yêu cầu Redeemer lại - không phải mốc thời gian.

    use aiken/transaction.{ScriptContext, Redeemer}

validator {
fn mint_my_cool_token(_Redeemer: Redeemer, _ctx: ScriptContext) -> Bool {
True
}
}

Trình xác thực này hoàn toàn bỏ qua Redeemer. Bởi vì nó luôn trả về True nên các token liên quan có thể được đúc và đốt một cách tự do! Để đúc một số token mới, chúng ta có thể tạo giao dịch với Lucid như thế này:

    //.. Setup lucid, select a wallet..

const assetName = `${ourMintingPolicyId}${fromText("COOL")}`
const tx = await lucid.newTx()
.attachMintingPolicy(ourMintingPolicy)
.mintAssets(
{
[assetName]: 10000000000n
},
Data.void()
)
.complete()

//.. Sign & submit transaction..

Việc gửi một giao dịch như vậy sẽ tạo ra 10 tỷ token có tên “COOL” theo id chính sách được lấy từ trình xác thực của chúng ta. Một số yếu tố đáng chú ý được yêu cầu ở đây.

  1. assetsName : Có thể có nhiều token có tên khác nhau theo một chính sách. Để xác định duy nhất một token cụ thể với một chính sách cụ thể, id chính sách được nối với tên token được mã hóa hex. Trong trường hợp này, chúng ta đang đúc các token có tên “COOL”.
  2. AttachMintingPolicy : Việc đính kèm toàn bộ nội dung của chính tập lệnh chính sách đúc là bắt buộc đối với mọi giao dịch đúc hoặc đốt token. Cố gắng đúc một nội dung sử dụng id chính sách mà không có chính sách đúc liên quan trong giao dịch sẽ dẫn đến lỗi.
  3. mintAssets : Đối số đầu tiên để đúc tài sản, một cách đơn giản, là bản đồ tên nội dung sẽ được đúc với số lượng đúc của nội dung đó. Đối số thứ hai là rederer , là tham số đầu tiên cho trình xác thực mà chúng ta đã xác định ở trên. Vì chính sách đúc tiền của chúng ta bỏ qua Redeemer nên chúng ta có thể gửi bất cứ thứ gì. Nhưng nếu không thì sao?

Hãy xem xét một hợp đồng yêu cầu người dùng biết một từ mã để đúc token. Điều này không an toàn lắm vì nội dung hợp đồng của bạn cũng như bản thân Redeemer đều không phải là bí mật. Tuy nhiên, một từ được đoán là một ví dụ tuyệt vời về loại dữ liệu mà chúng ta không thể dễ dàng truy cập trong chính sách đúc tiền độc lập nếu không có người đổi.

    type CoolTokenRedeemer {
guessed_word: ByteArray
}

validator {
fn mint_my_cool_token(Redeemer: CoolTokenRedeemer, _ctx: ScriptContext) -> Bool {
let code_word = "secret"
Redeemer.guessed_word == code_word
}
}

Bằng cách xác định loại cho Redeemer, chúng ta khai báo những gì trình xác thực mong đợi dưới dạng đầu vào: a guessed word dưới dạng a ByteArray. Trong Aiken, các chuỗi là ByteArray. Vì vậy cả loại và xác thực đều dễ dàng thực hiện ở đây. Nếu và chỉ khi Redeemer bao gồm một guessed_word trong số "secret", thì giao dịch được liên kết có thể đúc hoặc đốt bất kỳ số lượng token nào mà nó yêu cầu.

Mọi thứ trở nên kỳ lạ hơn với Lucid và TypeScript. Đây là cách triển khai một giao dịch được thực hiện từ hợp đồng đó.

    import { Data } from "https://deno.land/x/lucid@0.10.7/mod.ts"

const CoolTokenRedeemerSchema = Data.Object({
guessed_word: Data.Bytes()
})
type CoolTokenRedeemer = Data.Static<typeof CoolTokenRedeemer>
const CoolTokenRedeemer = CoolTokenRedeemerSchema as unknown as CoolTokenRedeemer

//.. Setup lucid..

const assetName = `${ourMintingPolicyId}${fromText("COOL")}`
const tx = await lucid.newTx()
.attachMintingPolicy(ourMintingPolicy)
.mintAssets(
{
[assetName]: 10000000000n
},
Data.to(
{ guessed_word: "secret" },
CoolTokenRedeemer
)
)
.complete()

//.. Sign & submit transaction..

Rất nhiều mã bổ sung để thêm Redeemer! Có một số mẫu soạn sẵn để biểu diễn các loại Aiken bằng Lucid và TypeScript, nhưng có lý do chính đáng. Nếu không sử dụng Lucid, các loại mà bạn đại diện trong Aiken sẽ phải được mã hóa dưới dạng JSON phân biệt thứ tự bằng các khóa không tên. Ví dụ của chúng ta CoolTokenRedeemer thực sự là thế này:

    {
"constructor": 0,
"fields": [{
"bytes": 736563726574
}]
}

Lucid cung cấp trình Data tạo lược đồ để đơn giản hóa việc xây dựng các cấu trúc JSON như vậy. Đáng chú ý, các kiểu TypeScript thực thi một số chuyển đổi dữ liệu nhất định, như chấp nhận chuỗi cho các trường byte. Chúng ta sẽ khám phá thêm Data, nhưng tôi khuyên bạn nên mở các thử nghiệm Dữ liệu trong Lucid làm tài liệu tham khảo khi lần đầu làm việc với nó.

Như chúng ta đã trình bày, Redeemer cung cấp cho người gọi khả năng phản ứng với trạng thái của hợp đồng. Thật vậy, cách duy nhất để thực hiện một chính sách đúc tiền hữu ích là làm cho hợp đồng trở nên có trạng thái. Trong trường hợp này, trạng thái là từ mã, "secret". Tuy nhiên, mã hóa cứng trạng thái của chúng ta không dẫn đến một hợp đồng linh hoạt. Làm thế nào để chúng ta có trạng thái động trong hợp đồng của mình?

Một cách bạn có thể cố gắng đạt được tính năng động này là tham số hóa chính Trình xác thực.

    type CoolTokenRedeemer {
guessed_word: ByteArray
}

validator(code_word: ByteArray) {
fn mint_my_cool_token(Redeemer: CoolTokenRedeemer, _ctx: ScriptContext) -> Bool {
Redeemer.guessed_word == code_word
}
}

Bằng cách này, bí mật được mã hóa cứng có thể được loại bỏ. Việc chạy aiken buildtạo ra plutus.jsontập lệnh của chúng ta trong đó và chúng ta có thể tham số hóa tập lệnh đó như sau:

    import blueprint from "./plutus.json" assert { type: "json" }
import {
applyDoubleCborEncoding,
applyParamsToScript
} from "https://deno.land/x/lucid@0.10.7/mod.ts"

const mintCoolToken = blueprint.validators.find(
v => v.title === "cool.mint_my_cool_token"
)!

const parameterizedMintCoolToken = {
type: "PlutusV2",
Script: applyDoubleCborEncoding(
applyParamsToScript(
guessMintingPolicy.Script, [fromText("new secret")]
)
)
}

const tx = await lucid.newTx()
.attachMintingPolicy(parameterizedMintCoolToken)
.mintAssets(
{
[assetName]: 10000000000n
},
Data.to(
{ guessed_word: fromText("new secret") },
CoolTokenRedeemer
)
)
.complete()

Trong trường hợp này, chúng ta tham số hóa trình xác thực từ TypeScript với bí mật là "new secret". Việc gọi hợp đồng cũng giống hệt nhau, bên cạnh bí mật được cập nhật.

Điều này thật thú vị vì giờ đây chúng ta có thể tạo các hợp đồng mới trong ứng dụng máy trạm của mình trong Runtimes. Mỗi người dùng có thể cung cấp một từ mã tùy chỉnh và nhận chính sách đúc tùy chỉnh chỉ hoạt động với từ họ đã chọn. Nhưng điều này có một lỗ hổng nghiêm trọng: token do mỗi người dùng tạo ra sẽ có id chính sách duy nhất. Chúng không giống nhau. Trong một số trường hợp nhất định, chẳng hạn như tạo thẻ quà tặng, điều đó có thể được mong muốn - chính sách tạo ra một thẻ quà tặng cho một khoản thanh toán duy nhất của một người dùng. Trong trường hợp của chúng ta, tất cả điều này có nghĩa là chúng ta chưa tiến gần hơn đến việc có trạng thái động! Chúng ta đang ở chính xác nơi chúng ta đã ở trước đây. Mỗi chính sách đúc tiền duy nhất có một từ mã được mã hóa cứng duy nhất; hiện tại chúng ta có nhiều chính sách.

và thực sự, làm cách nào để chúng ta lưu trữ trạng thái on-chain?

Trình xác thực và Datums chi tiêu

Giả sử chúng ta muốn tạo một chính sách đúc tiền chỉ cho phép một số lượng token nhất định được lưu hành. Giả sử nhiều nhất là 1.000.000 token. Khi đã đạt đến giới hạn đúc đó, chúng có thể bị đốt cháy, lúc đó bạn có thể đúc thêm. Làm thế nào chính sách đúc tiền có thể biết có bao nhiêu token đang được lưu hành? Thông tin này rất dễ bị lấy ra off-chain thông qua Block Frost hoặc cardano-db-sync , nhưng chỉ riêng chính sách đúc tiền thì không có cơ chế nào để chứng minh rằng giá trị được gửi từ các nguồn này không bị thao túng. Về mặt cơ học, chúng ta có thể chấp nhận nó như một giá trị Redeemer, nhưng điều đó thật vô nghĩa; người dùng gửi giao dịch có thể cung cấp bất kỳ giá trị nào và không có sự thật cơ bản nào để kiểm tra việc gửi đó.

Để làm được điều này, chúng ta cần trạng thái độngđược xác thực để có thể truy cập trong hợp đồng và có thể phục hồi trước các bản cập nhật độc hại. Trình xác thực chi tiêu, loại hợp đồng thứ hai của chúng ta, có được điều xa xỉ này thông qua dữ liệu.

Datums là dữ liệu tùy chọn, tùy ý được lưu trữ cùng với UTXO. Khi gửi nội dung đến một địa chỉ, tác giả giao dịch có thể đưa vào mỗi đầu ra bất kỳ dữ liệu nào và bất kỳ nội dung nào họ thích. Không có thứ gọi là trình xác thực “nhận”. Trình xác thực chi tiêu của chúng ta, như tên cho thấy, sẽ chỉ thực thi khi chi tiêu UTXO từ địa chỉ hợp đồng.

Vì một địa chỉ có thể có nhiều UTXO nên nó cũng có thể chứa nhiều Datum. Như vậy, đương nhiên không có một cơ sở dữ liệu nào chứa trạng thái global của địa chỉ hợp đồng. Mọi đầu ra chưa chi tiêu tại địa chỉ đều phải tuân theo các điều khoản chi tiêu do trình xác thực chi tiêu thực thi một cách độc lập.

Đây là một biến thể của ví dụ Hello World từ hướng dẫn Aiken. Trình xác thực chi tiêu này chấp nhận các UTXO có giá trị tùy ý chứa dữ liệu có owner khóa xác minh của và chỉ cho phép những UTXO đó được chi tiêu trong các giao dịch được ký bởi chính chủ sở hữu đó.

    use aiken/transaction.{
InlineDatum,
ScriptContext,
Redeemer,
Spend,
find_input
}
use aiken/list
use aiken/hash.{Blake2b_224, Hash}
use aiken/transaction/credential.{VerificationKey}

type VerificationKeyHash =
Hash<Blake2b_224, VerificationKey>

type OwnerDatum {
owner: VerificationKeyHash
}

validator {
fn only_for_owner(
_datum: Data,
_Redeemer: Redeemer,
ctx: ScriptContext
) -> Bool {
let ScriptContext { transaction, purpose } = ctx
expect Spend(spent_UTXO_reference) = purpose
expect Some(input) = find_input(
transaction.inputs,
spent_UTXO_reference
)

expect InlineDatum(maybe_owner_datum) = input.output.datum
expect owner_datum: OwnerDatum = maybe_owner_datum

list.has(transaction.extra_signatories, owner_datum.owner)
}
}

Việc hủy cấu trúc ScriptContext cho phép chúng ta truy cập vào một tệp Transaction. Tài liệu Aiken có mô tả đầy đủ về các trường có sẵn trong giao dịch , nhưng trong trường hợp này, chúng ta quan tâm đến inputsextra_signatories. Trình xác thực này tìm kiếm giao dịch inputsđể tìm UTXO đang được chi tiêu từ địa chỉ hợp đồng. Nó mong đợi một nội tuyến OwnerDatum tồn tại trên UTXO đó. Cuối cùng, nó kiểm tra xem giao dịch có được ký bởi chủ sở hữu được xác định trong dữ liệu hay không.

Hãy khóa tiền vào hợp đồng.

    import { Data } from "https://deno.land/x/lucid@0.10.7/mod.ts"

//.. Setup Lucid, select a wallet..
const owner = lucid.utils.getAddressDetails(
recipientAddress
).paymentCredential!.hash
const datum: OwnerDatum = { owner }

const tx = await lucid.newTx()
.payToAddressWithData(
contractAddress,
{
inline: Data.to(datum, OwnerDatum)
},
{
lovelace: 50000000n,
}
)
.complete()

Đây là lệnh gọi tới payToAddressWithData. Giao dịch của chúng ta có một thao tác duy nhất: thanh toán 50 ADA vào địa chỉ hợp đồng. Điều quan trọng là giao dịch này bao gồm dữ liệu hợp lệ. Hợp đồng sẽ không được thực thi cho đến khi hết UTXO. Vì vậy nội dung của lần gửi này hoàn toàn không được xác thực. Nó sẽ Always-Success, ngay cả khi dữ liệu có sai sót! Để đơn giản, chúng ta đang sử dụng cùng một ví để lưu trữ và truy xuất ADA, nhưng khoản tiền gửi này có thể đến từ bất kỳ ai và họ có thể đặt bất kỳ chủ sở hữu nào vào dữ liệu.

Cũng lưu ý rằng đây là một mốc thời gian nội tuyến. Điều này có nghĩa là toàn bộ nội dung của dữ liệu được lưu trữ on-chain chứ không là hàm băm. chúng ta sẽ sử dụng dữ liệu nội tuyến để thuận tiện nhưng hãy nhớ rằng chúng sẽ tăng quy mô giao dịch khi lượng dữ liệu chúng lưu trữ tăng lên. Đọc thêm ở đây.

Bây giờ chúng ta phải chi 50 ADA mà chúng ta đã đưa vào hợp đồng. Để làm như vậy, hãy tìm kiếm UTXO tại địa chỉ hợp đồng để tìm khoản tiền gửi vừa được thực hiện, sau đó chỉ định UTXO đó làm đầu vào cho giao dịch với collectFrom. Vì đây là chi tiêu từ địa chỉ tập lệnh nên Redeemer phải được cung cấp cho collectFrom. Nếu không làm như vậy sẽ dẫn đến một sự nhầm lẫn Error: Missing Script witness. Giá trị của Redeemer không liên quan trong trình xác thực của chúng ta. Vì vậy chúng ta có thể chuyển Data.void() dưới dạng đầu vào của Redeemer trống.

    const contractUTXOs = await lucid.UTXOsAt(contractAddress)
const depositUTXO = contractUTXOs.find(
txo => txo.txHash === depositTxHash
)!

const withdrawlTx = await lucid.newTx()
.collectFrom(
[depositUTXO],
Data.void()
)
.attachSpendingValidator(ownerValidator)
.addSigner(recipientAddress)
.complete()

const withdrawlSigned = await withdrawlTx.sign().complete()
const withdrawlTxHash = await withdrawlSigned.submit()

Giống như chính sách đúc tiền, toàn bộ trình xác thực chi tiêu cần được đưa vào giao dịch thông qua attachSpendingValidator. Ngoài ra, yêu cầu về người ký kết phải được chỉ định rõ ràng trong giao dịch thông qua addSigner. Không giống như chi tiêu thông thường từ một địa chỉ ví, owner Lucid không thể suy ra sự phụ thuộc của hợp đồng vào chữ ký.

Vậy là tiền đã được lấy thành công từ hợp đồng. Điều này có nghĩa là UTXO chứa chúng ta OwnerDatum đã được sử dụng và dữ liệu đó không còn có thể truy cập được trong hợp đồng nữa. Nó có thể được truy xuất off-chain, nhưng chỉ những đầu ra chưa được sử dụng mới có thể là đầu vào cho các giao dịch. Vậy làm thế nào để duy trì trạng thái on-chain mà không khiến UTXO không thể sử dụng được?

Hãy thực hiện một hợp đồng đếm để chứng minh. Hợp đồng sẽ thực thi rằng đối với mỗi khoản chi tiêu từ địa chỉ đếm, sẽ có một đầu ra mới quay lại địa làm tăng số lượng.

    validator {
type CountDatum {
owner: VerificationKeyHash,
count: mạng thử nghiệm
}

fn count(
_datum: Data,
_Redeemer: Redeemer,
ctx: ScriptContext
) -> Bool {
let ScriptContext { transaction, purpose } = ctx
expect Spend(spent_UTXO_reference) = purpose
expect Some(input) = find_input(
transaction.inputs,
spent_UTXO_reference
)

expect InlineDatum(maybe_old_count_datum) = input.output.datum
expect old_count_datum: CountDatum = maybe_old_count_datum

let count_Script_address = input.output.address

expect Some(output) = transaction.outputs
|> list.filter(fn (output) {
output.address == count_Script_address
})
|> list.head()

expect InlineDatum(maybe_new_count_datum) = output.datum
expect new_count_datum: CountDatum = maybe_new_count_datum

and {
list.has(transaction.extra_signatories, old_count_datum.owner),
new_count_datum.count == old_count_datum.count + 1,
new_count_datum.owner == old_count_datum.owner
}
}
}

Không có gì phức tạp đang xảy ra ở đây. Trình xác thực truy xuất dữ liệu cũ, truy xuất dữ liệu đã cập nhật, sau đó so sánh chúng để đảm bảo rằng bản cập nhật là phù hợp.

Điều này rất giống với Trình xác thực chi tiêu trước đó; chúng ta vừa thêm một yêu cầu bổ sung thông qua counttrường dữ liệu. Ngoài việc có quyền sử dụng UTXO tại địa chỉ tập lệnh đếm, người gọi phải bao gồm một đầu ra quay lại cùng một địa chỉ bao gồm dữ liệu tăng dần thích hợp. Để đơn giản, hợp đồng giả định rằng chỉ có một đầu ra tập lệnh duy nhất (một số lần cập nhật duy nhất), nhưng nó có thể được viết để hỗ trợ nhiều tập lệnh cùng một lúc.

Việc soạn thảo giao dịch ban đầu không khác gì so với trước đây. Chỉ cần cập nhật loại dữ liệu.

    const owner = lucid.utils.getAddressDetails(
recipientAddress
).paymentCredential!.hash
const count = 0n
const originalDatum: CountDatum = { owner, count }

const depositTx = await lucid.newTx()
.payToAddressWithData(
contractAddress,
{
inline: Data.to(originalDatum, CountDatum)
},
{}
)
.complete()

Vì giá trị tài sản được lưu trữ ở đây không liên quan nên có thể bỏ qua. Lucid sẽ tự động tính toán số lượng ADA tối thiểu cần thiết và thêm nó vào đầu ra giao dịch từ ví hiện được định cấu hình.

Chi tiêu tương tự, nhưng không giống với trình xác thực chi tiêu trước đó. Lần này hợp đồng yêu cầu chi tiêu trở lại hợp đồng với mốc thời gian được cập nhật. Và ngoài việc collectFrombắt đầu chi tiêu, chúng ta phải đưa thông số payToAddressWithDataquay lại hợp đồng vào hợp đồng.

    const contractUTXOs = await lucid.UTXOsAt(contractAddress)
const depositUTXO = contractUTXOs.find(
txo => txo.txHash === depositTxHash
)!

const updatedDatum: CountDatum = { owner, count: count + 1n }

const withdrawlTx = await lucid.newTx()
.collectFrom(
[depositUTXO],
Data.void()
)
.attachSpendingValidator(countValidator)
.payToAddressWithData(
contractAddress,
{ inline: Data.to(updatedDatum, CountDatum)},
{}
)
.addSigner(recipientAddress)
.complete()

Thành công! Hãy kiểm tra kết quả.

    const countUTXOs = await lucid.UTXOsAt(contractAddress)
console.log(JSON.stringify(countUTXOs, null, 2))

Điều đó cho chúng ta một mốc thời gian d8799f581cac8f9db1a45ce3ed263aac3fa022e82705d190e3e31dd963ee295a4701ff. Việc cắm nó vào bộ giải mã dữ liệu sẽ cho chúng ta dữ liệu mong đợi. count, trường thứ hai, đã tăng thêm 1.

    {
constructor: 0,
fields: [
{
bytes: "ac8f9db1a45ce3ed263aac3fa022e82705d190e3e31dd963ee295a47"
},
{
mạng thử nghiệm: 1
}
]
}

Để đảm bảo trình xác thực hoạt động bình thường, chúng ta có thể thử cập nhật dữ liệu không chính xác, như sau:

    const updatedDatum: CountDatum = { owner, count: count + 2n }

Điều đó dẫn đến một thất bại dự kiến.

    Uncaught (in promise) "Redeemer (Spend, 0): 
The provided Plutus code called 'error'

Bây giờ chúng ta đã xác thực, trạng thái on-chain liên tục mà chúng ta có thể sử dụng trong hợp đồng! Điều đó thật thú vị, nhưng…

Tất cả cùng nhau bây giờ

Thật không may, có một lỗ hổng rõ ràng trong cách tiếp cận của chúng ta. Điều gì sẽ xảy ra nếu giao dịch đầu tiên được gửi đến địa chỉ tập lệnh đếm trông như thế này?

    const count = 9999999n
const datum: CountDatum = { owner, count }

const depositTx = await lucid.newTx()
.payToAddressWithData(
contractAddress,
{
inline: Data.to(datum, CountDatum)
},
{}
)
.complete()

Vì lần gửi đầu tiên tới địa chỉ hợp đồng này không kích hoạt trình xác thực chi tiêu nên chúng ta không thể đảm bảo về tính hoàn tất của nó! Người gọi có thể dễ dàng giả mạo số lượng cao tùy ý. Nếu chúng ta đang cố gắng xây dựng một giao thức không cần sự tin cậy từ các hợp đồng của mình thì đây là một vấn đề. Làm cách nào để xác thực đầu ra "mới" cho địa chỉ hợp đồng?

Một đặc tính thú vị của chính sách đúc tiền là chúng tạo ra UTXO mà không tiêu tốn bất kỳ đầu vào nào. Điều này có nghĩa là nếu chúng ta có thể chứng minh một UTXO đã được tạo ra với sự cho phép của một chính sách đúc tiền cụ thể, thì chúng ta có thể biết - một cách đảm bảo và không cần tin cậy - rằng trạng thái ban đầu của dữ liệu trên UTXO đó là hợp lệ. “Bằng chứng” này có thể được cung cấp dưới dạng NFT được đúc và đặt trên UTXO. Chính sách đúc tiền sẽ xác thực rằng NFT chỉ được tạo tại địa chỉ tập lệnh đếm của chúng ta và hợp đồng đếm sẽ xác minh rằng NFT không bao giờ được chi tiêu đến một địa chỉ khác ngoài địa chỉ của hợp đồng. Bằng cách này, chúng ta có thể tin tưởng rằng bất kỳ đầu ra giao dịch nào có chứa NFT “ủy quyền” này đều được hình thành theo các quy tắc trong hợp đồng của chúng ta.

Hãy chia nhỏ nó ra và thực hiện nó. Hãy nhớ kiểm tra mã đầy đủ cho tập lệnhtrình xác thực nếu cần. Có ba bước kiểm tra logic cốt lõi mà chính sách đúc tiền phải thực hiện để đảm bảo tính toàn vẹn của cả NFT ủy quyền và chính dữ liệu đếm.

  1. Kiểm tra xem giao dịch này không sử dụng mốc thời gian đếm hiện có. Đây không phải là hạn chế kỹ thuật; có thể cập nhật cả hai mốc thời gian đếm hiện có trong cùng một giao dịch mà giao dịch mới được tạo. Đây là một sự đơn giản hóa mà chúng ta đang thực hiện để giảm bớt sự phức tạp của hợp đồng.
  2. Kiểm tra xem mỗi UTXO mới được tạo có chứa dữ liệu được tạo đúng không.
  3. Đảm bảo chỉ có một NFT được tạo ra cho mỗi mốc đếm mới. Nếu có thể đúc thêm NFT, thì trong các giao dịch trong tương lai, chúng có thể được gửi đến các địa chỉ tùy ý mà không bị chính sách đúc tiền kiểm tra. Điều này sẽ cho phép giả mạo dữ liệu bằng cách chi NFT vào hợp đồng đếm mà không ứng dụng chính sách đúc tiền!

Đây là một phác thảo về Trình xác thực của chúng ta.

    type CountDatum {
owner: VerificationKeyHash,
authorizing_policy: ScriptHash,
count: mạng thử nghiệm
}

validator(count_Script_hash: ByteArray) {
fn count_authorizer(
_Redeemer: Redeemer,
ctx: ScriptContext
) {
let ScriptContext { transaction, purpose } = ctx
let Transaction { inputs, outputs, mint, } = transaction
expect Mint(policy_id) = purpose

let authorizing_token_name = "COUNT"

expect no_count_data_in_inputs(inputs, count_Script_hash)

let new_count_outputs = find_Script_outputs(outputs, count_Script_hash)

expect no_invalid_count_data(
transaction,
new_count_outputs,
authorizing_token_name,
policy_id
)

let num_minted = mint
|> value.from_minted_value
|> quantity_of(policy_id, authorizing_token_name)

list.length(new_count_outputs) == num_minted
}
}

Có một trường mới trong dữ liệu của chúng ta: authorizing_policy. Bởi vì chính sách đúc này được tham số hóa bởi count_Script_hash, nên chúng ta không thể tham số hóa chính tập lệnh đếm bằng id chính sách đúc. Làm như vậy sẽ tạo ra sự phụ thuộc vòng tròn. Để xử lý việc đó, giá trị này policy_idđược gán cho một trường của mốc thời gian và được xác thực trong giao dịch đúc tiền. Điều đó policy_idsau đó sẽ được sử dụng trong trình xác thực chi tiêu.

Kiểm tra số 1 và số 2 đã được tính vào các chức năng no_count_data_in_inputsno_invalid_count_datatương ứng.

Đối với cái trước, chúng ta có thể tận dụng find_Script_outputs. Đưa ra một danh sách các kết quả đầu ra, hàm này sẽ chỉ trả về các kết quả đầu ra cho một tập lệnh cụ thể. Nếu danh sách đó trống, chúng ta có thể chắc chắn rằng giao dịch này không bao gồm bất kỳ khoản chi tiêu nào từ địa chỉ tập lệnh đếm.

    fn no_count_data_in_inputs(
inputs: List<Input>,
count_Script_hash: ScriptHash
) -> Bool {
list.map(inputs, fn (input){
input.output
})
|> find_Script_outputs(count_Script_hash)
|> list.is_empty()
}

Sau đó, đầu ra của địa chỉ tập lệnh đếm phải được xác minh bằng no_invalid_count_data. Có một số điều kiện để kiểm tra.

    fn no_invalid_count_data(
transaction: Transaction,
Script_outputs: List<Output>,
authorizing_token_name: ByteArray,
policy_id: ScriptHash
) -> Bool {
list.all(Script_outputs, fn (output) {
expect InlineDatum(maybe_new_count_datum) = output.datum

expect new_count_datum: CountDatum = maybe_new_count_datum

let has_exactly_one_authorizing_nft =
1 == quantity_of(output.value, policy_id, authorizing_token_name)

and {
list.has(transaction.extra_signatories, new_count_datum.owner),
has_exactly_one_authorizing_nft,
new_count_datum.count == 0,
new_count_datum.authorizing_policy == policy_id
}
})
}

Điều này kiểm tra rằng 1. Mốc có hình dạng phù hợp 2. Có một và chỉ một NFT từ chính sách đúc tiền này trong mỗi đầu ra 3. Mỗi đầu ra được ký bởi chủ sở hữu của nó 4. Mốc có các giá trị phù hợp cho mốc đếm mới (tức là 0)

Kiểm tra cuối cùng của chúng ta xác nhận rằng không có NFT bổ sung nào từ chính sách này được tạo ra. Các lần kiểm tra trước đây của chúng ta đảm bảo rằng không có nhiều hơn một NFT trên mỗi đầu ra và không có NFT tồn tại trước đó trong đầu vào. Vì vậy ở đây việc kiểm tra xem số lượng được đúc có bằng với số lượng đầu ra của tập lệnh hay không là đủ.

    let num_minted = mint 
|> value.from_minted_value
|> quantity_of(policy_id, authorizing_token_name)

list.length(new_count_outputs) == num_minted

Với chính sách đúc tiền được xác định, có thể tạo một UTXO tại địa chỉ hợp đồng đếm với dữ liệu ban đầu được xác thực của chúng ta. Lưu ý rằng ngoài việc đúc NFT ủy quyền, NFT phải được thanh toán rõ ràng đến địa chỉ hợp đồng.

    const countMintingPolicyId = lucid.utils.validatorToScriptHash(parameterizedCountMintingPolicy)
const contractAddress = lucid.utils.validatorToAddress(secureCountValidator)

const count = 0n
const originalDatum: CountDatum = {
owner,
count,
authorizing_policy: countMintingPolicyId
}

const authorizingNFTName = `${countMintingPolicyId}${fromText("COUNT")}`

const initializingTx = await lucid.newTx()
.attachMintingPolicy(parameterizedCountMintingPolicy)
.mintAssets(
{
[authorizingNFTName]: 1n,
},
Data.void()
)
.payToAddressWithData(
contractAddress,
{
inline: Data.to(originalDatum, CountDatum)
},
{
[authorizingNFTName]: 1n
}
)
.addSigner(recipientAddress)
.complete()

Bây giờ, trạng thái dữ liệu ban đầu của chúng ta cho trình xác thực đếm được đảm bảo bởi chính sách đúc tiền. Quay lại trình xác thực số lượng chi tiêu, cần có hai lần kiểm tra bổ sung.

  1. Đầu vào được sử dụng từ địa chỉ tập lệnh đếm chứa NFT ủy quyền.
  2. Đầu ra được tạo với số lượng cập nhật cũng chứa NFT ủy quyền.

Cả hai kiểm tra đều có cùng một logic:

    fn output_has_authorizing_nft(
output: Output,
authorizing_policy: ScriptHash,
) {
let authorizing_token_name = "COUNT"

1 == quantity_of(
output.value,
authorizing_policy,
authorizing_token_name
)
}

Bằng cách sử dụng điều này, chúng ta có thể thêm một số xác nhận mới vào counttrình xác thực chi tiêu…

    validator {
fn count(
_datum: Data,
_Redeemer: Redeemer,
ctx: ScriptContext
) -> Bool {
--.. Snip..

expect output_has_authorizing_nft(
input.output,
old_count_datum.authorizing_policy
)

--.. Snip..

expect output_has_authorizing_nft(
output,
old_count_datum.authorizing_policy
)

and {
--.. Snip..
new_count_datum.authorizing_policy == old_count_datum.authorizing_policy
}
}
}

Đây là mối liên kết giữa Trình xác thực chi tiêu và chính sách đúc tiền của chúng ta. Bằng cách xác minh chính xác 1 NFT ủy quyền luôn có trong UTXO lưu trữ Datum đếm, chúng ta có thể tin tưởng vào tính toàn vẹn của Datum.

Với chính sách đúc tiền cho NFT ủy quyền được ứng dụng và trình xác thực chi tiêu được cập nhật, giờ đây chúng ta có thể có một cơ sở dữ liệu đếm trạng thái được đảm bảo tuân theo các quy tắc mà chúng ta đã xác định! Hãy thực hiện cập nhật trên trình xác thực số lượng mới của chúng ta để đảm bảo.

    const contractUTXOs = await lucid.UTXOsAt(contractAddress)
const countUTXO = contractUTXOs.find(txo => txo.txHash === initializingTxHash)!

const updatedDatum: CountDatum = {
..originalDatum,
count: originalDatum.count + 1n
}

const updateCountTx = await lucid.newTx()
.collectFrom(
[countUTXO],
Data.void()
)
.attachSpendingValidator(secureCountValidator)
.payToAddressWithData(
contractAddress,
{ inline: Data.to(updatedDatum, CountDatum)},
{
[authorizingNFTName]: 1n
}
)
.addSigner(recipientAddress)
.complete()

Không có gì trong giao dịch này mà chúng ta chưa từng thực hiện trước đây. Quan trọng nhất, các đầu ra bao gồm việc chuyển tiếp NFT ủy quyền ban đầu.

Cuối cùng, kết quả có thể được xác minh:

    const countUTXOs = await lucid.UTXOsAt(contractAddress)
console.log(JSON.stringify(countUTXOs, null, 2))

Dữ liệu kết quả giải mã xác nhận rằng trình xác thực số lượng chi tiêu được cập nhật của chúng ta hoạt động! Bây giờ, nếu các hợp đồng khác muốn tận dụng giá trị “đếm” này, họ có thể làm như vậy vì biết rằng tính toàn vẹn của nó sẽ vẫn nguyên vẹn.

Cái gì tiếp theo?

Bây giờ chúng ta đã hiểu được mối quan hệ giữa các chính sách đúc tiền và trình xác thực chi tiêu, chúng ta có thể sử dụng những nguyên tắc cơ bản này để bắt đầu soạn thảo các giao thức phi tập trung, không cần tin cậy trên Cardano! Vui lòng liên hệ với tôi trên GitHub hoặc Discord nếu nội dung này hữu ích cho bạn hoặc bạn muốn đọc thêm các bài đăng tương tự. Có chủ đề Aiken hoặc Cardano nào khác mà bạn muốn khám phá không? Cho tôi biết!


Bài tập về nhà / Thức ăn cho suy nghĩ:

  1. Trình xác thực số lượng chi tiêu giả định rằng chỉ có một số lượng duy nhất được cập nhật. Sửa đổi Trình xác thực và giao dịch gọi nó để hỗ trợ cập nhật nhiều dữ liệu đếm cùng một lúc.
  2. Chính sách đúc tiền cho các NFT ủy quyền được sử dụng bởi trình xác thực chi tiêu số lượng không cho phép cập nhật dữ liệu số lượng hiện có trong các giao dịch bao gồm việc đúc tiền. Sửa đổi Trình xác thực để hỗ trợ thực hiện cả hai trong một giao dịch. Bạn cần thay đổi điều gì để có thể xác định được đầu ra nào thuộc về đầu vào nào (nếu có)?
  3. Có những lựa chọn thay thế nào, nếu có, cho chương trình “ủy quyền NFT”? Làm cách nào khác để chúng ta có thể nhận được dữ liệu đáng tin cậy on-chain?

Nguồn bài viết tại đây


Picture