跳到主要内容

成为 OpenFiat 仲裁者

仲裁者用自己质押的 OPEN 承担风险来裁决争议,而不是靠信誉。本指南将带你完成质押、寻找案件,以及投出真正决定结果的「先承诺后揭示」投票。

你需要准备什么

  • 一个持有至少 10,000 OPEN 的 Solana 钱包——这是已部署质押配置中的 min_stake_arbitrator,可由治理调整——可通过 openfiat-app 的质押页面完成,若你在开发自己的客户端,也可参照下方说明
  • 一个支持消息签名的钱包(主流 Solana 钱包均已支持)——openfiat-app 的「Arbitrate」页面可在浏览器中完成整个案件流程;若你更倾向于脚本化处理,也可以使用 SDK
  • 至少一个 OpenFiat 节点 JSON-RPC 端点的网络访问权限,可以是你自己的节点,也可以是公共节点

处理一个案件

按顺序处理单个案件的这些步骤。每一步都附有所需的命令或代码。

  1. 01

    质押 OPEN 以解锁仲裁资格

    仲裁者必须先质押,才能看到任何一个案件的证据——这既让贿赂变得毫无意义(你不知道该盯上哪个案件),也让网络在你投票偏离最终揭示的共识时有东西可以罚没。已部署的 devnet 配置将仲裁者的最低质押量设为 10,000 OPEN,是其他所有角色所需的 1,000 OPEN 的十倍。

    stake.ts
    import { PublicKey, Transaction } from "@solana/web3.js";
    import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
    import { onchain } from "@openfiat/sdk";
    
    // Role.Arbitrator = 1 — same instruction pair openfiat-app's Stake page
    // submits when you connect a wallet and bond there directly.
    const from = getAssociatedTokenAddressSync(mint, owner, false, TOKEN_2022_PROGRAM_ID);
    const amount = 10_000n * 1_000_000_000n; // OPEN has 9 decimals (OFS-4100 §1)
    
    const instructions = [
      onchain.staking.initializeStakeAccountIx(owner, onchain.Role.Arbitrator),
      onchain.staking.stakeIx(owner, mint, onchain.Role.Arbitrator, from, amount),
    ];
    const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
    const tx = new Transaction({ feePayer: owner, blockhash, lastValidBlockHeight }).add(...instructions);
    const { signature } = await wallet.signAndSendTransaction(tx);
  2. 02

    寻找一个待处理的案件

    仲裁者是自行选择要处理的争议,而非被系统指派。可以向任意节点查询尚未凑齐所需仲裁者人数的案件。

    shell
    # Any node will do — arbitrators pick a case, nobody assigns one.
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getDisputes","params":{}}'
    # Filter the result for status: "Open" and arbitrators.length < required_arbitrators
  3. 03

    先加入,才能看到证据

    加入案件正是解锁证据的方式:买卖双方各自提交的材料、付款确认,以及这笔交易自身的沟通记录。一旦案件凑齐所需的仲裁者人数,就会锁定并进入承诺阶段。

    join.rs
    use openfiat_disputes::events::ArbitratorJoin;
    use openfiat_sdk::wallet::Keypair;
    use openfiat_sdk::{Client, ClientConfig};
    
    // Joining is what unlocks the case's evidence for you — buyer/seller
    // submissions and the trade's own message log become visible once you're
    // in `dispute.arbitrators`.
    let client = Client::new(ClientConfig { endpoint: node_url, ..Default::default() });
    client
        .send_arbitrator_join(
            ArbitratorJoin {
                dispute_id: dispute.id.clone(),
                arbitrator: peer_id(&keypair),
                arbitrator_public_key: keypair.public_key(),
                timestamp: Timestamp::now(),
            },
            &keypair,
        )
        .await?;
  4. 04

    提交你的承诺——需要两次

    有两个并行的「先承诺后揭示」投票:一个是链下的,记入案件自身的审计记录与信誉;另一个是链上的,针对 openfiat-escrow 的 DisputeCase 账户,真正决定按质押加权的结果。两边要使用同一个裁决结论与同一个随机数(salt)——但必须各用各自的枚举,而不是同一个数字:链下的 Invalid 为 2,链上的 InvalidDispute 为 3,因为链上的 2 是 MutualSettlement。一旦哈希了错误的字节,你所承诺的内容将永远无法揭示,而无法揭示恰恰正是仲裁者被罚没的情形。

    commit.rs + commit.ts
    // Off-chain (marketplace record + reputation) — Rust SDK
    let secret: [u8; 32] = rand::random();
    let commitment = openfiat_disputes::commitment::compute(vote, &secret); // sha256(vote || secret)
    client.send_vote_commit(VoteCommit { dispute_id, arbitrator: peer_id(&keypair), commitment, timestamp: Timestamp::now() }, &keypair).await?;
    // keep `secret` — you need it again at reveal, and it must never be guessable before then
    
    // On-chain (the stake-weighted vote that actually pays out) — TypeScript SDK
    import { onchain } from "@openfiat/sdk";
    import { createHash } from "node:crypto";
    
    // onchain.DisputeOutcome, NOT the off-chain Vote byte: the two enums
    // diverge from Invalid onwards (see this file's header). Hash the wrong
    // one and the commitment can never be opened.
    const commitment = createHash("sha256").update(Buffer.from([onchain.DisputeOutcome.BuyerWins])).update(salt).digest();
    const ix = onchain.escrow.commitDisputeVoteIx(arbitrator, reservationId, commitment);
  5. 05

    承诺窗口关闭后进行揭示

    承诺窗口关闭后,在两边分别揭示你的结果与随机数。在链上,这一步也是你的投票获得真实权重的地方:揭示指令会直接读取你的仲裁者角色质押账户,因此没有仲裁者质押的钱包根本无法提供有效账户。

    reveal.rs + reveal.ts
    // Off-chain — Rust SDK
    client.send_vote_reveal(VoteReveal { dispute_id, arbitrator: peer_id(&keypair), vote, secret, timestamp: Timestamp::now() }, &keypair).await?;
    
    // On-chain — reads your Arbitrator StakeAccount directly (no CPI) to weight this vote
    import { onchain } from "@openfiat/sdk";
    
    const [arbitratorStake] = onchain.staking.stakeAccountPda(arbitrator, onchain.Role.Arbitrator);
    const ix = onchain.escrow.revealDisputeVoteIx(
      arbitrator,
      reservationId,
      onchain.DisputeOutcome.BuyerWins,
      salt,
      arbitratorStake,
    );
  6. 06

    结果会自动执行

    一旦所有仲裁者都完成揭示,或揭示窗口关闭,任何人——你自己、买方、卖方,或是无关的自动化脚本——都可以调用 execute_dispute_outcome。它只是对链上已记录的投票进行计票。按照设计,随后会向多数意见的仲裁者发放该案件手续费的一部分作为奖励,并扣减揭示结果偏离多数一方的部分质押——但奖励与处罚均尚未实现,因此目前计票只会转移交易双方的资金,不产生其他效果。

    resolve.ts
    import { onchain } from "@openfiat/sdk";
    
    // Permissionless — it only tallies votes the reveal step above already
    // recorded on-chain, so anyone (the buyer, the seller, either arbitrator,
    // or an unrelated crank bot) can call it once the reveal window closes.
    const ix = onchain.escrow.executeDisputeOutcomeIx(mint, seller, reservationId, destinations);

仲裁者从不托管交易资金。裁决只会调动履约托管程序在争议发生前就已经锁定的资金。