跳到主要内容

为某个角色质押 OPEN

除买方之外,几乎每一种角色所做的事,一旦草率行事都可能损害他人,因此这些角色必须先让自己的 OPEN 承担风险,才能获得相应权限。质押就是这笔保证金。它仍归你所有,在你行为不当时可能被削减;但目前它不会带来任何收益。奖励的计算逻辑现已实现,节点也会公布其所依据的存活性观测数据,但 devnet 上尚无人执行提交环节,且奖励金库为空,因此还没有任何质押真正获得过一枚代币。

里程碑 1 / 6 · 运行基础设施

开始之前

  • 一个 devnet 上的 Solana 钱包,并持有少量 SOL 用于支付交易手续费
  • 该钱包中持有 OPEN,且为 Token-2022 账户——OPEN 从何而来请见下文对应步骤
  • 明确你要担任的角色:每个(钱包,角色)组合对应一个独立的质押账户,因此请先确定再创建
  • 除质押本身之外的操作——解除质押、提取、领取奖励——都需要用 SDK 编写脚本,因为目前只有质押提供了浏览器操作界面

操作步骤

  1. 01

    质押是什么,又不是什么

    质押是一笔你仍然拥有所有权的保证金,而不是付给谁的费用。它也不是信誉,更无法转化为信誉:表现糟糕的运营者无法靠多质押来弥补,因为信誉来自已完成的工作,并且是单独记录的。质押真正带来的是「利益绑定」——一个在你违规时协议可以削减的数字,正是它让陌生人愿意信任你。

  2. 02

    选择你要质押的角色

    共有七种需要质押的角色,质押账户以(所有者,角色)为键——因此同一个钱包可以同时持有多个角色下彼此完全独立的质押,各自拥有独立的余额与解押计时。角色在创建账户时即确定;若要在角色之间转移质押,需要先从一个角色解押,再质押到另一个角色。

    The seven roles, and their on-chain values
    Merchant                  = 0
    Arbitrator                = 1
    NodeOperator              = 2
    NotificationProvider      = 3
    OracleProvider            = 4
    RiskIntelligenceProvider  = 5
    SnapshotProvider          = 6
    
    // The stake account's address is derived from the pair, so the role is
    // part of the account's identity rather than a field you can change:
    //     seeds = ["stake", owner_pubkey, role_as_u8]
  3. 03

    先让钱包持有 OPEN

    devnet 上 OPEN 铸币的铸造权限已被永久移除——十亿枚的全部供应量在创世时一次性铸造完成,此后包括项目方在内的任何人都无法再增发。因此这里没有水龙头,你也无法自行铸造测试用的 OPEN:在 devnet 上,只能由已从创世分配中持有 OPEN 的人转给你。此外,你的钱包必须先拥有该铸币对应的 Token-2022 账户,才能接收 OPEN。

    shell
    # The devnet OPEN mint. Note "Mint authority: (not set)" — the supply
    # is fixed at 1,000,000,000 OPEN (9 decimals) and cannot grow.
    spl-token display 29w8TroBTYoaqrXBDcpv5L54VZRA8Kf7kU5U1cakvFdj --url devnet
    
    # Your own balance for that mint, once somebody has sent you some.
    spl-token balance 29w8TroBTYoaqrXBDcpv5L54VZRA8Kf7kU5U1cakvFdj --url devnet
  4. 04

    创建质押账户并注入资金

    需要两条指令:一条为你的(钱包,角色)组合创建账户,另一条把 OPEN 从你的代币账户转入程序金库。账户只需创建一次,因此实际的客户端会先检查它是否已存在,若已存在则只发送质押指令。最简单的方式是使用 OpenFiat 应用中的质押页面,连接钱包后它执行的正是上述操作——不过该表单目前只提供商户、节点运营者、仲裁者与通知提供方四种角色,其余三种角色仍需通过 SDK 操作。

    TypeScript
    import { PublicKey, Transaction } from "@solana/web3.js";
    import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
    import { onchain } from "@openfiat/sdk";
    
    const mint = new PublicKey("29w8TroBTYoaqrXBDcpv5L54VZRA8Kf7kU5U1cakvFdj");
    const role = onchain.Role.NodeOperator;
    
    // Where your OPEN actually sits. Token-2022, not the older token program.
    const from = getAssociatedTokenAddressSync(mint, owner, false, TOKEN_2022_PROGRAM_ID);
    
    // OPEN has 9 decimals, so amounts are in units of 1e-9 OPEN.
    const amount = 1_000n * 1_000_000_000n;
    
    // Skip initializeStakeAccountIx if this (wallet, role) already has one —
    // the instruction creates the account and will fail if it exists.
    const instructions = [
      onchain.staking.initializeStakeAccountIx(owner, role),
      onchain.staking.stakeIx(owner, mint, role, from, amount),
    ];
    
    const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();
    const tx = new Transaction({ feePayer: owner, blockhash, lastValidBlockHeight }).add(...instructions);
    const { signature } = await wallet.signAndSendTransaction(tx);
    await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "confirmed");
    Rust — note the argument order differs from TypeScript
    use openfiat_sdk::onchain::{staking, Role};
    
    let role = Role::NodeOperator;
    let amount = 1_000 * 1_000_000_000u64; // 1,000 OPEN, 9 decimals
    
    let instructions = vec![
        staking::initialize_stake_account_ix(&owner, role),
        staking::stake_ix(&owner, role, &mint, &from, amount),
    ];
  5. 05

    读取真实参数,而不要相信文档中的数字

    最低质押量、解押周期与罚没比例都保存在链上唯一的 StakingConfig 账户中,且治理机制可以修改它们——因此以你实际读取到的数值为准,而不是本文写下的数字。需要特别注意:程序虽然存储了两项最低质押量,但目前并不对其做任何强制校验,因此当前质押低于所述最低值仍会被链上接受。请把它们视为你应当达到的标准,而不是会拦住你的护栏。

    The values deployed on devnet at the time of writing
    min_stake              1,000 OPEN     # all roles
    min_stake_arbitrator  10,000 OPEN     # arbitrators, a higher bar
    unbonding_period_secs   604,800       # 7 days
    slash_bps                  1,000      # 10% of the active stake
    
    # Read them yourself rather than trusting the above — the account is a
    # singleton PDA, so its address is fixed:
    #     seeds = ["staking_config"]   program HYEXk8XQukBkZbiYB33JyVefQDxqyCpPudad3wBCyYmx
    TypeScript
    import { onchain } from "@openfiat/sdk";
    
    const [stakingConfig] = onchain.staking.stakingConfigPda();
    const account = await connection.getAccountInfo(stakingConfig);
    
    // The SDK builds instructions but does not decode accounts yet. The
    // layout after the 8-byte discriminator is:
    //   admin(32) mint(32) min_stake(8) min_stake_arbitrator(8)
    //   unbonding_period_secs(8) slash_bps(2) ...
    // openfiat-app's lib/onchain-decode.ts has a working decoder to copy.
  6. 06

    需要退出时解除质押

    发起解押请求会立即把相应金额从你的有效质押中扣除,早于你实际拿回代币——这是刻意设计的,以防有人一边申请解押、一边仍用这笔即将撤出的质押去投票或主张资格。代币本身则会一直锁定到解押周期结束。有一点需要提前规划:每个质押账户只跟踪一批解押,因此第二次请求会并入第一批,并把释放时间重新推迟为从该时刻起的完整周期。

    TypeScript
    import { onchain } from "@openfiat/sdk";
    
    // There is no browser flow for this yet — unbonding, withdrawing and
    // claiming rewards are SDK-only today.
    const ix = onchain.staking.requestUnstakeIx(owner, role, 500n * 1_000_000_000n);
    Rust
    use openfiat_sdk::onchain::staking;
    
    let ix = staking::request_unstake_ix(&owner, role, 500 * 1_000_000_000u64);
  7. 07

    周期结束后提取

    提取操作不需要指定金额:它会一次性返还全部处于解押状态的余额,且必须在释放时间到达之后才能执行——过早尝试会直接失败,而不会部分支付。你可以指定接收的代币账户,但它必须对应 OPEN 铸币。

    TypeScript
    const ix = onchain.staking.withdrawUnstakedIx(owner, mint, role, destinationTokenAccount);
    Rust
    let ix = staking::withdraw_unstaked_ix(&owner, role, &mint, &destination_token_account);
  8. 08

    奖励会累积,罚没则会扣减

    目前仍未向任何人发放过奖励,但差距已经缩小。计算部分是真实存在的:节点会记录在一个周期内听到过哪些对等节点,并据此按质押量乘以连通性与可用性来计算份额,真正连接 Solana RPC 的节点收益高于仅参与 gossip 的节点。缺的是最后两步——devnet 上无人执行提交,且没有任何指令为奖励金库注资,其余额为零。因此所有待领取余额均为零,领取操作会失败。请把质押视为获得行为资格的前提,而不是收益来源。将来奖励到账后会以待领取余额的形式存在,直到你主动领取,因此不会悄悄改变用于衡量角色资格的质押数额。罚没的方向则相反:罚没授权方可以按固定比例削减你的有效质押,被没收的代币会进入国库而非被销毁,累计罚没额会永久记录在你的账户上。只有有效质押会承担这一风险——已进入解押状态的部分不会。

    TypeScript
    // Claiming pays out the whole pending balance. Today that balance is always
    // zero for everyone, because nothing distributes rewards yet, so this fails.
    const ix = onchain.staking.claimRewardsIx(owner, mint, role, destinationTokenAccount);
    Rust
    let ix = staking::claim_rewards_ix(&owner, role, &mint, &destination_token_account);