Skip to main content

一次性見證 (One Time Witness)

一般的 Witness 是靜態證明型別所有權的絕佳方式,但在某些情況下,我們需要確保見證只會被具現化一次——這正是一次性見證(One Time Witness,OTW)的用途。

背景 (Background)​

為了解 OTW 所解決的問題,讓我們嘗試使用現有工具建置一個簡單的泛型代幣實作。TreasuryCap<T> 控制型別為 T 的代幣供應量,而建立它需要 T 的 見證:

module book::simple_coin;

/// 控制 Coin 的供應量。
public struct TreasuryCap<phantom T> has key, store {
id: UID,
total_supply: u64,
}

/// 使用見證建立新的 `TreasuryCap`。
/// 弱點:沒有任何機制能阻止呼叫端使用相同見證建立
/// 多個 `TreasuryCap<T>`!
public fun new<T: drop>(_witness: T, ctx: &mut TxContext): TreasuryCap<T> {
TreasuryCap { id: object::new(ctx), total_supply: 0 }
}

一般見證可證明呼叫模組擁有型別 T,但無法證明見證已經或將會被建構 多少次。不誠實的開發者可以直接呼叫 new 兩次,並將第二個金庫留給自己:

module book::simple_coin_cheater;

/// Coin 見證……使用了兩次。>_<
public struct MOVE has drop {}

fun init(ctx: &mut TxContext) {
let treasury = book::simple_coin::new(MOVE {}, ctx);
let secret_treasury = book::simple_coin::new(MOVE {}, ctx);

transfer::public_transfer(treasury, ctx.sender());
transfer::public_transfer(secret_treasury, ctx.sender());
}

任何人在決定是否信任以此方式建置的代幣時,都必須稽核一整套條件:指定 T 是否只存在一個 TreasuryCap、模組是否沒有能額外發行代幣的後門,以及未來升級是否無法加入此類後門。這些條件無法從 Move 原始碼內部檢查——驗證它們需要信任作者,並仔細且重複檢閱原始碼。

為了不再需要這種信任,Sui 引入了一次性見證——由系統本身保證恰好只會具現化一次,且可於執行階段檢查的見證。

定義 (Definition)​

OTW 是只能使用一次的特殊見證型別。它無法手動建立,且保證在每個模組中都是唯一的。如果型別符合以下規則,Sui 執行環境會將其視為 OTW:

  1. 僅具有 drop 能力。
  2. 沒有欄位。
  3. 不是泛型型別。
  4. 名稱與模組名稱相同,且全部使用大寫字母。

以下是 OTW 的範例:

module book::one_time;

/// The OTW for the `book::one_time` module.
/// Only `drop`, no fields, no generics, all uppercase.
public struct ONE_TIME has drop {}

/// Receive the instance of `ONE_TIME` as the first argument.
fun init(otw: ONE_TIME, ctx: &mut TxContext) {
// do something with the OTW
}

OTW 無法手動建構,任何嘗試這麼做的原始碼都會導致編譯錯誤。OTW 可作為第一個引數傳入 模組初始化函式。由於每個模組的 init 函式只會呼叫一次,因此 OTW 保證只會具現化一次。

強制執行 OTW (Enforcing the OTW)​

為了檢查型別是否為 OTW,Sui Framework 的 sui::types 模組提供了特殊函式 is_one_time_witness。這是上述規則在執行階段的對應機制:預期接收 OTW 的函式庫函式應呼叫它,以確認接收到的見證是真正的一次性見證,而不是僅具有 drop 能力的一般型別。

use sui::types;

const ENotOneTimeWitness: u64 = 1;

/// Takes an OTW as an argument, aborts if the type is not OTW.
public fun takes_witness<T: drop>(otw: T) {
assert!(types::is_one_time_witness(&otw), ENotOneTimeWitness);
}

這個單一的 assert! 修正了背景章節中的代幣範例:若 simple_coin::new 要求的是 OTW 而非一般見證,作弊模組中的第二次呼叫便會失敗,因為 OTW 執行個體只存在一次——也就是第一次呼叫時。

總結 (Summary)​

OTW 模式是確保型別只會使用一次的絕佳方法。大多數開發者只需要知道如何定義及接收 OTW,而檢查與強制執行大多是函式庫與框架需要處理的事。例如,sui::coin 模組要求在 coin::create_currency 方法中使用 OTW,因此可強制確保 coin::TreasuryCap 只會建立一次——恰好解決了我們在背景章節中描述的問題。

OTW 是強大的工具,為 Publisher 物件奠定基礎;我們將在下一節介紹它。