ZK 電路需要其內部的每個計算都能表達為有限體上的算術。這意味著 ZK 電路只能證明那些可以化簡為加法和乘法的計算。
除法、基於私人值的條件語句,以及任何涉及外部數據的操作都不能以那種方式表達。你不能通過使用電路來讀取本地存儲或從用戶設備中獲取用戶的私人金鑰。
但任何有用的 DApp 都需要做這些事情。例如,一個遊戲需要將玩家的得分除以回合數,而一個權限控制的合約需要檢查呼叫者的身份與存儲的秘密是否一致。
半夜透過見證人來處理。見證人讓你可以在鏈下執行任意 TypeScript,將結果輸入到一個迴路中,然後讓迴路透過 ZK 限制來驗證這個結果,而鏈下計算本身並不包含在證明中。所以你可以在鏈下進行你的除法運算,並在鏈上驗證其正確性
前置條件
繼續之前,你应该具備:
- 已安裝並運作緊湊工具鏈
- 對緊湊語法的基礎理解 — 線路、帳本聲明和類型
- 熟悉TypeScript,因為證人實現位於此處
證人是什麼?
見證是一個你在Compact中宣告但在TypeScript中實現的函數。你的Compact電路會呼叫它,但它無法看見它的內部。它只能看見輸出。因此,被證明的不是見證計算的內容,而是它的輸出(回傳值)滿足電路對它的限制條件。
這意味著:
- 你在Compact中宣告一個見證,帶有一個名稱和類型簽名,沒有實體內容
- 你將它實現在傳遞給合約的見證物件中
- 在執行時,當電路呼叫見證時,TypeScript實現離線執行,返回值流回電路
這是Compact中最基本的見證宣告樣式:
witness secretKey(): Bytes<32>;
有了這樣的宣告,編譯器就會知道這個函數存在,並知道它的回傳類型。合約中的任何電路都可以呼叫它。它在執行時期實際做什麼,完全由 TypeScript 的實現來決定。
這是一個 TypeScript 實現的 secretKey() 證明可能看起來像這樣:
// witnesses.ts file
import { WitnessContext } from "@midnight-ntwrk/compact-runtime";
import { Ledger } from "./managed/<myApp>/contract/index.js"; // update your path
export const witnesses = {
secretKey: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, Uint8Array] => [
privateState,
privateState.secretKey,
],
};
在上述片段中,該函數接收一個 WitnessContext 物件,該物件具有兩個泛型參數:
-
Ledger:預射到鏈上狀態的形態 -
PrivateState:此合約用戶本地私有的數據形態
它從中解構 privateState,並返回一個元組,其中首先包含更新的私有狀態,然後是電路實際接收的值。
NOTE: WitnessContext 顯示三個欄位 — ledger、privateState 和 contractAddress。這個證明只解構 privateState,因為它不需要讀取鏈上狀態或合約地址才能完成它的任務。
私人狀態線程問題。見證人是唯一在電路執行期間讀取和修改私人狀態的機制。私人狀態本身從不觸及帳本。
注意:您的witnesses.ts檔案應該與您的.compact檔案位於同一目錄。managed/ 資料夾是由編譯器產生的,並存放在該相同目錄下。請勿手動編輯它。您的 witnesses.ts 與您的 .compact 檔案並排放置,並從它旁邊的 managed/ 子資料夾中匯入編譯過的型別。
傳證人與組合邏輯在 Compact 中的不同之處
當你編寫一個電路時,其中每個操作都變成 ZK 證明的部分。證明者必須證明每個步驟都發生了正確的,而不揭露私人輸入。這就是讓電路值得信賴的原因。這也是讓它們昂貴的原因。你無法在電路內進行隨意計算。你無法執行讀取檔案、調用 API 或除法的操作。
見證人,另一方面,沒有這種限制。它們在證明生成之前就運行了,完全在證明之外。它們的輸出作為輸入進入電路,而電路的任務是驗證這個輸入,而不是重現生成它的計算。如果一個計算是廉價且隨機的,它屬於見證人。如果它需要被證明是正確的,它屬於電路.
這是在實際中的樣子:
緊湊代碼:
witness getAge(): Uint<8>;
export circuit verifyAdult(): [] {
const age = getAge();
assert(disclose(age >= 18), "Must be 18 or older");
}
TypeScript實現:
export const witnesses = {
getAge: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, bigint] => [
privateState,
privateState.age,
],
};
getAge() 在鏈下運行並從私人狀態返回用戶年齡。該電路從不證明該值是如何獲得的。它只證明該值滿足條件:
assert(disclose(age >= 18), "Must be 18 or older");
在上面的代码片段中,disclose(age >= 18) 是對比較進行包裝,而不是對年齡進行包裝,因為
disclose() 控制著什麼離開私人領域。包裝age 會直接將原始數字上鏈。然而,將比較包裝起來只會將布林結果上鏈。該電路可以在實際年齡從未離開私人狀態的情況下證明條件為真。
使用證人之一後果是,編譯器會將來自證人的每一個值視為潛在的私人。任何從證人回傳衍生出來的值——透過算術、型別轉換或邏輯——都繼承私人狀態。如果這類數據沒有透過明確的disclose()呼叫就到達公共帳本欄位或導出的電路回傳值,編譯器將拒絕編譯。
常見使用見證人的方法在 Compact
您將會遇到多種情況,需要在 Compact 中使用見證人。本節列舉了一些在為 Midnight 鏈編寫代碼時使用見證人的常見方法。
1. 私有身份,如密鑰
午夜合約中最常見的見證者從本地儲存中取出用戶的私鑰。密鑰從未觸及公證帳本。只有它的哈希值是公開的.
這是一個緊湊合約的例子:
pragma language_version >= 0.22;
import CompactStandardLibrary;
witness secretKey(): Bytes<32>; //witness declaration
ledger owner: Bytes<32>;
circuit publicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([
pad(32, "myapp:auth:pk:"),
_sk
]);
}
constructor() {
const _sk = secretKey();
owner = disclose(publicKey(_sk));
}
在上面的片段中,構造函數在部署時運行一次。它調用了secretKey() 證人從鏈下取回部署者的私鑰,使用 persistentHash 從中導出一個公鑰,並僅將其哈希儲存於 owner 記錄簿欄位中。
部署後,密鑰就從執行上下文中消失了。鏈上只留下其加密承諾。
在 TypeScript 中,您將像這樣實現您的證人:
import type { WitnessContext } from "@midnight-ntwrk/compact-runtime";
import { Ledger } from "./managed/<myApp>/contract/index.js"; // update your path
type PrivateState = {
secretKey: Uint8Array;
};
export const witnesses = {
secretKey: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, Uint8Array] => [
privateState, // private state is unchanged
privateState.secretKey, // this is what the circuit receives
],
};
TypeScript 的實現只是從私人狀態中取出密鑰並返回。該電路如何使用它——哈希、存儲——與證人無關
2. 證人驗證的分離
電路沒有內建的除法運算符。支援 ZK 證明的場運算不直接支援它。如果你需要在電路內除兩個數字,你必須使用見證在鏈下計算結果,然後在電路內證明結果是正確的。
這是一個來自真實的 Midnight game contract:
pragma language_version >= 0.22;
witness _divMod(x: Uint<32>, y: Uint<32>): [Uint<32>, Uint<32>];
export circuit div(x: Uint<32>, y: Uint<32>): Uint<32> {
const res = disclose(_divMod(x, y));
const quotient = res[0];
const remainder = res[1];
assert(remainder < y && x == y * quotient + remainder, "Invalid divMod witness impl");
return quotient;
}
export circuit mod(x: Uint<32>, y: Uint<32>): Uint<32> {
const res = disclose(_divMod(x, y));
const quotient = res[0];
const remainder = res[1];
assert(remainder < y && x == y * quotient + remainder, "Invalid divMod witness impl");
return remainder;
}
在上述程式碼中,電路並不盲目信任見證。它們驗證除法的基礎身分:x == y * quotient + remainder,並檢查餘數嚴格小於除數。如果攻擊者提供一個惡意的見證實現,它返回不正確的值,則斷言失敗,交易回滾。
在TypeScript中,這是實現的內容_divMod 看起來像:
export const witnesses = {
_divMod: (
context: WitnessContext<Ledger, Game2PrivateState>,
x: bigint,
y: bigint,
): [Game2PrivateState, [bigint, bigint]] => {
const xn = Number(x);
const yn = Number(y);
const remainder = xn % yn;
const quotient = Math.floor(xn / yn);
console.log(
`dyn witness _divMod(${x}, ${y}) = [${quotient}, ${remainder}]`,
);
return [context.privateState, [BigInt(quotient), BigInt(remainder)]];
},
};
總體來說,TypeScript 执行了廉價的算術,而電路證明了結果是正確的.
3. 使用證人管理私有狀態
有些證人並不返回單個計算值。相反地,他們管理一個本地的狀態機,該狀態機反映了合約的鏈上狀態。這裡有一個例子:
pragma language_version >= 0.22;
import CompactStandardLibrary;
witness localSecretKey(): Bytes<32>;
witness localState(): LocalState;
witness localAdvanceState(): [];
witness localRecordVote(vote: Boolean): [];
witness localVoteCast(): Maybe<Boolean>;
enum LocalState { initial, committed, revealed }
export ledger committedVotes: MerkleTree<10, Bytes<32>>;
export ledger committed: Set<Bytes<32>>;
circuit commitmentNullifier(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "nullifier:"), sk]);
}
circuit commitWithSk(ballot: Bytes<32>, sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([ballot, sk]);
}
export circuit voteCommit(ballot: Boolean): [] {
assert(localState() == LocalState.initial, "Already committed");
localRecordVote(ballot);
const sk = disclose(localSecretKey());
const nullifier = commitmentNullifier(sk);
assert(!committed.member(nullifier), "Already used this identity");
const cm = commitWithSk(ballot ? pad(32, "yes") : pad(32, "no"), sk);
committedVotes.insert(disclose(cm));
committed.insert(disclose(nullifier));
localAdvanceState();
}
注意localAdvanceState()和localRecordVote()返回[]。它們沒有回傳值給電路。它們全部的工作是作為副作用來更新私人狀態。Compact 側宣告這些函數存在;TypeScript 側才是實際狀態轉換發生的地方.
TypeScript 的實現看起來會像這樣:
type PrivateState = {
secretKey: Uint8Array;
localState: "initial" | "committed" | "revealed";
localVote: boolean | null;
};
export const witnesses = {
localSecretKey: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, Uint8Array] => [
privateState,
privateState.secretKey,
],
localState: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, string] => [
privateState,
privateState.localState,
],
localAdvanceState: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, []] => {
const next =
privateState.localState === "initial"
? "committed"
: privateState.localState === "committed"
? "revealed"
: privateState.localState;
return [{ ...privateState, localState: next }, []];
},
localRecordVote: (
{ privateState }: WitnessContext<Ledger, PrivateState>,
vote: boolean,
): [PrivateState, []] => [{ ...privateState, localVote: vote }, []],
localVoteCast: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [
PrivateState,
{ is_some: boolean; value: boolean },
] => [
privateState,
privateState.localVote !== null
? { is_some: true, value: privateState.localVote }
: { is_some: false, value: false },
],
};
每一個見證者都將完整的更新後的私人狀態作為其第一個元素返回。這就是午夜線程如何透過迴路執行功能在迴路中傳遞私人狀態的方式。如果迴路在任何時候失敗,則沒有任何私人狀態變更是被提交的.
4. Merkle 路徑見證者
當合約需要驗證一個值屬於一個集合而不揭露哪個成員時,它使用默克爾樹。路徑計算是太過昂貴且隨機的,無法在電路內發生。一個證人將路徑離鏈遍歷並將路徑交給電路,電路驗證根。
這是一個緊湊合約的例子:
pragma language_version >= 0.22;
import CompactStandardLibrary;
witness localSecretKey(): Bytes<32>;
witness localPathOfPk(pk: Bytes<32>): Maybe<MerkleTreePath<10, Bytes<32>>>;
export ledger eligibleVoters: MerkleTree<10, Bytes<32>>;
circuit voterPublicKey(sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<2, Bytes<32>>>([pad(32, "election:voter:"), sk]);
}
export circuit proveEligibility(): [] {
const sk = localSecretKey();
const pk = voterPublicKey(sk);
const path = localPathOfPk(pk);
assert(
disclose(path.is_some) &&
eligibleVoters.checkRoot(disclose(merkleTreePathRoot<10, Bytes<32>>(path.value))) &&
pk == path.value.leaf,
"Not an eligible voter"
);
}
該電路接收來自見證者的路徑,並檢查三件事:
- 路徑是否存在
- 路徑根與鏈上樹根是否匹配
- 葉子節點是否為呼叫者的公鑰.
如果任何檢查失敗,斷言會撤銷所有操作。投票者的身份從未被揭露。相反,證明僅僅證明一個有效路徑存在。
這是證人的TypeScript實現:
type PrivateState = {
secretKey: Uint8Array;
localVoterTree: Map<string, unknown>; // local Merkle tree structure
};
export const witnesses = {
localSecretKey: ({
privateState,
}: WitnessContext<Ledger, PrivateState>): [PrivateState, Uint8Array] => [
privateState,
privateState.secretKey,
],
localPathOfPk: (
{ privateState }: WitnessContext<Ledger, PrivateState>,
pk: Uint8Array,
): [PrivateState, { is_some: boolean; value: unknown }] => {
// Traverse the local Merkle tree to find the path for this public key. Implement your version computeMerklePath
const path = computeMerklePath(privateState.localVoterTree, pk);
return [
privateState,
path ? { is_some: true, value: path } : { is_some: false, value: null },
];
},
};
TypeScript持有Merkle樹的本地副本並計算路徑。電路驗證它。樹遍歷無需以場運算表示。
5. 將外部數據輸入電路
見證人可以拉取合約外存在的任何數據(例如,遊戲狀態、棋盤位置、API 回應)。關鍵要求是,電路必須約束和驗證見證人提供的任何內容。一個返回未驗證外部數據的見證人以及一個盲目信任它的電路是一個安全漏洞.
The 海戰遊戲由ErickRomeroDev開發 清晰地展示了這個模式。遊戲開始前,每個玩家私下提交他們的棋盤設置。見證者收到完整的棋盤,本地存儲它,並僅向電路返回一個哈希值。電路將該哈希值寫入賬本,而實際的棋盤位置從未上鏈。
export ledger playerOnePk: Cell<Bytes<32>>;
export ledger playerOneCommit: Cell<Bytes<32>>;
export ledger playerOneHasCommitted: Cell<Boolean>;
witness local_sk(): Bytes<32>;
// Receives the full board setup, stores it in private state,
// and returns only a hash to the circuit
witness set_local_gameplay(playerSetup: Vector<100, Uint<1>>): Bytes<32>;
export circuit commitGrid(player: Bytes<32>, playerSetup: Vector<100, Uint<1>>): Void {
// Verify the caller is who they claim to be
assert(playerOnePk == public_key(local_sk())) "PlayerOne confirmation failed";
// The witness stores the board locally and hands back only the hash
const commit = set_local_gameplay(playerSetup);
// Only the hash goes on-chain — the board positions stay private
playerOneCommit.write(commit);
playerOneHasCommitted.write(true);
}
export circuit vectorHash(sk: Vector<100, Uint<1>>): Bytes<32> {
return persistent_hash<Vector<100, Uint<1>>>(sk);
}
之後,當一個玩家做出一個移動時,電路使用vectorHash 电路用來驗證本地儲存的板子是否與提交的哈希值相符。這能夠發現遊戲開始後任何試圖篡改板子的行為.
// Verify the player has not tampered with their board since committing
assert(vectorHash(local_gameplay()) == playerOneCommit) "Player one has tampered with the grid";
這裡是兩個見證人的TypeScript實現:
export const witnesses = {
local_sk: ({
privateState,
}: WitnessContext<Ledger, NavalBattlePrivateState>): [
NavalBattlePrivateState,
Uint8Array,
] => [privateState, privateState.secretKey],
set_local_gameplay: (
{
privateState,
contractAddress,
}: WitnessContext<Ledger, NavalBattlePrivateState>,
playerSetup: bigint[],
): [NavalBattlePrivateState, Uint8Array] => {
const updatedGameplay =
privateState.localGameplay ?? new Map<string, bigint[]>();
// Store the full board setup locally, keyed by contract address
updatedGameplay.set(contractAddress, playerSetup);
return [
{ ...privateState, localGameplay: updatedGameplay },
// Return only the hash to the circuit — the board itself stays private
pureCircuits.vectorHash(playerSetup),
];
},
};
local_sk 回傳玩家的秘密金鑰來自私人狀態。這個模式在早前的章節中已有解釋。
set_local_gameplay 是外部數據採納發生的地方。它接收完整的棋盤設定作為一個 bigint[],將其儲存在本地遊戲地圖中,鍵為 contractAddress,並透過 pureCircuits.vectorHash 回傳其哈希值。使用 contractAddress 作為鍵意味著玩家可以
能夠同時參與多個遊戲實例,而無法從一個遊戲中洩漏至另一個遊戲.
注意:pureCircuits 是來自您編譯合約的生成物件,它暴露了無帳本側效的電路,因此可以直接從TypeScript中調用,而無需提交交易。您可以到Midnight's Documentation了解更多有關此運作方式的資訊。.
基於見證的訪問控制
Midnight 中的訪問控制並非像以太坊那樣運作。沒有原生版本的 msg.sender,而且迴路無法觀察到誰提交了交易。
在緊湊模式中,電路可透過要求呼叫者證明其知曉一個秘密,並驗證該證明與鏈上存儲的承諾相匹配來執行訪問控制。它遵循以下模式:
- 在設置時儲存秘密的哈希值
- 在呼叫時重新導出相同的哈希值
- 斷言上述兩者彼此匹配。如果它們不匹配,電路將回滾。
這是一個示範如何在 Compact 中實施權限控制的例子:
pragma language_version >= 0.22;
import CompactStandardLibrary;
witness secretKey(): Bytes<32>;
ledger authority: Bytes<32>;
export ledger round: Counter;
export ledger sensitiveValue: Bytes<32>;
circuit publicKey(_sk: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([
pad(32, "myapp:auth:pk:"),
round as Field as Bytes<32>,
_sk
]);
}
// Runs once at deployment. Stores a hash of the deployer's secret key as the authority.
constructor(value: Bytes<32>) {
const _sk = secretKey();
authority = disclose(publicKey(_sk));
sensitiveValue = disclose(value);
}
// Only the holder of the original secret key can call this.
export circuit update(newValue: Bytes<32>): [] {
const _sk = secretKey();
assert(publicKey(_sk) == authority, "Not authorized");
sensitiveValue = disclose(newValue);
round.increment(1);
authority = disclose(publicKey(_sk));
}
// Anyone can read. Only the authority can write.
export circuit read(): Bytes<32> {
return sensitiveValue;
}
在上面的程式碼片段中,建構函數在部署時運行一次。它呼叫 secretKey() 證人
來取得部署者的私鑰,從中導出一個公鑰,並僅存儲其雜湊值。authority。每一次後續對update()的調用都會從證據中重新導出那個相同的哈希值,並與authority進行比較。如果呼叫者不知道原始的密鑰,那麼assert會失敗,交易將會回滾。
read() 沒有訪問控制。任何呼叫者都可以執行它。這兩個電路的區別在於訪問控制模型:一個需要身份證明,另一個不需要。
注意:您會注意到使用了循環計數器。在update()成功後,該函數會增加round並重新導出。authority 與新的計數值。這意味著儲存的權限哈希在每個授權行為後都會變更,因此觀察者無法將鏈上的多個 update() 呼叫鏈接到同一個鍵.
基於角色的訪問控制與見證人
至今,你已了解如何處理單一權威的訪問控制。但當你的合約需要多個角色,例如鑄錠者或管理員時,該怎麼辦?解決方案很簡單,就是擴展你的合約,為每個角色存儲獨立的權威哈希:
pragma language_version >= 0.22;
import CompactStandardLibrary;
witness secretKey(): Bytes<32>;
ledger adminAuthority: Bytes<32>;
ledger minterAuthority: Bytes<32>;
circuit roleKey(_sk: Bytes<32>, role: Bytes<32>): Bytes<32> {
return persistentHash<Vector<3, Bytes<32>>>([
pad(32, "myapp:role:"),
role,
_sk
]);
}
export circuit mint(amount: Uint<64>): [] {
const _sk = secretKey();
assert(roleKey(_sk, pad(32, "minter")) == minterAuthority, "Not a minter");
// mint logic
}
export circuit pause(): [] {
const _sk = secretKey();
assert(roleKey(_sk, pad(32, "admin")) == adminAuthority, "Not an admin");
// pause logic
}
每一個角色在哈希中都使用不同的域字串,因此相同的密鑰對每一個角色產生不同的承諾。所以,即使同一个人同時擁有這兩個角色,觀察者也不能將管理員行為與鑄造者行為進行關聯。
官方 Midnight DApps 如何使用見證
1. 選舉合約
範例選舉合約 使用兩位見證人來實現一種私有的提交-揭露投票方案:
witness localSk(): Bytes<32>;
witness localGetVote(): VoteChoice;
export circuit commitVote(): [] {
assert(votingState == VotingState.OPEN, "Voting has not opened yet");
const _sk = localSk();
const pubKey = getDappPubKey(_sk);
assert(registeredVoters.member(pubKey), "You are not registered to vote.");
assert(!hashedVoteMap.member(pubKey), "Attempt to double vote");
const _currentVote = localGetVote();
assert(_currentVote == VoteChoice.BAD || _currentVote == VoteChoice.WORSE, "Please provide a valid vote");
const hash = commitWithSk(_currentVote as Field as Bytes<32>, _sk);
hashedVoteMap.insert(pubKey, hash);
totalVoteCount.increment(1);
}
// check privateState for original vote, if it's changed, then error
export circuit revealVote(): [] {
assert(votingState == VotingState.CLOSED, "Voting is still open");
const _sk = localSk();
const pubKey = getDappPubKey(_sk);
// we want to check that they have already voted..
assert(hashedVoteMap.member(pubKey), "You have not voted yet");
assert(registeredVoters.member(pubKey), "You are not a registered voter");
const vote = localGetVote();
assert(vote == VoteChoice.BAD || vote == VoteChoice.WORSE, "Please supply a valid vote");
// here is the money
const hashedVote = commitWithSk(vote as Field as Bytes<32>, _sk);
assert(hashedVoteMap.lookup(pubKey) == hashedVote, "Attempt to change the vote!");
if(disclose(vote) == VoteChoice.BAD){
candidate0VoteCounter.increment(1);
} else if (disclose(vote) == VoteChoice.WORSE) {
candidate1VoteCounter.increment(1);
}
}
// hash a random "public key" that is only traceable in this dapp
circuit getDappPubKey(_sk: Bytes<32>): Bytes<32> {
return disclose(persistentHash<Vector<2, Bytes<32>>>([pad(32, "election:pk:"), _sk]));
}
localSk 處理身分。每一個需要驗證呼叫者的電路都從它那裡導出他們的公鑰。localGetVote 從私有狀態中取得投票者的選擇。
IncommitVote(),該電路從見證者那裡取出投票,將其與呼叫者的密鑰一起哈希,並僅在鏈上存儲該哈希值,而不揭露投票:
const _currentVote = localGetVote();
const hash = commitWithSk(_currentVote as Field as Bytes<32>, _sk);
hashedVoteMap.insert(pubKey, hash);
InrevealVote(),該電路會再次取得投票,並重新導出相同的哈希值。如果重新導出的哈希值與所提交的值相符,該投票就會被計算。如果投票者在提交與揭露之間嘗試改變他們的投票,哈希值就會不匹配,並且斷言失敗:
const hashedVote = commitWithSk(vote as Field as Bytes<32>, _sk);
assert(
hashedVoteMap.lookup(pubKey) == hashedVote,
"Attempt to change the vote!",
);
您可以基於Midnight的官方文件瀏覽完整程式碼.
2. 海戰遊戲
該海戰合約使用三個證人:
witness local_sk(): Bytes<32>;
witness local_gameplay(): Vector<100, Uint<1>>;
witness set_local_gameplay(playerSetup: Vector<100, Uint<1>>): Bytes<32>;
local_sk處理玩家身份。set_local_gameplay接收完整的棋盤設置,本地存儲,並僅向電路返回一個哈希值。local_gameplay 在遊戲進行中取出儲存的棋盤。
三位見證人共同在遊戲中的兩個關鍵時刻合作。在提交時,set_local_gameplay 儲存棋盤,而電路僅在鏈上寫入哈希:
const commit = set_local_gameplay(playerSetup);
playerOneCommit.write(commit);
在移動時,local_gameplay 嘗取儲存的板卡,且電路驗證自提交以來未曾被竄改:
assert(vectorHash(local_gameplay()) == playerOneCommit) "Player one has tampered with the grid";
板卡位置從未上鏈。鏈上保存的是一個提交。
證人安全規則
在使用證人時(而且你會使用它們),你應該記住以下幾點,以確保你的隱私不受到侵犯:
1. 核實,永遠不信任。 證人返回的每一個值都應被視為敵對輸入。編寫電路斷言來證明輸出基於公共輸入和數學恆等式是正確的。你不應該假設證人是誠實的或正確的。
2. 證人跑出 ZK 證明 用戶自己的 DApp 提供證人實現。如果用戶修改他們的前端以從證人返回惡意值,那麼迴路斷言就是那個和無效狀態轉換之間的唯一屏障。讓那些斷言緊密
3. 永遠不要用 ownPublicKey() 進行訪問控制。 它是一個證人。它可以回傳任何東西。使用密鑰→哈希模式。
4. 尽可能晚地保留disclose()。 當你將值包裝在disclose()中,編譯器就停止追踪它以防止意外洩露。在值進入賬本字段或迴路返回之前立即披露,而不是在它離開證人的時候。
5. 證人無法修改公共狀態. 他們只能更新私人狀態並將值傳回給電路。如果您需要更改賬本欄位,那必須在電路邏輯中進行,使用證人返回的內容.
總結
見證者就是午夜隱私架構執行大多數實際工作的地方。它們處理 ZK 證明系統無法處理的一切,例如隨機計算、隱私數據訪問、外部數據採集,並將結果交給限制和驗證它們的電路。
本文中的模式涵蓋了你最常遇到的情況:
- 密鑰見證者 建立私人身分而不將憑證上鏈
- 見證驗證的分割 處理電路無法原生執行的算術,以電路斷言證明正確性
- 私有狀態管理見證 執行反映鏈上狀態的本地狀態機
- 梅爾克路徑見證 在鏈下計算成員證明並交給電路進行驗證
- 外部數據見證輸入任意鏈下數據,電路進行約束
在每一個模式中,結構都是相同的:見證者計算,電路驗證。這個界線不是限制。它是使整個系統值得信賴的原因。










