Перейти к содержимому

Стать арбитром OpenFiat

Арбитры решают споры, рискуя собственным OPEN, а не репутацией. Здесь разбираются бонд, поиск дела и подача голоса «сначала коммит, потом раскрытие», который и приносит выплату.

Что вам нужно

  • Кошелёк Solana как минимум с 10 000 OPEN — min_stake_arbitrator из развёрнутой конфигурации стейкинга, который управление может изменить — внесите бонд через страницу Stake в openfiat-app или по инструкциям ниже, если вы строите собственный клиент
  • Кошелёк, способный подписывать сообщения, что делает любой релевантный кошелёк Solana — страница Arbitrate в openfiat-app проводит всё дело в браузере, а SDK доступны, если вы предпочитаете программировать
  • Сетевой доступ к JSON-RPC-эндпоинту хотя бы одного узла OpenFiat, вашего или публичного

Работа над делом

Выполняйте эти шаги по порядку, по одному делу за раз. Каждый шаг приводит нужную команду или код.

  1. 01

    Внесите бонд в OPEN, чтобы разблокировать арбитражный пул

    Арбитры должны застейкать, прежде чем смогут увидеть доказательства по одному делу — это делает подкуп одного из них бесполезным (вы не знаете, какое дело нацелить) и даёт сети то, что можно слэшить, если вы проголосуете против раскрытого консенсуса. Развёрнутая конфигурация devnet задаёт минимум арбитра в 10 000 OPEN, в десять раз больше 1000, которые вносит каждая другая роль.

    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

    Закоммитьте голос — дважды

    Два голосования «коммит-раскрытие» идут бок о бок: одно офчейн, записываемое в собственный аудит-след и репутацию дела, и одно ончейн против аккаунта DisputeCase из openfiat-escrow, которое и решает исход, взвешенный по стейку. Используйте одно и то же решение и один и тот же salt для обоих — но собственный enum каждой стороны, а не одно и то же число: Invalid офчейн это 2, InvalidDispute ончейн это 3, потому что MutualSettlement занимает 2 ончейн. Захешируйте не тот байт — и вы закоммитите то, что никогда не сможете раскрыть, а именно это и приводит к слэшингу арбитра.

    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

    Раскройте, как только откроется окно

    Раскройте свой исход и salt в обоих местах, как только окно коммита закроется. Ончейн здесь же ваш голос обретает свой реальный вес: инструкция reveal читает ваш аккаунт стейка роли Arbitrator напрямую, поэтому кошелёк без стейка арбитра просто не может предоставить валидный.

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

Арбитры никогда не держат средства сделок. Решение двигает лишь те деньги, которые сама программа хранения уже заблокировала до открытия спора.