Skip to main content

模式:見證 (Pattern: Witness)

見證是一種透過建構證據來證明事實的模式。在程式設計的情境中,見證是藉由提供一個只有在該性質成立時才能建構的值,來證明系統具有特定性質的方法。

Move 中的見證 (Witness in Move)​

在 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 {})
}

結構 W 的執行個體會傳入 new_instance 函式以建立 Instance<W>,從而證明 book::witness_source 模組擁有型別 W。

泛型型別具現化 (Instantiating a Generic Type)​

見證允許使用具體型別來具現化泛型型別。若模組提供這項能力,這對於繼承型別的相關行為,並選擇擴充這些行為相當實用。

module sui::balance;

/// T 的供應量。用於鑄造與銷毀。
/// 在 `Coin` 模組中包裝為 `TreasuryCap`。
public struct Supply<phantom T> has store {
value: u64,
}

/// 為型別 T 建立新的供應量。
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 是一個泛型結構,只能藉由提供型別 T 的見證來建構。見證會以值的方式取得並 捨棄,因此 T 必須具有 drop 能力。

具現化後的 Supply<T> 可用於鑄造新的 Balance<T>,其中 T 是供應量的型別。

module sui::balance;

const EOverflow: u64 = 1;

/// 可儲存的餘額——Coin 型別的內部結構。
/// 可用於儲存不需要 key 能力的代幣。
public struct Balance<phantom T> has store {
value: u64,
}

/// 將供應量增加 `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——本章稍早說明的 能力——是以見證具現化的 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 模式相近,但有一項重要差異:能力是一種物件,因此它會授權擁有它的人——即帳戶;見證只能由定義它的模組建構,因此它會授權原始碼。使用見證的授權在撰寫原始碼時便已決定,不需要儲存,也無法轉移。

一次性見證 (One Time Witness)​

雖然結構可以建立任意次數,但有些情況下必須保證結構只能建立一次。為此,Sui 提供「一次性見證」——只能使用一次的特殊見證。我們會在下一節中更詳細說明。

標準函式庫也提供此證明的現成形式: Internal Permit。internal::Permit<T> 可證明呼叫已獲定義 T 的模組授權——函式庫無須設計自訂見證型別,或要求 T 本身具有 drop。

總結 (Summary)​

  • 見證是一種透過建構證明來驗證特定性質的模式。
  • 在 Move 中,模組透過建構型別來證明其擁有該型別。
  • 見證常用於泛型型別具現化與授權。

後續步驟 (Next Steps)​

在下一節中,我們將學習 一次性見證 模式。