CaptainZ

CaptainZ

Prompt Engineer. Focusing on AI, ZKP and Onchain Game. 每周一篇严肃/深度长文。专注于AI,零知识证明,全链游戏,还有心理学。
twitter

ZK-Hunt:全鏈遊戲實現隱藏資訊的新嘗試

—— 介紹 ——#

ZK Hunt 是一款類似 RTS 的鏈上 PvP 遊戲,探索了不同的 ZK 遊戲機制和資訊不對稱性。它是使用 MUD 框架製作的,該框架處理整體合約架構、網絡和客戶端同步邏輯,以及用於 ZK 證明生成和驗證的 circom。

這款遊戲是在 2022 年 0xPARC Autonomous Worlds 駐地期間構建的,當時還有許多其他團隊在研究他們自己的精彩項目,你可以在這裡找到一份完整的演示列表。如果你想要一個更短的 ZK Hunt 機制摘要,那麼你可以在這裡查看我的演示錄像。

ZK Hunt 最初只是關於在鏈上遊戲中實現私密移動的新方法的簡單想法,而在駐地期間,加入了更多利用不同加密構造的機制,從而開啟了新的遊戲體驗。

在開發過程中,我有關於如何將其擴展成一個完整的 EFT(逃離塔科夫)式遊戲的想法,在這個遊戲中,玩家帶著一定數量的戰利品 / 裝備進入狩獵場,殺死其他玩家以奪取他們的戰利品,然後試圖在被其他人殺死之前 “提取” 他們的戰利品。然而,隨著時間的推移,我意識到這個項目更好地作為一種探索 ZK 遊戲機制可能性的媒介,最終作為一個教育資源。

所以在這篇文章中,我將介紹 ZK Hunt 中存在的不同機制,用於實現這些機制的具體技術,我圍繞私有狀態開發的一些思維模型,以及這些機制如何被更廣泛地概括。

我試圖在較高的層次上解釋核心概念,同時也更深入地探討技術方面,所以希望這篇文章能對具有這些主題不同熟練程度的合理範圍的讀者有所幫助。如果任何部分過於深入技術細節而超出了你的喜好,請隨時跳過,因為在後續部分中你可能會發現不依賴於所述技術細節的進一步價值。

最好對智能合約、加密、哈希函數和 ZK 證明有基本的了解。

免責聲明:

由於是在 MUD(1.x,2.x 兩個版本是不同的)上構建的,ZK Hunt 中的合約遵循 ECS 模式。你可以在這裡了解更多關於 MUD 如何實現這種模式的信息,但在較高的層次上,遊戲中的所有 “實體” 都表示為唯一的數值 ID(EVM 中的 uint256s),實體的屬性存儲在不同的組件合約中,這些合約不包含業務邏輯(本質上只是從實體 ID 到值的映射),玩家與不同的系統合約互動,這些合約包含業務邏輯但不包含實體狀態,它們從不同的組件合約中讀取和寫入。

當我提到 “合約” 時,我是在口語上指特定情況下相關的特定合約,通常在每種情況下都會有所不同。

ZK 電路實現的探索假定你對 circom 語言有一定的熟悉度,並且也使用了一些尚未包含在標準文檔中的較新 circom 語法。為簡潔起見,下面的電路代碼的某些部分已被排除。

—— 叢林 / 平原移動 ——#

下面的視頻展示了 ZK Hunt 的核心機制;公共 / 私有移動的二分法。在視頻中,你可以看到兩個玩家,左邊的玩家 A 和右邊的玩家 B。每個玩家控制一個他們之前生成的單位,用白色輪廓突出顯示,並可以看到另一個玩家的單位,用紅色標出以顯示他們是敵人。

移動是通過選擇目的地並確認路徑來執行的。每個移動都作為單獨的交易提交,一旦前一個移動被確認,就提交新的移動。ZK Hunt 運行在一個配置為有一秒鐘區塊時間的 EVM 鏈上,也就是單位能夠以每秒一個瓦片的速度移動,而其他單位動作能夠以低延遲處理。

在這個世界中,我們有兩種類型的瓦片;用草顯示的平原瓦片,和用樹顯示的叢林瓦片。穿越平原是公開的,這一點通過玩家 B 能看到玩家 A 的位置更新為他們移動的事實來說明。進入叢林也是公開的,但穿越叢林是私密的,以至於玩家 A 失去了玩家 B 在叢林中的位置的蹤跡,並且只能模擬一個不斷增長的潛在位置集合,這些位置用問號顯示。再次從叢林出來回到平原是公開的,所以潛在位置集合崩潰了。

這種行為是 ZK Hunt 的基礎;單位有一個狀態片段(它們的位置),可以根據遊戲中的動作從公開變為私有,然後再變回公開。當一個單位的位置變得私有時,它不是從沒有歧義變為完全歧義,而是獲得了一個可以隨時間增加的有限歧義度,這是由於基於瓦片的移動的受限性。這允許其他玩家對單位的位置有一定程度的信心,並且他們越早採取行動,信心就越大。

在深入了解這個機制是如何工作的之前,我需要建立一些關於 ZK 證明輸入 / 輸出和承諾的先決條件理解。

關於 ZK 電路輸入輸出的一些看法#

在一個 ZK 電路的 circom 代碼中,您可以定義公共輸入、私有輸入和輸出,其中輸入由用戶提供,輸出則是電路內部進行的某些計算的結果,這些結果在創建證明時通過證明過程返回給用戶:

template Example() {
    signal input a, b;
    signal output c;
    a + b === 7; // Checks a + b is equal to 7
    c <== a * b; // Outputs a * b
}
// a is a public input, b is a private input by omission from the list
component main {public [a]} = Example();

然而,重要的是要知道輸出實際上只是語法抽象,在底層被視為額外的公共輸入。從根本上講,一個 ZK 電路接受一列輸入,並檢查這些輸入之間的一組數學約束是否得到滿足,唯一的輸出是 “真” 或 “假”。上面的電路在功能上等同於這個:

template Example() {
    signal input a, b, c;
    a + b === 7; // Checks a + b is equal to 7
    a * b === c; // Checks that a * b is equal to c
}
component main {public [a, c]} = Example();

這裡的區別在於,如果 c 被定義為輸出而不是輸入,那麼用戶不必計算 c 的值,而是在證明生成期間,電路內部定義的邏輯為他們做了這件事,這對於確保使用的值滿足電路是方便的。

輸出實際上只是額外的公共輸入這一事實,在查看合約中的證明驗證邏輯時是相關的。solidity 驗證器接受一列輸入(連同證明本身),其中在此列表中電路代碼中定義的輸出首先出現,公共輸入隨後出現,唯一真正的 “輸出” 是 “成功” 或 “失敗”。

儘管如此,從概念上講,認為公共輸入和輸出之間存在區別仍然是有用的,特別是當涉及到驗證計算過程(如狀態轉換)的電路時,這些電路具有自然的輸入(舊狀態)和輸出(新狀態)。

對於 ZK Hunt 中的電路,公共輸入通常是已經在之前的證明中計算 / 驗證並存儲在合約中的值,而輸出則是新證明內部執行的計算結果,這些結果由該證明驗證,然後保存到合約中。

最後要了解的一點是,儘管 ZK 證明驗證的成本被認為是恆定的(至少對於某些證明系統,如 groth16 等),但它實際上基於公共輸入的數量而增加,這在進行鏈上驗證時可能很重要。在我理解公共電路輸入和輸出之間缺乏功能區別之前,我認為你可能通過將所有公共輸入轉換為輸出來最小化這種成本,但基於上面的解釋,這顯然是行不通的。

關於 Commitment 方法的一些看法#

Commitment(承諾)是一種工具,它(除其他外)可以被一個零知識證明(ZK proof)用來可驗證地引用用戶先前 “承諾” 的一些私有狀態,而無需向驗證者(在鏈上驗證的情況下,就是向觀察鏈的每個人)透露該狀態。用戶將承諾 C 作為公開輸入提供給證明,將私有狀態 s 作為私有輸入,並且證明在內部計算出由 s 產生的承諾,並檢查它是否與 C 匹配:

template Example() {
	signal input state; // Private
	signal input commitment; // Public
	// Calculates the poseidon hash commitment from 'state'
	signal result <== Poseidon(1)([state]);
	result === commitment;
	// The rest of the circuit can now trust the validity of 'state'
	...
}
component main {public [commitment]} = Example();

在驗證證明時,驗證者會獲得公共信號的值,因此在檢查提供的承諾值是否正確(即與用戶先前提交的值匹配)時,他們可以確信在生成證明時使用了正確的私有狀態值。

您可以使用各種不同的承諾方案,但也許最簡單的就是取狀態的哈希值。在 ZK Hunt 中使用 poseidon 哈希是因為它在電路內計算比其他常見的哈希函數更有效率。如果私有狀態是從足夠大的範圍內隨機選擇的足夠隨機的值(如私鑰或隨機種子),那麼僅僅取該值的哈希就足以作為一個承諾。

但是,如果狀態可能採取的值範圍相對較小(例如,介於 1 和 10 之間的值),那麼對手只需計算每個這些值的結果承諾,看看哪一個與用戶提交的承諾相匹配,從而找出它對應的狀態值,破壞了承諾的隱私性。

為了防止這種蠻力攻擊,可以向承諾中添加一個 “隨機數” 值,使其採取 poseidon(狀態,隨機數)的形式。隨機數作為額外的私有輸入提供給電路,並且從足夠大的範圍內隨機選擇,以確保預先計算所有可能的承諾是不可行的,從而保留狀態的隱私性。

如果一個證明以某些私有狀態的承諾作為輸入,根據一些規則在內部對狀態進行一些更改,然後輸出對新狀態的承諾,那麼證明就可以有效地代表一個可驗證的私有狀態轉換。如果你將一個證明的輸出承諾作為另一個證明的輸入,那麼你可以隨著時間創建一系列私有狀態轉換。

Snip20231013_70

正是這些對私有狀態的承諾,以及這種隨著時間更新承諾的過程,構成了 ZK Hunt 中移動方式的核心。現在我們已經建立了這種先決條件的理解,我們現在可以看一下四種不同的移動情景:

1. 平原到平原#

Snip20231019_74

單位的位置存儲在 PositionComponent 中。為了讓單位穿越平原,玩家將期望的新位置提交給 PlainsMoveSystem(繼承自 MoveSystem),該系統檢查移動是否有效,然後更新位置組件中單位的位置值。

這種驗證邏輯檢查單位的舊位置和新位置都是平原瓦片,新位置在地圖內,並且移動是單一的基本步驟(曼哈頓距離為 1)。任何對單位公開位置的更新都會反映在所有玩家的客戶端上。

2. 平原到叢林#

Snip20231019_75

進入叢林的過程與上述相同,不同之處在於合約檢查移動到的新位置是叢林瓦片而不是平原瓦片。此外,玩家還提交對新單位位置的承諾(以類似的方式保存到組件中),以及一個 ZK 證明,表明承諾是從新位置正確計算出來的。這個承諾採取 poseidon (x, y, nonce) 的形式。

ZK Hunt 的地圖大小相對較小(31*31,但可以配置得更大 / 更小),這意味著可能的位置總數是有限的,因此需要包含一個隨機數以確保承諾不能被蠻力破解。當然,入口位置已經是公開的,所以沒有必要蠻力破解承諾,但這對未來位置承諾來說將不是這種情況。

這個承諾不是承諾一個私人位置,而是作為隨後在叢林中私下移動的起點。隨機數需要保持私密(稍後會詳細說明為什麼),因此需要 ZK 證明來允許合約檢查承諾是否與位置相匹配,而不必透露隨機數。電路非常簡單:

template PositionCommitment() {
    signal input x, y, nonce;
    signal output out;
    out <== Poseidon(3)([x, y, nonce]);
}
component main {public [x, y]} = PositionCommitment();

在驗證期間,合約為 x 和 y 提供輸入值,並將輸出的承諾保存到相關的 PositionCommitmentComponent 中。這個電路被稱為 PositionCommitment(而不是 JungleEnter,因為它在某一點是這樣的),因為它在其他情況下被重用,需要檢查公開提供的位置是否與一個承諾相匹配,而不揭露隨機數。

3. 叢林到叢林#

Snip20231019_76

在叢林中移動時,玩家不再向合約提交新位置,而是只提交對新位置的承諾,以及一個 ZK 證明,證明之前承諾的舊位置和新位置之間的移動是有效的。這意味著所有其他玩家都知道該單位進行了某些移動,但實際上並不知道該單位移動到了哪個確切的位置。

ZK 證明驗證了從一個私人位置到另一個私人位置的狀態轉換,參考了一個舊的位置承諾,並導致一個新的位置承諾,因此從進入叢林時提交的位置承諾開始,這可以用來創建一個任意長度的通過叢林的移動鏈,其中一個證明的輸出承諾成為下一個的輸入。

新位置承諾的有效性取決於舊位置承諾的有效性(這裡的有效性意味著承諾並不代表單位按照移動規則本不應該到達的位置),因此,即使入口位置也是公開的,但進入叢林時提交初始位置承諾的原因是要以合約已知有效的承諾開始移動鏈。

從玩家 A 的角度來看,單位的位置模糊性在視覺上通過問號的存在顯示出來,每個問號代表單位可能所在的一个潛在位置。剛進入叢林時,如果單位進行了新的移動,那麼它們可能已經移動到了與入口瓦片相鄰的任何叢林瓦片上。如果他們再移動,那麼他們可能已經移動到了他們先前潛在位置相鄰的任何叢林瓦片上,依此類推,這就是你看到的洪水填充行為。

驗證移動正確性的 JungleMove 電路相當簡單:

template JungleMove(mapSize, merkleTreeDepth) {
    signal input oldX, oldY, oldNonce, oldCommitment;
    signal input newX, newY;
    // See MerkleDataBitAccess template for signal explanations
    signal input mapDataMerkleLeaf, mapDataMerkleSiblings[merkleTreeDepth];
	signal input mapDataMerkleRoot;
    signal output newCommitment;
    // Check that the supplied oldX, oldY and oldNonce match the oldCommitment
	// stored in the contract
    signal commitment <== Poseidon(3)([oldX, oldY, oldNonce]);
    commitment === oldCommitment;
    // Check that movement is single cardinal step, and stays within the map
    signal xDiff <== CheckDiff(mapSize)(oldX, newX);
    signal yDiff <== CheckDiff(mapSize)(oldY, newY);
    xDiff + yDiff === 1;
    // Check that the new map cell is of type jungle (1)
    signal bitIndex <== newX + newY * mapSize;
    signal tileType <== MerkleDataBitAccess(merkleTreeDepth)(
        bitIndex, mapDataMerkleLeaf, mapDataMerkleSiblings, mapDataMerkleRoot
    );
    tileType === 1;
    // Calculates the new nonce and outputs the new commitment
    signal newNonce <== oldNonce + 1;
    newCommitment <== Poseidon(3)([newX, newY, newNonce]);
}
component main {public [oldCommitment, mapDataMerkleRoot]} = JungleMove(31, 2);

第一部分檢查舊的 (x, y) 值是否真的是已經承諾的值,oldCommitment 公共輸入是在驗證期間由合約提供的,確保玩家不能對他們的舊位置撒謊。

第二部分使用 CheckDiff 計算每個軸的舊位置和新位置之間的絕對差值,該部分還檢查差值不超過 1,並且新值仍在地圖內:

template CheckDiff(mapSize) {
    signal input old;
    signal input new;
    signal output out;
    signal diff <== old - new;
    out <== IsEqualToAny(2)(diff, [1, -1]);
    // Ensures that the absolute diff is 1 or 0
    signal isZero <== IsZero()(diff);
    out + isZero === 1;
    // Ensures that the new value is not outside the map
    signal isOutsideMap <== IsEqualToAny(2)(new, [-1, mapSize]);
    isOutsideMap === 0;
}

雖然每個軸的 CheckDiff 限制了單位的移動距離為單個瓦片,但該部分末尾的 xDiff + yDiff === 1; 行確保單位只在 x 軸或 y 軸上移動,防止對角線移動。

第三部分檢查新位置是否是叢林瓦片,但邏輯有點複雜,所以稍後再討論。

第四部分輸出新的位置承諾,如果移動成功,合約將其保存為單位的新值。

signal newNonce <== oldNonce + 1;
newCommitment <== Poseidon(3)([newX, newY, newNonce]);

注意,用於新承諾的新隨機數是 oldNonce + 1,而不是作為額外私人輸入提供的新隨機值。這是一個重要的選擇,對稍後討論的一些機制產生了影響,這就是為什麼初始隨機數在進入叢林時需要保持私密。

4. 叢林到平原#

Snip20231019_77

為了離開叢林,玩家必須向合約揭露單位在叢林中的當前位置(因為合約不知道這個位置),以便它可以檢查從叢林瓦片到平原瓦片的移動是否有效。為了防止玩家提供他們所在的叢林區域邊界上的任意叢林瓦片,他們必須證明揭露的叢林位置是與合約中存儲的位置承諾相匹配的位置。

然而,這並不需要提交一個 ZK 證明,因為玩家可以直接揭露單位位置的(x,y)坐標,以及用於位置承諾的隨機數,然後合約簡單地比較這些值的 poseidon 哈希是否與存儲的位置承諾相匹配。離開叢林會導致位置模糊性消失,並且單位的位置會向所有其他玩家公開。

電路內地圖數據檢查 - I#

由於 ZK Hunt 中的地圖瓦片只能是平原或叢林,因此它們的狀態可以用單個比特表示(平原為 0,叢林為 1)。從理論上講,這意味著我們可以將這些值打包到單個整數中,並使用單個 uint256 表示整個 16 * 16 瓦片地圖。

然而,由於(默認的)circom 素數域大小的性質,circom 信號只能表示最大約 2^253.6 的值,因此單個信號只能攜帶 253 個 “有用的” 資訊位。這意味著您無法用單個信號表示一個 16 _ 16 的地圖,但您可以表示一個 15 _ 15 的地圖,這將使用 225 位,這正是 ZK Hunt 的第一個原型所做的。

如果您想檢查電路中給定的(x,y)處的瓦片值,那麼您計算 tileIndex,即 x + y * mapSize(在此示例中,mapSize = 15),將地圖數據信號輸入分解為每個代表單個比特的一系列信號,使用 circomlib 的 Num2Bits (),然後從該數組中選擇第 tileIndex 位(在 circom 中是 O (n) 操作而不是 O (1))。下面是它的樣子(簡化版):

var mapSize = 15;
var mapTileCount = mapSize * mapSize;
signal input x, y;
signal input mapData;
signal mapDataTiles[mapTileCount] <== Num2Bits(mapTileCount)(mapData);
signal tileIndex <== x + y * mapSize;
signal tileType <== SelectIndex(mapTileCount)(mapDataTiles, tileIndex);
tileType === 1;

電路內地圖數據檢查 - II#

如果您想表示大於 15 _ 15 的地圖,比如 22 _ 22 的地圖怎麼辦?嗯,這樣的大小的地圖需要 484 位來表示,所以這將適合兩個信號,第一個信號存儲前 253 位,第二個信號存儲其餘的 231 位。我把這些信號稱為 “地圖數據塊”。在電路中,您將使用 Num2Bits () 將這兩個塊分解為信號數組,連接數組,然後再從數組中選擇第 tileIndex 個元素:

var mapSize = 22;
var mapTileCount = mapSize * mapSize;
var chunk1TileCount = 253, chunk2TileCount = 231;
signal input x, y;
signal input mapDataChunks[2];
// Note, the Concat template doesn't actually exist in circomlib or ZK Hunt,
// but the implementation would be simple
signal mapDataTiles[mapTileCount] <== Concat(chunk1TileCount, chunk2TileCount)(
	Num2Bits(chunk1TileCount)(mapDataChunks[0]),
	Num2Bits(chunk2TileCount)(mapDataChunks[1])
);
signal tileType <== SelectIndex(mapTileCount)(mapDataTiles, x + y * mapSize);
tileType === 1;

您可以通過增加地圖數據塊的數量來擴展這種方法,以表示更大的地圖。然而,因為地圖數據塊需要由合約提供作為公共輸入,地圖越大,驗證成本就越高,因為公共輸入的數量增加了。為了解決這個問題,地圖數據塊可以作為私有輸入傳入,然後根據對塊的公開承諾進行檢查,這意味著無論地圖的大小如何,都只需要一個公共輸入:

signal input mapDataChunks[4]; // Private
signal input mapDataCommitment; // Public
signal commitment <== Poseidon(4)(mapDataChunks);
commitment === mapDataCommitment;
// The map data chunks can now be trusted by the rest of the circuit
...

在之前的場景中,承諾被用來允許玩家在電路中可驗證地引用一些私有狀態,而在這裡,它被用來允許電路引用合約提供的任意大的公共狀態,同時只需要傳遞一個公共信號,而不是包含全部公共數據的足夠數量的信號。需要注意的重要一點是,circomlib 實現的 poseidon 哈希僅支持多達 16 個輸入,但您可以通過像這樣將哈希鏈接在一起來解決這個問題:poseidon (x1, x2, ..., x15, poseidon (x16, x17, ...))。

電路內地圖數據檢查 - III#

儘管這種方法解決了公共輸入數量與地圖大小(就瓦片總數而言)成線性增長的問題,但驗證地圖數據承諾所需的電路內計算仍與地圖的大小成線性增長,對於非常大的地圖,可能導致非常大的電路(許多限制),從而導致更長的證明生成時間。

為了改善這一點,可以將對地圖數據塊的線性 / 鏈式承諾換成默克爾樹承諾,這樣,如果電路中需要檢查單個瓦片,那麼只需計算與默克爾樹相關分支有關的哈希,從而使成本與地圖的大小呈對數關係,而不是線性關係。

4oGlDSa

相關的地圖數據塊(包含正在移動至的瓦片的塊)和用於重構從塊到根的路徑所需的默克爾兄弟節點作為私有輸入傳入,而產生的默克爾路徑的根則與合約作為公共輸入傳入的樹的根進行核對。

這是 ZK Hunt 最終採用的方法,也是 JungleMove 電路的第三部分所做的事情,利用了 MerkleDataBitAccess 電路,除了執行描述的默克爾檢查外,還進行地圖數據默克爾葉子的位分解,並返回提供的 bitIndex 處的相關瓦片值:

這種實現還有一個好處,就是只對包含瓦片的地圖數據塊執行 Num2Bits (),而不是全部,然後只需要從一個塊中的瓦片數量中選擇,而不是所有塊中的瓦片總數(O (n) 操作),但這種優化也可以應用於線性承諾方法,所以兩者之間的主要區別是承諾驗證的效率。

電路內地圖數據檢查 - IV#

上面的示例展示了一個二叉樹,這與 ZK Hunt 中的實現方式相匹配,但這實際上並不是最高效的樹結構證明方式。在電路內,計算具有 2 個輸入的波塞冬哈希(Poseidon (2)(...))需要 240 個約束,但有趣的是,計算具有 16 個輸入的波塞冬哈希(Poseidon (16)(...))只需要 609 個約束,因此,使用六叉樹而不是二叉樹可以讓您獲得最大的回報,儘管這會帶來一些額外的實現複雜性。

當時我並不知道關於波塞冬電路實現的這一事實,這就是為什麼我選擇了二叉樹。然而,即使考慮到這一點,對於 ZK Hunt 用來表示地圖的塊數量(31 * 31 的 4 塊),線性承諾與樹承諾之間沒有區別;在這兩種情況下,它只是 Poseidon (4)(...),對於最多需要 16 塊的地圖,這將是正確的。

即使當您超過 16 個塊以至於存在區別時(線性承諾需要鏈式多個哈希,樹承諾需要多個級別),我有一種預感,由於額外的默克爾邏輯開銷,樹承諾實際上可能比線性的效率低,只有當地圖足夠大時,才會更加高效。樹承諾對於 ZK Hunt 來說絕對是過度的,線性承諾很可能足以滿足大多數用例,但不管怎樣擁有概念證明都是好的。

由於 ZK Hunt 中的地圖數據是作為輸入傳入電路的,而不是硬編碼到電路中或基於在電路中計算的程序生成算法,所以地圖可以被設計為具有任意內容(每場比賽),並且實際上可以隨時間變化。我在開發過程中有一個想法是玩家能夠燒毀叢林瓦片,新的叢林瓦片會隨著時間自然生長,但這樣做會破壞用於計算潛在位置顯示的邏輯。

這裡描述的方法可以用來將任何類型的公共數據集傳入電路,您想從中選擇,就像查找表一樣。這可以是武器傷害值、物品價格、NPC 位置等。ZK Hunt 使用單個比特來表示數據集的每個元素,因為它們只能是兩個選項之一,但可以對實現進行更改,以允許每個元素用任意數量的比特表示,使得每個元素可以取任意數量的值。

最後可能需要知道的有用的事情是,circomlibjs 可以生成的 solidity 實現的波塞冬只能容納最多六個輸入(我認為是由於 EVM 的堆棧深度有限),因此,使用超過六個輸入的波塞冬哈希不能通過合約中的直接計算創建或驗證,但您當然可以通過使用 ZK 證明來解決這個問題,從而每個哈希最多獲得 16 個輸入。

叢林 / 平原移動概括#

在一定程度的概括層面上,上述描述的移動系統允許遊戲擁有隱身區域、非隱身區域,以及實體在這兩者之間移動的能力。特定的平原 / 叢林情境可以為不同類型的遊戲重新設計:

  • 您可以用光亮和陰影區域來替換它,在那裡世界中放置了特定的光源,這些光源徑向地發出光線,而固體障礙物通過阻擋這些光線來創建陰影區域(這個點子歸功於 lermchair)。如果光源可以移動(例如手持燈籠),那麼光亮區域可以隨時間更新,儘管這需要額外的邏輯來處理玩家在未移動的情況下從光明過渡到陰影(更不用說鏈上動態陰影投射計算了)。
  • 基於上述想法,您可以創建一個不對稱的多人遊戲,比如捉迷藏或甚至是《逃殺》(Dead by Daylight),在那裡有一個始終處於公開位置的搜索者,並發出一個可以被障礙物阻擋的視野區域。會有一些隱藏者,他們的位置對搜索者保持私密,直到他們進入其視線,此時他們的位置變得公開並可以更容易地被追捕。
  • 您可以創建一個系統,在該系統中隱身不綁定到地圖的特定區域,而是玩家可以隨時進入隱身,無論他們的位置如何,但只有有限的時間 / 移動次數。有了這個,您可以構建一個遊戲,在這個遊戲中,玩家可以鑽進地下,並私下地隧道到其他地方,但他們被迫重新浮出水面,以免耗盡能量(或其他一些原因),限制可以實現的最大位置模糊性,而無需將隱身鎖定到特定區域。
  • 您可以用基於圖的位置 / 移動(例如,在通過走廊連接的離散房間之間移動)來替換基於網格的位置 / 移動,這會影響在私下移動期間位置模糊性的增長方式。

在更高層次的概括上,平原 / 叢林移動系統代表了一種給予玩家某種狀態的方式,這種狀態可以是公開的,可以過渡到私密(或在某些情況下甚至開始時就是私密的),可以在保持私密的同時有效地更新,並可以被公開揭示。在 ZK Hunt 中,這種狀態用於代表單位的位置,但它可以輕易地代表任何其他類型的狀態,具有任意的更新邏輯:

  • 玩家可以擁有私密的健康狀況,在受到另一位玩家的傷害時私下更新,並且只有在受到足夠的傷害而被殺時才揭示。
  • 玩家可以有一個私人的耐力計和一個私人的位置,他們在移動時耗費的耐力量決定了他們可以移動的距離。這意味著當玩家進行私下移動時,其他玩家將無法判斷他們是否選擇了移動很遠的距離,或是為了保存耐力而移動較短的距離,或是介於兩者之間。這將使位置模糊性(以及對這種模糊性的任何視覺表示嘗試)變得更加複雜。
  • 玩家可以擁有一個私人物品庫,從中他們可以使用產生公共或私人效果的物品(例如,公開傷害其他玩家,或私下治療自己)。為了填充這樣的私人庫存,玩家可以打開箱子,其中包含一系列可能性中的一件物品,每件物品都有不同的發生概率。箱子包含的物品是私下確定的,這樣其他玩家就不知道玩家收到了哪個物品,他們的庫存內容可以保持私密。
  • 或者,也許玩家可以發現另一個玩家庫存的內容,然後私下從他們那裡偷走一件物品。您如何實現這樣的事情將在後續部分變得更加清晰。

顯然,這裡可以做很多事情。然而,必須指出的是,這裡所涉及的核心思想絕不是全新的……

在黑暗森林的肩膀上#

這種私有狀態 / 承諾 / 轉換方案的明顯靈感,甚至是一般利用 ZK 構建鏈上遊戲,顯然來自《黑暗森林》(Dark Forest)。然而,DF 採取的方法與 ZK Hunt 中使用的方法略有不同。先快速回顧一下 DF 的工作原理:

在 DF 中,一個特定位置是否包含行星,取決於該位置的整數坐標的 MiMC 哈希值是否大於某個閾值(mimc (x, y) > threshold)。這意味著,為了讓玩家找出空間的特定區域包含哪些行星,他們只需哈希該區域中的所有唯一位置,並查看每個結果是否大於閾值。這就是您可以在 DF 中逐步 “挖掘” 戰爭迷霧的原因(生成和檢查一堆哈希的方式與比特幣挖礦的工作方式非常相似)。

由於 DF 地圖的龐大尺寸和行星的自然稀疏性,挖掘地圖中的每一個位置都需要相當多的時間(除非您擁有足夠強大的硬件),因此您被迫戰略性地選擇利用您的哈希算力開採的地圖的特定區域。

eF5OO0n

在 DF 中,玩家可以同時向多個行星發送資源並占領它們,因此一個 “玩家” 實際上可以同時在多個位置,但為了簡化與 ZK Hunt 的比較,我們將假設玩家一次只能在一個行星上。以下解釋仍應成立。

當玩家在其初始行星上出現或移動到另一行星時,行星位置的哈希值會作為位置承諾提交給合約,而不是直接提交位置,並附上一个 ZK 證明,顯示承諾的有效性(與哈希對應的位置實際上包含一個行星,如果是移動,則新行星與舊行星的距離不超過閾值),這就是玩家能夠保持位置隱私的方式,與 ZK Hunt 的方式相同。

一旦您根據其位置哈希發現某個位置存在行星,那麼您可以通過將他們最近提交的位置承諾與該位置哈希進行比較,來看看哪些玩家(如果有的話)在那行星上。現在,讓我們花一點時間來建立這些尋找行星和尋找玩家的過程是如何融入更廣泛的概念框架中的。

鏈上世界發現 / 玩家發現#

在 DF 中通過挖掘戰爭迷霧來發現行星是我所稱的 “鏈上世界發現” 更大類別中的一種特定方法。簡單的定義是:遊戲世界的(非玩家)內容 / 狀態最初對玩家是隱藏的,玩家必須執行某些操作才能逐漸隨時間發現它。

最明顯的例子可能是您在 DF 或傳統策略遊戲中看到的戰爭迷霧系統,您可以揭露世界的地形 / 佈局、物品、NPC 等,但世界發現的最廣泛定義也可以包括像了解 NPC 的背景故事,甚至通過資源組合創建新穎物品之類的事情。

由於 ZK Hunt 的地圖從一開始就完全公開,因此世界發現的主題與 ZK Hunt 關係不大,所以我不會在這篇文章中更深入地討論它,但您可以在這裡找到我對世界發現不同方法的討論,我將在未來的項目中探索一些新方法。

另一方面,通過在 DF 中挖掘戰爭迷霧來發現玩家,我認為這是一種 “鏈上玩家發現” 的特定方法,這與 ZK Hunt 有更直接的關係,正如名稱所暗示的。再次簡單定義:玩家(和 / 或玩家控制的實體)擁有私密的位置,其他玩家可以通過執行特定操作來發現。更完整的定義可能會擴展到發現玩家的所有私密屬性(例如他們的健康狀況,他們擁有的物品等),但現在我們只關注位置。

甚至可能有意義在定義上建立世界 / 玩家發現作為嚴格的二分法;發現屬於 “玩家” 的事物,和發現屬於 “非玩家” 的事物,但是如果建立一個複雜的非玩家代理的第三類別更有意義,那麼這可能會崩潰,因為他們的發現過程與世界發現足夠不同。

在我探索玩家發現的各種方法時,我遇到了幾個不同的子類別。我的當前模型如下:

  • 公开玩家发现 - 发现玩家会向每个人揭露他们的位置
  • 私人玩家发现 - 发现玩家只向发现者揭露他们的位置
    私人发现的子类别:

对称玩家发现 - 发现玩家也会导致他们发现你

非对称玩家发现 - 分为三个层级,每个层级都继承了前一层级的权限:

  • 在不让玩家发现你的情况下发现玩家(但他们确实会知道搜索是否成功)
  • 在不让玩家知道您是否成功搜索他们的情况下发现玩家(但他们确实会知道您搜索了他们)
  • 在不让玩家知道您根本搜索了他们的情况下发现玩家

正如您所看到的,模型越深入,信息泄露就越少。考虑到区块链的本质上公开性,我发现通常情况下,类别的隐私性越强,实现的难度 / 复杂性就越大。展望未来,我们可以使用这个框架来评估 DF 和 ZK Hunt 用于玩家发现的方法。

黑暗森林分析#

由于 DF 的战争迷雾挖掘和行星 / 玩家位置哈希比较完全是外部过程,不需要与游戏世界 / 合约逻辑 / 其他玩家直接互动,DF 的方法实现了最高层次的不对称玩家发现。然而,它并不完美,有几个缺点:

空间上无限制:玩家可以选择挖掘地图的任何部分,而不论他们的位置。可以说,鉴于世界叙事(将你的望远镜对准遥远的太空),这对 DF 来说在某种程度上是有意义的,但对于其他类型的游戏,无法基于玩家的位置限制发现是这种方法的一个重大缺点。如果你在玩 PvP 地牢游戏,那么你不应该能够发现离你很远的玩家。玩家发现应该有能力被本地化。

时间上无限制:玩家发现新行星的速度,以及因此在这些行星上可能发现的任何玩家,基本上取决于他们计算哈希的速度,因此取决于他们的硬件有多强大。这创造了一个本质上不平等的竞技场,通过投入额外的资本可以使其更加不平等。你发现玩家的能力应该完全由世界的规则 / 状态决定(例如,你的角色的移动速度,你的望远镜的强度,玩家之间的障碍等)。

永久性:一旦你找到一个星球并确定了它的位置哈希,你将能够在游戏剩余时间里看到任何到 / 离它的玩家移动。当涉及到发现世界时(至少对于静态属性而言),永久性发现可能是好的,但当涉及到发现玩家时就不那么是了。仅仅因为你访问了一个特定区域,并不意味着你应该能够看到在你离开后访问该同一区域的玩家。玩家发现应该具有动态性和非永久性。

在 DF 中,玩家发现在某种程度上可以被认为是世界发现的副产品,因此上面列出的缺点实际上只是适用于世界发现的同样缺点的延伸。不要误会,DF 用于允许私人世界和玩家发现的系统设计相当不可思议,并且在其用例中运作良好,但如果有一种方法不受这些缺点的影响,那将会更好。

在 DF 中支撑玩家发现的基本机制是能够 “猜测和检查”(计算并比较)位置哈希的能力,行星的存在只是为了缩小你实际检查的位置哈希。这是可能的,因为玩家提交的位置承诺只是位置的哈希;mimc (x, y)。DF 地图足够大,以至于搜索整个地图并非易事,但也不是如此之大,以至于随机放置在地图中的玩家永远没有找到彼此的机会。

ZK Hunt 中的位置承诺与 DF 的不同之处在于它包括了私有随机数;poseidon (x, y, nonce),选择 poseidon 而不是 mimc 没有功能差异(它只是电路更有效率)。这种添加特别阻止了 “猜测和检查” 位置哈希的能力,尽管 ZK Hunt 的地图明显小于 DF 的,因此你不能通过这种方式找到玩家。如果情况是这样,那么 ZK Hunt 中的玩家发现是如何工作的?你如何与在丛林中位置模糊的玩家互动?

— 矛 —#

矛是一组线性排列的四个 “击中瓦片”(或更通用地说,是 “挑战瓦片”),从玩家身上延伸出去,你可以朝 32 个不同的方向之一瞄准。为了展示矛的一些方面,我们还会引入第三个玩家,玩家 C,位于右下角。玩家 C 不控制任何单位,他们只是作为第三方观察者,代表游戏中所有其他玩家的视角。

在上面的视频中,你看到玩家 A 向平原上玩家 B 的一个单位投掷矛,导致他们被杀并丢下他们的战利品,然后玩家 A 的单位捡起来。然后玩家 B 将他们的另一个单位送入丛林中以获得一些位置模糊性,玩家 A 试图向他们投掷矛。第一次尝试没有命中,这种模糊性得以保持,但第二次尝试成功了,导致该单位的位置被揭示,它死亡并丢下其战利品。

这是 ZK Hunt 包含的第一个工具,允许你与在丛林中位置不明的单位互动。矛既是一种战斗能力,也是一种玩家发现方法,但这些方面可以独立存在。你可以换成以相同方式投掷的简单石头,如果它击中了它们,它会让一个单位 “大声喊叫”,揭示它们在丛林中的位置而不杀死它们。

被矛击中会向所有玩家揭示单位的位置,由玩家 C 也了解到单位位置的事实说明,所以矛会被分类为公开玩家发现的方法。让我们了解它是如何工作的…

击中瓦片#

这种 4 个击中瓦片的线性排列实际上是任意的,我们可以有任何数量的击中瓦片的任何种类的排列。你可以用这个创建一个俱乐部,利用更小的、弧形的击中瓦片,或者也许是一个炸弹,产生一个更大的圆形区域的击中瓦片,远离投掷它的单位。

矛可以被投掷的方向数量,以及每个方向的瓦片排列也是完全任意的。完整的排列集合被硬编码在合约中,作为每个方向的偏移量列表。执行矛投掷时,选择的 directionIndex 会被发送到合约,然后通过将一组偏移量(对应于该方向)加到单位的位置来确定击中瓦片的结果位置。

击中瓦片经历 3 个阶段:

Snip20231019_78

  1. 潜在的(以半透明白色显示):当玩家还在瞄准矛时。
  2. 待定的(以实心白色显示):玩家已经确认投掷并向合约提交了方向。
  3. 已解决的(以红色显示):合约已确定击中瓦片是否击中了任何东西。击中瓦片是基于逐个瓦片解决的,因为有些瓦片可能在其他瓦片之前解决。

矛投入丛林#

在视频中,玩家 A 向丛林中的玩家 B 的单位投掷他们的矛,第一次尝试未命中,第二次尝试成功。但等一下,如果玩家 A 和合约都不知道单位的确切位置,他们怎么知道矛投掷尝试是否成功命中呢?答案是我们迫使玩家 B 揭示他们是否被击中,使用一个挑战 / 响应过程。

提交矛投掷时,合约可以立即确定哪些(如果有的话)单位被平原上的瓦片击中,并在同一交易中杀死它们。然而,如果任何击中瓦片落在丛林中,那么每个在丛林中的单位都会被放置一个 “待定的挑战”,这意味着该单位不能执行任何动作,直到挑战被清除(由 ActionLib 强制执行)。

为了清除一个单位的待定挑战,拥有玩家有两个有效的选项:

  1. 他们没有被击中,这种情况下他们生成一个显示这一事实的零知识证明(ZK proof),提交给合约,并通过这样做保持他们位置的模糊性。这就是你在第一次尝试中看到的情况。

  2. 他们被击中,这种情况下他们不能生成这样的证明,所以他们公开提交他们的位置给合约,接受击中并丢下他们的战利品。这就是你在第二次尝试中看到的情况。

这就是为什么在视频中,落在丛林中的击中瓦片解决得比平原中的击中瓦片更慢;因为它们必须等待被挑战玩家的响应。

用于选项 1 的 JungleHitAvoid 电路相当直接,第一部分与 JungleMove 电路相匹配:

template JungleHitAvoid(hitTileCount) {
    signal input x, y, nonce, positionCommitment;
    signal input hitTilesXValues[hitTileCount], hitTilesYValues[hitTileCount];
    signal input hitTilesCommitment;
    // Checks that the supplied x and y match the commitment
    signal commitment <== Poseidon(3)([x, y, nonce]);
    commitment === positionCommitment;
    // Checks that the passed (x, y) aren't part of the hit tiles, and that
		// the hit tiles match the supplied hit tiles commitment
    signal wasHit <== CoordSetInclusion(hitTileCount)(
        x, y, hitTilesXValues, hitTilesYValues, hitTilesCommitment
    );
    wasHit === 0;
}
component main {public [positionCommitment, hitTilesCommitment]} = JungleHitAvoid(4);

类似于 JungleMove 中处理地图数据的方式,击中瓦片的 x 和 y 值不是作为公共信号传入,而是作为私有信号,只有一个承诺值作为公共信号传入。对于 JungleMove,只有来自总集的特定地图数据块对电路来说是相关的,所以当块的数量变大时,一个树形承诺是有意义的,但对于挑战瓦片,需要每个集合中的每个瓦片,因为它们都需要被检查,所以无论瓦片的总数是多少,线性承诺都是足够的。

这与 JungleHitAvoid 的第二部分相关,在那里 CoordSetInclusion 电路确定单位提供的位置是否与任何击中瓦片匹配,并检查击中瓦片是否与提交矛投掷时计算的公共承诺匹配(poseidon (x1, poseidon (x2, poseidon (x3, ...))))。

在当前的实现中,如果有任何一个击中瓦片在丛林中,那么每个当前在任何丛林瓦片中的单位都会被放置一个待定的挑战,无论它们离击中瓦片有多近。这显然非常低效,因为许多没有被击中机会的玩家仍然必须回应。可以进行一些优化,例如只在被击中瓦片触及的特定丛林区域的单位上放置待定挑战,或者只在触及其潜在位置的单位上,但我选择了最简单的方法。

惩罚不响应#

当一个待处理的挑战被放置在一个单位上时,除了上述的两个选项外,玩家实际上还有第三个选择,这个选择并没有被合约逻辑直接阻止,但是违反了 “游戏规则”;他们可以选择根本不提交任何响应。

这意味着该单位实际上是被冻结的,这并不直接提供任何好处,但是如果玩家控制多个单位,或者与其他玩家合作,那么通过让对手等待一个永远不会到来的回应来拖延对手的时间可能是有利的。即使这样做没有好处,它仍然是一个简单的方法来使其他玩家感到不快,那么我们如何防止这种行为呢?

让我们回到起点。当进入游戏时,每个玩家都要放下一个押金来玩。如果他们在按规则玩过游戏后离开游戏,那么他们可以取回这个押金。每个待处理的挑战都有一个有限的响应期。如果一个单位收到了待处理的挑战,而拥有单位的玩家在该期间内没有响应,那么他们可以被任何人惩罚(slashing),导致他们的押金被清算,所有的单位被消灭。

在这段视频中,我已经阻止了玩家 B 的客户端响应挑战,这样在向丛林中的单位投掷矛后,等待大约 5 秒的响应时间后,玩家 A 惩罚了玩家 B,导致他们的两个单位都死亡并丢下了他们的战利品。一旦玩家的客户端检测到响应期已经结束而没有提交响应,惩罚就会自动执行,并通过清算合约进行。

请注意,在整个代码库中,我一直将该过程称为 “清算”,只是后来决定 “惩罚” 是一个更合适的术语。还要注意的是,我实际上并没有实现让玩家在进入游戏时下押金的能力,所以惩罚只会杀死玩家的单位而不会拿走所说的押金,但添加这一功能是相当简单的。

泛化惩罚#

尽管长矛的作用是揭示单位的私密位置,但底层的挑战 / 响应 / 惩罚系统可以用来强迫玩家揭示任何类型的私密状态。

更一般地考虑,可惩罚的押金的存在可以用来确保玩家执行世界要求的任何类型的互动。合约和电路逻辑可以用来确定这种互动的确切性质,而零知识证明的使用可以允许在过程中包含私密状态。ZK Hunt 中后续的机制进一步探索了这个想法。

响应期应该调整以匹配生成响应、提交它,并由合约接受所需时间的预期上限,同时要有足够的错误边际来考虑玩家在硬件性能和网络延迟方面的差异。

为了使惩罚的威胁有效,押金应该足够大 / 重要,以至于其损失对玩家来说比不采取行动获得的任何价值更为重要。这种押金可以有不同的形式;代币、游戏内物品、声誉、活了一段时间的角色的经验 / 等级等。

将游戏的部分内容限制在只有一定押金金额的玩家(例如不同的匹配等级、地下城的难度等级等),但允许玩家通过游戏内行动随时间增加他们的押金,是防止因押金的前期成本而在游戏的任何部分限制参与的好方法。

惩罚的威胁应该提供足够的激励以不违反规则,但如果规则无论如何都被打破,那么重要的是要有回退逻辑。如果游戏使用基于 1 对 1 比赛的系统,那么回退逻辑很简单;比赛可以立即结束,并将 “胜利”(及任何其他相关后果)归于没有被惩罚的玩家。

世界完整性#

然而,如果有两个以上的玩家,并且游戏旨在拥有一个更持久的世界(如 ZK Hunt 的情况),那么这种回退逻辑可能会根据世界 / 惩罚上下文的性质变得更加复杂,并且应该被设计为最大限度地保持 “世界完整性”。我认为世界完整性是 “世界行为如何紧密符合玩家期望的度量标准,同时考虑到由其设计产生的一整套后果”。

例如,在 ZK Hunt 中,期望 “如果丛林中的一个单位被矛击中,那么它应该死亡并掉落战利品” 即使发生惩罚也会得到强烈的保持。相反,期望 “如果丛林中的一个单位被矛击中,那么它应该在大约 x 秒内死亡”(其中 x 是玩家响应被击中和后果传播给所有其他玩家所需时间的合理上限)没有得到强烈的保持,因为可能发生不响应,这导致在惩罚发生之前的等待期超过了 x。

同样,期望 “如果一个单位在丛林中被杀死,那么它的战利品应该在与单位相同的瓦片中掉落” 也没有得到强烈的保持。如果一个玩家因为没有回应挑战而被惩罚,那么合约不知道他们的单位在丛林中的确切位置,它能做的最好的就是将战利品放在单位的最后已知位置(丛林入口瓦片)。从另一个确实知道单位位置的玩家(通过后面讨论的方式获得)的角度来看,这可能看起来像是战利品在瞬移。

你可以尝试改善这个情况,比如改为在与单位可能位置相交的被击中的瓦片之一中掉落战利品,这基于这样的概念:如果玩家没有响应,那么他们最有可能被长矛击中。尽管这仍然允许瞬移,但这种瞬移的可能幅度被最小化了。然而,这种方法导致了更复杂的实现,并且不处理没有一个被击中的瓦片与单位的任何可能位置匹配的情况,这就是我选择更简单方法的原因。

为了从惩罚中获得便利,你可能不得不牺牲一定程度的世界完整性。上面的两个 ZK Hunt 示例强调了惩罚的两个重要考虑因素:时间敏感性和私有状态回退。

惩罚曲线#

在上述挑战 / 应对系统中,如果玩家未能及时回应,他们的押金将会被罚没。一个策略性的恶意行为者会等到回应期快结束时才作出回应,这样既可以避免被罚没押金,同时还能延误等待回应的其他玩家。

将玩家损失的金额与他们在回应前等待的时间作图,图表如下:

0kdryI8

显然,我们想要通过惩罚来阻止这种拖延行为,因此我们可以在玩家回应时罚没他们押金的一部分,而不仅仅是在一个严格的截止时间之后才罚没玩家的全部押金,罚没的比例由他们等待的时间长短来决定:

Ptf7vI2

在末端依然有一个严格的截止点;如果他们在这个时间点之前回应,他们的押金会被罚没一部分,但他们还能继续参与游戏,而如果他们在截止时间之后回应 / 根本不回应,那么他们的全部押金会被罚没,并且他们将被移除游戏。

这样做最低程度地惩罚了及时回应的玩家,同时大幅惩罚了试图延迟回应的玩家。一个恶意行为者可能会量化他们通过延迟回应时间 t 所获得的价值,然后找到曲线上的一个点,在这一点上,该价值减去罚没的金额达到最大。

可以调整曲线,以影响这类行为者的反应,甚至可以有一种自我调节的曲线,根据玩家平均回应时间的长短不断进行调整。

信息披露的程度#

投矛是一种与位置不明的单位互动的方法,也是迫使他们揭示位置的一种方式,但可以进行更改,以限制实际披露的信息量。当丛林中的一个单位被投矛击中并死亡时,揭示其位置是有意义的,因为合约需要知道在哪里掉落战利品。然而,如果每个单位都有一定的 HP,并且需要多于一次的投矛击中才能杀死,那么情况就不再一定如此。

如果一个单位被击中但没有死亡,那么你可以只强迫拥有玩家揭示它被击中的事实,以便合约可以减少其健康值,而无需揭示其确切位置(使用零知识证明)。这实际上将单位可能的位置减少到了它可能被击中的瓦片集合。

如果单位有私密的健康状况,那么你可以更进一步;被挑战的玩家通过提交一个新的健康承诺来回应,如果他们被击中,承诺会减少健康值,如果他们没有被击中,健康值保持不变(通过零知识证明强制执行)。这意味着其他玩家只看到单位健康承诺的更新,因此不知道单位是否被击中,从而保持了单位的完全位置模糊性。

你甚至可以同时存在所有三种信息披露程度,并根据单位拥有的某些属性或物品来确定使用哪一种。

丛林中的行动#

接着第一段投矛视频的内容,我们将看到你也可以在丛林中进行操作。

在丛林中杀死 B 玩家的单位后,A 玩家捡起了掉落的战利品。请注意,这样做迫使 A 玩家揭示他们单位的位置,以便合约能够确保他们确实处于正确的位置来拾取战利品,但之后他们能够立即重新隐入丛林,并重新获得位置的模糊性。从丛林中投掷矛的行为也是一样,单位的位置必须向合约揭示,以便它可以确定应该是哪一组被击中的瓦片。

正如你之前看到的,当退出丛林时,玩家揭示单位的位置和随机数,合约检查这些值的波塞冬哈希是否与存储的位置承诺相匹配。在丛林中进行操作与此过程不同,它只揭示位置,并使用一个零知识证明来表明它与承诺相匹配。这意味着随机数可以保持私密,因此丛林中的移动可以从相同的承诺继续,而不是必须建立一个带有新随机数的新承诺。

这个证明重复使用了 PositionCommitment 电路(进入丛林时使用的相同电路),并且在丛林中捡起战利品和投掷矛的合约都委托给一个处理以这种方式揭示单位位置的逻辑的单一合约。

如前所述,投矛属于公开玩家发现的范畴,但我们能做得更好吗?我们能实现私人玩家发现吗?

— 搜索 —#

为此,我们有了搜索能力。虽然它使用与投矛相同的线性挑战瓦片构造,但搜索不是一种战斗能力,而是一种信息收集能力。

这里我们看到玩家 A 搜索玩家 B,但没有成功,什么也没了解到。他们再次尝试,搜索定位到玩家 B,他们的位置就被揭露了。这与你刚才看到的投矛是同样的情况(不包括死亡),但在这种情况下,只有玩家 A 得知了玩家 B 的位置,玩家 C 并不知道,这一点通过玩家 C 的视角下持续存在的模糊性指标得以体现。事实上,玩家 C 不仅没有得知该单位的位置,他甚至不知道玩家 A 的搜索是否成功。这实现了私人玩家发现。

当玩家 B 的单位(被玩家 A 找到后)在丛林中进行额外的移动时,从玩家 C 的视角来看,位置的模糊性会如预期的那样继续增长,但玩家 A 会保持对其位置的精确了解。这允许玩家 A 有效地随时间追踪玩家 B 的单位,这种追踪行为将持续,直到单位退出并重新进入丛林,在这一点上,该单位可以重新获得相对于玩家 A 的位置模糊性。

搜索如何工作?#

与投矛相似,玩家 A 向合约提交搜索方向,合约确定结果挑战瓦片并在丛林中对玩家 B 的单位发起待决挑战。当玩家 B 看到这个待决挑战时,他们不是直接向合约提交他们的位置(如果成功的话),而是将他们的位置承诺随机数加密(有效地揭露他们的位置,稍后会详细说明),以便只有玩家 A 能够解密,然后提交密文以及证明加密操作正确的证明。

这解释了玩家 A 如何能够私下了解玩家 B 的位置,但玩家 C 是如何被阻止至少找出玩家 A 的搜索是否成功的呢?他们肯定可以观察玩家 B 的回应,如果他们提交了密文,这意味着他们正在发送他们加密的随机数,也就是说玩家 A 搜索成功,对吧?

不,因为无论玩家 B 是否在搜索中被抓获,他们都会提交一个密文和相关的证明,但这个密文只有在他们在搜索中被抓获时才包含他们的随机数,否则将包含一个无意义的值。这意味着,尽管玩家 A 的搜索可能成功或失败,但作为外部观察员的玩家 C 无法分辨差异。

w98STOb

夜市#

我对惩罚的思考从用矛解决非响应问题的方式变为允许更一般的 “强制互动”,这一机制的出现是这种思考的结果。如果,在有可惩罚的存款和合约 / 电路逻辑的情况下,你可以强迫玩家以特定的方式响应挑战,那么为什么不只是强迫他们用加密的方式响应他们的位置(或位置承诺随机数),这样只有挑战者才能解密它呢?

你必须验证加密在电路内部是正确完成的,所以下一个问题就变成了哪些加密方法对 SNARK(零知识证明)是友好的?我简单查了一下,意识到这个问题已经由 Dark Forest Nightmarket 插件解决了。

Nightmarket 插件是一种在 Dark Forest 中私密且可验证地出售行星坐标的方式。玩家可以发布列表,出售他们发现的行星的坐标,买家可以通过向托管合约存入一些以太坊来提出报价,卖家可以通过使用对称加密私下向买家揭露坐标来接受报价,并提取存款。

使用零知识证明来确保坐标实际上包含一个行星,并且加密是正确完成的,尽管完整的过程比这里描述的要复杂一些,所以你可以在上面链接的博客文章中了解更多。我最终重用了 Nightmarket 的一些共享密钥派生和波塞冬密码电路,而它本身已经对 maci 和 Koh Wei Jei 的这个 circomlib pr 中的一些电路进行了更改。

尽管 Nightmarket 和 ZK Hunt 中的搜索都涉及私下分发信息,但两者之间有一个有趣的区别是 Nightmarket 中的销售是自愿互动;一旦制作了列表,买家选择提出报价,卖家选择是否接受,而搜索是非自愿的互动;一旦对玩家提出挑战,如果他们没有正确响应,他们就会被惩罚。

搜索实现#

对于搜索(以及夜市),Poseidon 密码被用作对称加密方案,因为它比传统的非对称加密方案更适合于 snark,可能的原因与 Poseidon 哈希比传统哈希函数更适合于 snark 的原因类似(它们共享了部分电路实现的一些部分)。

两个玩家之间的共享密钥是通过一个 “离线” ECDH 密钥交换来建立的。实际上,这意味着当所有玩家进入游戏时,他们都会建立一个私钥 / 公钥对,并将他们的公钥提交给合约。有了你的私钥和另一个玩家的公钥,你可以在

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。