樣式:見證 (Pattern: Witness)
見證是一種透過建構證據來證明事實的樣式。在程式設計的情境中,見證是一種透過提供一個值來證明系統某項屬性的方法,而該值只有在該屬性成立時才能被建構出來。
Move 中的見證 (Witness in Move)
在 Struct 章節中,我們展示過一個 struct 只能由定義它的模組建立——或稱為 打包(packed)。因此,在 Move 中,模組透過建構型別來證明對該型別的擁有權。這是 Move 中最重要的樣式之一,廣泛應用於泛型型別實例化與授權。
實務上,要使用見證,必須有一個函式預期接收見證作為引數。在下面的範例中,new 函式預期接收一個 T 型別的見證,以建立 Instance<T>。
見證通常會被捨棄而非儲存,這也是為什麼此類函式通常要求見證型別具備 drop 能力的原因。
module book::witness;
/// A struct that can only be created with a witness of `T`.
public struct Instance<phantom T> has drop {}
/// Create a new `Instance<T>` with the provided witness. The witness is
/// discarded after use.
public fun new<T: drop>(_witness: T): Instance<T> {
Instance {}
}
建構 Instance<T> 的唯一方法是以型別 T 的實例呼叫 new 函式。這是 Move 中見證樣式的基本範例。提供見證的模組通常會有對應的實作,如下面的 book::witness_source 模組:
module book::witness_source;
use book::witness::{Self, Instance};
/// A struct used as a witness - canonically, an empty struct with `drop`.
public struct W has drop {}
/// Create a new instance of `Instance<W>`.
public fun new_instance(): Instance<W> {
witness::new(W {})
}
struct W 的實例被傳入 new_instance 函式以建立 Instance<W>,藉此證明模組 book::witness_source 擁有型別 W。
實例化泛型型別 (Instantiating a Generic Type)
見證讓泛型型別可以用具體型別實例化。這對於從該型別繼承相關行為並在模組提供能力時選擇性擴充它們,非常有用。
module sui::balance;
/// 一種 T 的 Supply。用於鑄造與銷毀。
/// 在 `Coin` 模組中被包裝成 `TreasuryCap`。
public struct Supply<phantom T> has store {
value: u64,
}
/// 為型別 T 建立一個新的 supply。
public fun create_supply<T: drop>(_: T): Supply<T> {
Supply { value: 0 }
}
/// 取得 `Supply` 的值。
public fun supply_value<T>(supply: &Supply<T>): u64 {
supply.value
}
在上面借用自 Sui Framework 的 balance 模組 的範例中,Supply 是一個泛型 struct,只能透過提供型別 T 的見證來建構。見證是以值的方式取得並被 捨棄——因此 T 必須具備 drop 能力。
實例化後的 Supply<T> 接著可用來鑄造新的 Balance<T>,其中 T 即為該 supply 的型別。
module sui::balance;
const EOverflow: u64 = 1;
/// 可儲存的 balance——Coin 型別的內部結構。
/// 可用於儲存不需要 key ability 的 coin。
public struct Balance<phantom T> has store {
value: u64,
}
/// 將 supply 增加 `value`,並以此值建立一個新的 `Balance<T>`。
public fun increase_supply<T>(self: &mut Supply<T>, value: u64): Balance<T> {
assert!(value <= (std::u64::max_value!() - self.value), EOverflow);
self.value = self.value + value;
Balance { value }
}
這就是 Sui 上通常建立新貨幣的方式:本章前面介紹過的 TreasuryCap——一種 capability——是 Supply<T> 的包裝器,以見證實例化而成。
使用見證進行授權 (Authorization with Witness)
實例化型別並非見證的唯一用途:任何函式都可以要求見證,使得該呼叫只對定義 T 的模組可用。下面的模組實作了一個泛型 RegulatedCoin,其中特權操作——mint、burn 與 transfer——需要見證,而共用功能——join——則對所有人開放:
/// A custom RegulatedCoin type with implementable functions.
public struct RegulatedCoin<phantom T> has key {
id: UID,
value: u64
}
/// Protected function - requires a Witness.
/// Mints a new `RegulatedCoin` with the value.
public fun mint<T: drop>(_: T, value: u64, ctx: &mut TxContext): RegulatedCoin<T> {
RegulatedCoin { id: object::new(ctx), value }
}
/// Protected function - requires a Witness.
/// Burns the `RegulatedCoin` and returns the value.
public fun burn<T: drop>(_: T, coin: RegulatedCoin<T>): u64 {
let RegulatedCoin { id, value } = coin;
id.delete();
value
}
/// Protected function - requires a Witness.
public fun transfer<T: drop>(_: T, coin: RegulatedCoin<T>, to: address) {
transfer::transfer(coin, to)
}
/// Public API - does not require a Witness.
public fun join<T>(coin: &mut RegulatedCoin<T>, other: RegulatedCoin<T>) {
let RegulatedCoin { id, value } = other;
coin.value = coin.value + value;
id.delete();
}
定義見證型別並呼叫 mint 的模組會取得屬於自己的受規範貨幣:它可以獨自決定如何——以及是否——公開其代幣的鑄造、銷毀與轉帳功能,而基礎模組則實作所有此類貨幣共用的邏輯。
見證的這種用法與 Capability 樣式相近,但有一個重要差異:capability 是一個物件,因此它授權給擁有它的人——也就是帳戶;而見證只能由定義它的模組建構,因此它授權的是程式碼。使用見證進行授權是在程式碼撰寫時就決定的,不需要儲存空間,也無法被轉移。
一次性見證 (One Time Witness)
雖然一個 struct 可以被建立任意次數,但在某些情況下,struct 應該被保證只能建立一次。為此,Sui 提供了「一次性見證(One-Time Witness)」——一種只能使用一次的特殊見證。我們會在下一節中更詳細地說明。
標準函式庫也提供了一種現成的證明形式:Internal Permit。internal::Permit<T> 證明該呼叫已由定義 T 的模組授權——而不需要函式庫設計自訂的見證型別,或要求 T 本身具備 drop。
總結 (Summary)
- 見證是一種透過建構證明來證明某項屬性的樣式。
- 在 Move 中,模組透過建構型別來證明對該型別的擁有權。
- 見證通常用於泛型型別實例化與授權。
下一步 (Next Steps)
在下一節中,我們將學習一次性見證樣式。