从 0 到 1:实现一个基于零知识证明的寻宝游戏

  • hello2jie
  • 更新于 2024-12-06 17:00
  • 阅读 259

本教程分享基于o1js2.*开发一个去中心化应用(DApp),构建一个基于零知识证明(ZKP)的寻宝游戏。游戏中,玩家扮演精英盗贼,需完成一系列盗宝任务。通过零知识证明,玩家可以向系统展示任务完成的真实性,同时保护任务细节(如密码、路线或地点等)。整个游戏采用去中心化架构,智能合约确保公平性,

本教程分享基于 o1js 2.* 开发一个去中心化应用(DApp),构建一个基于零知识证明(ZKP)的寻宝游戏。游戏中,玩家扮演精英盗贼,需完成一系列盗宝任务。通过零知识证明,玩家可以向系统展示任务完成的真实性,同时保护任务细节(如密码、路线或地点等)。整个游戏采用去中心化架构,智能合约确保公平性,玩家之间可以竞争宝藏。


页面效果

task.png

create_task.png


核心功能

任务逻辑

  1. 任务创建者

    • 定义任务目标(如密码哈希、地点哈希等)。
    • 提供奖励金额和任务截止时间。
  2. 玩家

    • 完成任务后提交解决方案。
    • 系统通过零知识证明验证任务完成的真实性,无需透露解决方案细节。

零知识证明的价值

  • 隐私保护:任务完成者无需公开细节即可证明完成情况。
  • 公平性:去中心化验证过程避免作弊。
  • 灵活扩展:任务类型可包含多种谜题形式,如密码破解或路线规划。

开发流程

1. 初始化项目

使用 zkApp CLI 初始化项目并安装必要依赖。

npm install -g zkapp-cli
zk project SecretHeist

2. 实现合约

任务结构体定义

定义任务结构体 HeistTask,用于存储任务信息。

class HeistTask {
  id: number;          // 任务ID
  target: Field;       // 目标哈希(如密码或位置的哈希值)
  reward: UInt64;      // 奖励金额
  deadline: UInt32;    // 截止时间
}
验证任务解决方案的方法

实现 verifyTaskSolution 方法,验证玩家提交的解决方案哈希是否匹配任务目标哈希。

verifyTaskSolution(taskTarget: Field, solutionHash: Field): Bool {
  return taskTarget.equals(solutionHash);
}
合约核心方法

SecretHeist 合约中实现两个主要方法:

1. 创建任务:createTask

玩家可以创建新任务并设置目标哈希、奖励金额和截止时间。

  • 输入参数

    • target: 目标哈希(密码哈希或位置哈希)
    • reward: 奖励金额
    • deadline: 截止时间
  • 主要逻辑

    1. 自动生成任务ID。
    2. 存储任务信息。
    3. 发出任务创建事件。
// 创建新任务
  @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);
  }

2. 提交任务解决方案:submitSolution

玩家完成任务后提交解决方案。

  • 输入参数

    • taskId: 任务ID
    • solution: 解决方案
  • 主要逻辑

    1. 验证任务是否存在且未过截止时间。
    2. 使用 verifyTaskSolution 方法验证解决方案是否正确。
    3. 更新任务状态并发出任务完成事件。
// 提交任务解决方案
  @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);

    // 这里可以添加奖励发放逻辑
  }

三. 编写测试用例与部署合约

1. 测试环境设置

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);

    // ... 在这里添加部署交易逻辑
});

2. 测试用例分类

  • 合约初始化: 测试合约的初始状态是否正确
  • 任务创建: 验证创建任务的功能是否按预期工作
  • 解决方案提交:
    • 正确解决方案:验证合约能成功接收并处理正确的解决方案
    • 错误解决方案:验证合约对错误解决方案进行拒绝

3. 关键测试用例详解

(1) 测试任务创建功能
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);  // 验证截止时间
});
(2) 测试提交正确的解决方案
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
});
(3) 测试提交错误的解决方案
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();  // 预期抛出错误
});

4. 核心功能验证

上述测试用例全面覆盖了合约的关键功能,确保:

  1. 合约初始化状态正确,能够正常运行;
  2. 能创建新任务并存储相关信息;
  3. 对正确的解决方案进行处理;
  4. 拒绝错误的解决方案,保证合约逻辑安全性。

四、部署到 Devnet

1. 配置 Devnet 环境

在部署合约到 Devnet 之前,需要确保正确配置网络环境。zk config 提供了配置工具,可以方便地设置相关参数。

  1. 运行 zk config:执行以下命令以进入 Devnet 配置界面:

    zk config
  2. 设置 Devnet 参数:在配置过程中,主要填写以下关键信息:

    • Network: 选择 devnet 网络。
    • Node URL: 输入 Devnet 节点的 URL(通常由 Mina 网络提供)。
    • Private Key: 提供部署账户的私钥,用于签署交易。
    • Fee: 设置交易的手续费,根据网络要求填写适当值。
  3. 验证配置:完成设置后,可运行以下命令查看当前的配置是否正确:

    zk config show

    输出应包含 devnet 网络的相关参数,如节点地址和账户信息。

2. 部署合约到 Devnet

完成配置后,使用以下命令将合约部署到 Devnet:

zk deploy devnet

部署成功后,终端会返回合约地址(通常是公共密钥 zkAppAddress)。记录此地址,后续测试将用到。

五、实现前端页面

实现两个页面:

  1. 创建任务页面:用于用户填写表单并提交创建任务请求。
  2. 进行任务页面:用于显示任务详情并允许用户提交解决方案。

以下详细说明 创建任务页面 的主要功能与实现:


1. 状态管理

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 包含了创建任务所需的所有字段,便于统一管理表单数据。

2. 钱包连接功能

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 地址,供后续交易使用。
  • 在连接失败时,捕获并显示错误信息。

3. 表单提交处理

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)。
  • 智能合约交互:创建并发送任务到区块链,通过 Mina 网络的 zkApp.createTask 方法完成操作。
  • 错误处理:捕获所有可能的错误并更新 error 状态,便于在 UI 上展示。

4. 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>
);
  • 钱包连接区域:提供按钮引导用户连接钱包,成功连接后显示钱包地址。
  • 任务表单:通过表单输入任务信息,包括标题、难度、奖励、目标等字段。
  • 错误提示:显示捕获的错误信息,帮助用户了解问题所在。
  • 样式管理:使用 CSS Modules 管理样式文件(styles.*),保持组件样式隔离。

进行任务页面:主要功能与实现

1. 状态管理

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 用于捕获和显示错误信息。

2. 合约初始化

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 状态。

3. 任务选择处理

const handleTaskSelect = (task: any) => {
  setSelectedTask(task); // 更新选中任务详情
  setTaskId(task.id); // 设置当前任务ID
  setError(''); // 清除错误提示
};
  • 处理任务选择逻辑:更新选中任务的状态,并清空之前可能的错误信息。

4. 解决方案提交

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); // 关闭加载状态
  }
};
  • 验证用户输入:确保用户已连接钱包并填写任务ID和解决方案。
  • 交互逻辑:通过智能合约方法 zkApp.submitSolution 提交解决方案,生成证明并签名交易。
  • 错误处理:捕获并显示异常信息,同时通过 finally 确保加载状态正确关闭。

5. UI 结构

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"))。

完整代码地址:git@github.com:hello2jie/SecretHeist.git

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
hello2jie
hello2jie
江湖只有他的大名,没有他的介绍。