本教程分享基于o1js2.*开发一个去中心化应用(DApp),构建一个基于零知识证明(ZKP)的寻宝游戏。游戏中,玩家扮演精英盗贼,需完成一系列盗宝任务。通过零知识证明,玩家可以向系统展示任务完成的真实性,同时保护任务细节(如密码、路线或地点等)。整个游戏采用去中心化架构,智能合约确保公平性,
本教程分享基于 o1js 2.* 开发一个去中心化应用(DApp),构建一个基于零知识证明(ZKP)的寻宝游戏。游戏中,玩家扮演精英盗贼,需完成一系列盗宝任务。通过零知识证明,玩家可以向系统展示任务完成的真实性,同时保护任务细节(如密码、路线或地点等)。整个游戏采用去中心化架构,智能合约确保公平性,玩家之间可以竞争宝藏。
任务创建者
玩家
使用 zkApp CLI 初始化项目并安装必要依赖。
npm install -g zkapp-cli
zk project SecretHeist
定义任务结构体 HeistTask
,用于存储任务信息。
class HeistTask {
id: number; // 任务ID
target: Field; // 目标哈希(如密码或位置的哈希值)
reward: UInt64; // 奖励金额
deadline: UInt32; // 截止时间
}
实现 verifyTaskSolution
方法,验证玩家提交的解决方案哈希是否匹配任务目标哈希。
verifyTaskSolution(taskTarget: Field, solutionHash: Field): Bool {
return taskTarget.equals(solutionHash);
}
在 SecretHeist
合约中实现两个主要方法:
createTask
玩家可以创建新任务并设置目标哈希、奖励金额和截止时间。
输入参数:
target
: 目标哈希(密码哈希或位置哈希) reward
: 奖励金额 deadline
: 截止时间 主要逻辑:
// 创建新任务
@method async createTask(target: Field, reward: Field, deadline: UInt64) {
const currentCounter = this.taskCounter.get();
this.taskCounter.requireEquals(this.taskCounter.get());
const newTask = new HeistTask({
id: currentCounter,
target,
reward,
deadline,
});
// 更新状态
this.tasks.set(newTask);
this.taskCounter.set(currentCounter.add(1));
// Emit task created event
this.emitEvent('task-created', currentCounter);
}
submitSolution
玩家完成任务后提交解决方案。
输入参数:
taskId
: 任务ID solution
: 解决方案 主要逻辑:
verifyTaskSolution
方法验证解决方案是否正确。// 提交任务解决方案
@method async submitSolution(taskId: Field, solution: Field) {
const task = this.tasks.get();
this.tasks.requireEquals(this.tasks.get());
// 确保任务存在且未完成
task.id.assertEquals(taskId);
// 验证当前时间是否在截止时间之前
this.network.globalSlotSinceGenesis.requireEquals(this.network.globalSlotSinceGenesis.get());
this.network.timestamp.get().lessThan(task.deadline).assertTrue();
// 验证解决方案
const isCorrect = task.verifyTaskSolution(solution);
isCorrect.assertTrue();
// 更新任务状态为已完成
const completedTask = new HeistTask({
...task,
});
this.tasks.set(completedTask);
// Emit solution submitted event before the method ends
this.emitEvent('solution-submitted', taskId);
// 这里可以添加奖励发放逻辑
}
beforeEach(async () => {
// 初始化本地区块链实例,关闭证明以提升测试速度
const Local = await Mina.LocalBlockchain({ proofsEnabled: false });
Mina.setActiveInstance(Local);
// 获取测试账户及其密钥
[deployerAccount, senderAccount] = Local.testAccounts;
deployerKey = deployerAccount.key;
senderKey = senderAccount.key;
// 部署合约实例
zkAppPrivateKey = PrivateKey.random();
zkAppAddress = zkAppPrivateKey.toPublicKey();
zkApp = new SecretHeist(zkAppAddress);
// ... 在这里添加部署交易逻辑
});
it('should create a new task', async () => {
// 准备测试数据
const secret = Field(123); // 密码
const target = Poseidon.hash([secret]); // 计算目标哈希
const reward = Field(1000); // 奖励
const deadline = UInt64.from(Date.now() + 3600000); // 截止时间为1小时后
// 发起任务创建交易
const txn = await Mina.transaction(deployerAccount, async () => {
await zkApp.createTask(target, reward, deadline);
});
await txn.prove();
await txn.sign([deployerKey]).send();
// 验证任务是否成功创建
const taskCounter = await zkApp.taskCounter.get();
const task = await zkApp.tasks.get();
expect(taskCounter).toEqual(Field(1)); // 任务计数器应更新
expect(task.target).toEqual(target); // 验证任务哈希
expect(task.reward).toEqual(reward); // 验证奖励
expect(task.deadline).toEqual(deadline); // 验证截止时间
});
it('should successfully submit correct solution', async () => {
// 创建测试任务
const secret = Field(123);
const target = Poseidon.hash([secret]);
// ... 任务创建逻辑
// 提交正确解决方案
const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.submitSolution(Field(0), secret); // 使用正确的secret
});
await txn.prove();
await txn.sign([senderKey]).send();
// 验证任务状态更新
const task = await zkApp.tasks.get();
expect(task.id).toEqual(Field(0)); // 验证任务ID
});
it('should fail when submitting wrong solution', async () => {
// 创建测试任务
// ... 任务创建逻辑
const wrongSolution = Field(456); // 模拟错误的解决方案
// 验证提交错误解决方案时交易失败
await expect(async () => {
const txn = await Mina.transaction(senderAccount, async () => {
await zkApp.submitSolution(Field(0), wrongSolution); // 使用错误的解决方案
});
await txn.prove();
await txn.sign([senderKey]).send();
}).rejects.toThrow(); // 预期抛出错误
});
上述测试用例全面覆盖了合约的关键功能,确保:
在部署合约到 Devnet 之前,需要确保正确配置网络环境。zk config
提供了配置工具,可以方便地设置相关参数。
运行 zk config
:执行以下命令以进入 Devnet 配置界面:
zk config
设置 Devnet 参数:在配置过程中,主要填写以下关键信息:
devnet
网络。 验证配置:完成设置后,可运行以下命令查看当前的配置是否正确:
zk config show
输出应包含 devnet
网络的相关参数,如节点地址和账户信息。
完成配置后,使用以下命令将合约部署到 Devnet:
zk deploy devnet
部署成功后,终端会返回合约地址(通常是公共密钥 zkAppAddress
)。记录此地址,后续测试将用到。
实现两个页面:
以下详细说明 创建任务页面 的主要功能与实现:
const [taskData, setTaskData] = useState({
title: '',
difficulty: '中等',
description: '',
reward: '',
target: '',
deadline: 7, // 默认截止时间为 7 天
});
const [loading, setLoading] = useState(false); // 加载状态
const [error, setError] = useState(''); // 错误信息
useState
管理任务表单数据 (taskData
)、加载状态 (loading
) 和错误提示 (error
)。taskData
包含了创建任务所需的所有字段,便于统一管理表单数据。const { wallet, setWallet } = useWallet(); // 使用上下文管理钱包状态
const handleConnectWallet = async () => {
try {
const account = await connectAuroWallet(); // 调用 Auro 钱包的连接方法
if (account) {
setWallet(account); // 成功连接后保存钱包地址
} else {
throw new Error('钱包连接失败');
}
} catch (err) {
setError(err.message || '无法连接钱包');
}
};
connectAuroWallet
与 Auro 钱包建立连接。wallet
地址,供后续交易使用。const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!wallet) {
setError('请先连接 Auro 钱包');
await handleConnectWallet(); // 引导用户连接钱包
return;
}
setLoading(true); // 开始加载状态
try {
// 初始化 Mina 网络实例
const network = Mina.Network({
mina: 'https://api.minascan.io/node/devnet/v1/graphql/',
archive: 'https://api.minascan.io/archive/devnet/v1/graphql/',
});
Mina.setActiveInstance(network);
// 构造任务数据
const rewardAmount = Field(taskData.reward);
const deadlineTimestamp = UInt64.from(Date.now() + taskData.deadline * 86400000); // 以天为单位计算截止时间
const targetHash = Poseidon.hash([Field(taskData.target)]); // 计算任务目标哈希
// 创建并发送交易
const tx = await Mina.transaction(PublicKey.fromBase58(wallet), async () => {
await zkApp.createTask(targetHash, rewardAmount, deadlineTimestamp);
});
await tx.prove(); // 生成证明
const { hash } = await tx.sign([PrivateKey.fromBase58(wallet)]).send(); // 签名并发送交易
console.log(`交易成功,哈希值为: ${hash}`);
} catch (err) {
setError(err.message || '任务创建失败');
} finally {
setLoading(false); // 无论成功或失败都结束加载状态
}
};
targetHash
) 和截止时间 (deadlineTimestamp
)。 zkApp.createTask
方法完成操作。 error
状态,便于在 UI 上展示。return (
<div className={styles.container}>
<Head>
<title>创建任务</title>
</Head>
<main className={styles.main}>
<div className={styles.taskContainer}>
{/* 钱包连接区域 */}
<div className={styles.walletSection}>
<button onClick={handleConnectWallet}>
{wallet ? `已连接:${wallet}` : '连接钱包'}
</button>
</div>
{/* 创建任务表单 */}
<form onSubmit={handleSubmit} className={styles.createTaskForm}>
{/* 表单字段 */}
<div className={styles.formGroup}>
<label htmlFor="title">任务标题</label>
<input
id="title"
value={taskData.title}
onChange={(e) =>
setTaskData({ ...taskData, title: e.target.value })
}
required
/>
</div>
<div className={styles.formGroup}>
<label htmlFor="difficulty">难度</label>
<select
id="difficulty"
value={taskData.difficulty}
onChange={(e) =>
setTaskData({ ...taskData, difficulty: e.target.value })
}
>
<option value="简单">简单</option>
<option value="中等">中等</option>
<option value="困难">困难</option>
</select>
</div>
{/* 其他输入字段省略... */}
{/* 提交按钮 */}
<button type="submit" disabled={loading}>
{loading ? '创建中...' : '创建任务'}
</button>
</form>
{/* 错误信息提示 */}
{error && <div className={styles.error}>{error}</div>}
</div>
</main>
</div>
);
styles.*
),保持组件样式隔离。const [taskId, setTaskId] = useState<string>(''); // 当前选中任务的ID
const [solution, setSolution] = useState<string>(''); // 用户输入的解决方案
const [loading, setLoading] = useState<boolean>(false); // 加载状态
const [taskList, setTaskList] = useState<any[]>([]); // 可选任务列表
const [selectedTask, setSelectedTask] = useState<any>(null); // 当前选中的任务详情
const [wallet, setWallet] = useState<string | null>(null); // 用户钱包地址
const [error, setError] = useState<string>(''); // 错误提示信息
useState
管理页面核心状态:任务选择、解决方案输入、任务列表和加载状态。wallet
用于保存用户钱包地址,error
用于捕获和显示错误信息。useEffect(() => {
(async () => {
try {
const { Mina, PublicKey } = await import('o1js');
const { SecretHeist } = await import('../lib/contracts');
const zkAppAddress = 'B62qr...'; // 智能合约地址
const zkApp = new SecretHeist(PublicKey.fromBase58(zkAppAddress));
// 可根据需要设置 Mina 网络实例
const network = Mina.Network({
mina: 'https://api.minascan.io/node/devnet/v1/graphql/',
archive: 'https://api.minascan.io/archive/devnet/v1/graphql/',
});
Mina.setActiveInstance(network);
// 后续交互中可直接使用 zkApp
} catch (error) {
setError('合约初始化失败,请检查网络连接或合约地址');
}
})();
}, []);
useEffect
初始化 Mina 网络连接和智能合约实例。error
状态。const handleTaskSelect = (task: any) => {
setSelectedTask(task); // 更新选中任务详情
setTaskId(task.id); // 设置当前任务ID
setError(''); // 清除错误提示
};
const handleSubmitSolution = async () => {
if (!wallet || !taskId || !solution) {
setError('请确保已连接钱包并填写完整信息');
return;
}
setLoading(true); // 开启加载状态
setError(''); // 清空错误提示
try {
// 初始化 Mina 网络
const network = Mina.Network({
mina: 'https://api.minascan.io/node/devnet/v1/graphql/',
archive: 'https://api.minascan.io/archive/devnet/v1/graphql/',
});
Mina.setActiveInstance(network);
// 构造并发送解决方案交易
const tx = await Mina.transaction(PublicKey.fromBase58(wallet), async () => {
await zkApp.submitSolution(Field(taskId), Field(solution)); // 提交任务ID和解决方案
});
await tx.prove(); // 生成零知识证明
const { hash } = await tx.sign([PrivateKey.fromBase58(wallet)]).send(); // 签名并发送交易
console.log(`解决方案提交成功,交易哈希:${hash}`);
} catch (err: any) {
setError(err.message || '解决方案提交失败');
} finally {
setLoading(false); // 关闭加载状态
}
};
zkApp.submitSolution
提交解决方案,生成证明并签名交易。 finally
确保加载状态正确关闭。return (
<div className={styles.container}>
{/* 头部区域 */}
<header className={styles.header}>
<h1>秘密盗宝任务系统</h1>
<button onClick={handleConnectWallet}>
{wallet ? `已连接: ${wallet}` : '连接钱包'}
</button>
</header>
{/* 游戏主区域 */}
<main className={styles.main}>
<div className={styles.taskPanel}>
{/* 任务列表 */}
<h2>可选任务</h2>
{taskList.length > 0 ? (
taskList.map((task) => (
<div
key={task.id}
className={`${styles.taskCard} ${
selectedTask?.id === task.id ? styles.selected : ''
}`}
onClick={() => handleTaskSelect(task)}
>
<h3>{task.title}</h3>
<p>奖励: {task.reward} TOKEN</p>
<p>截止时间: {new Date(task.deadline).toLocaleString()}</p>
</div>
))
) : (
<p>暂无任务</p>
)}
</div>
<div className={styles.actionPanel}>
{/* 解决方案提交 */}
<h2>提交解决方案</h2>
{selectedTask ? (
<div className={styles.solutionForm}>
<label htmlFor="solution">解决方案:</label>
<input
id="solution"
value={solution}
onChange={(e) => setSolution(e.target.value)}
/>
<button
onClick={handleSubmitSolution}
disabled={loading || !wallet || !solution}
>
{loading ? '提交中...' : '提交解决方案'}
</button>
</div>
) : (
<p>请选择一个任务以提交解决方案</p>
)}
</div>
</main>
</div>
);
在点击create task时,会报错,提示Couldn't send zkApp command: (invalid (Invalid_proof "In progress"))。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!