Ksana
EN

概念

操作、路径与手续费

你在 Ksana 上做的每件事,都是一笔签了名的操作。把它发给任意节点,用 KSA 付手续费(路径 B);或者把它放进你自己的比特币交易,只付比特币手续费(路径 C)。

本页目录

转账、合约调用和部署都是操作:一个账户签出的一条消息。路径 B 经任意 Ksana 节点发送,付 KSA 手续费;路径 C 经比特币发送,只付比特币手续费,而且没有出块者能拒绝它。

什么是操作

一笔操作(代码里叫 VmOp)依次包含下面这些字段(这个顺序就是编码格式):

字段 内容
version 2,当前格式
chain_id 所属网络:signet 上是 1002,主网上是 1000,devnet 上是 1001。操作在其他任何网络上都无效。
account 签名账户(账户)
nonce 这笔操作使用的账户 nonce
fee 路径 B 为 Token { amount },即以最小单位计的 KSA 数额;路径 C 为 BtcNative
max_gas 这笔操作最多可用的 gas
action 要做什么
sig 对以上全部内容的 BIP340 签名或 BIP322 simple 签名

动作(action)是以下三种之一:

  • AssetTransfer { asset, to, amount }:把 KSA 或其他内置资产转给一个账户或合约。
  • Call { contract, method, args, funds }:用 Borsh 编码的参数调用合约的一个方法。
  • Deploy { code, init_args, funds }:部署 WebAssembly 代码,并运行它的 init。

funds 给调用或部署附带最多 16 种内置资产,按资产 ID 升序排列,每项都大于零;这些资产在合约运行之前转给合约。操作用 Borsh 编码,最长 65,536 字节,它的操作 ID 是整个编码(含签名)的 SHA-256。

两条路径

路径 B代付
  1. 你签一条消息,用钱包或私钥都行。
  2. 节点把操作收进条目池;手续费用 KSA 支付。
  3. 出块者几秒内把它收进下一个区块。
  4. 这个区块的结算把它带进比特币。
  • 几秒
  • 用 KSA 付费
  • 不需要比特币
路径 C原生
  1. 你把操作放进一笔自己的比特币交易。
  2. 它在比特币上确认;手续费用 BTC 支付。
  3. 之后每个出块者都必须收录它,否则区块无效。
  4. 从此它和其他操作一样,快轨确认,再到最终。
  • 比特币确认
  • 用 BTC 付费
  • 谁也拦不住

路径 B 与路径 C 并排对照:路径 B 是你签一条消息,出块者收取 KSA 手续费,几秒内收录它;路径 C 是由你自己的比特币交易携带操作,交易一经确认,每个出块者都必须收录它。

路径 B:代付 路径 C:原生
发往 任意 Ksana 节点,通过 ksana_submitOp 比特币,放在你自己的交易里
手续费 KSA,付给出块者和携带者 BTC,只付给比特币矿工
生效 下一个区块,几秒之内 比特币确认之后:signet 上 2 个,主网上 5 个
出块者能否拒绝 能 不能

日常使用走路径 B。手上没有 KSA、没有出块者肯收你的操作,或者要在同一笔交易里既付比特币又执行操作(充值就是这样)时,走路径 C。

路径 B:代付

用 fee: Token { amount } 签好操作,再用 ksana_submitOp 提交给任意节点。节点会检查它(格式、链 ID、签名、nonce 不低于账户的 nonce、余额够付你所有排队操作的手续费),把它放进自己的条目池,并转发给对等节点。出块的节点要等操作进了自己的下一个出块模板才会应答,所以出块者通常几秒内就会收录它,之后再付比特币手续费,把它写进比特币。

被拒绝时会返回错误:违反共识规则的操作返回 -32010(发到这里的路径 C 操作也在此列),被本节点策略拒绝的操作返回 -32011,比如手续费低于门槛、条目池已满,或者替换时没有提高手续费。

手续费在操作执行前扣除,即使操作失败也不退。它随区块一起锁定,直到区块的数据写入比特币,之后一半归该区块的携带者,其余归它的出块者;如果出块者自己携带了自己的区块,或者没有人够资格当携带者,就全部归出块者(KSA 与奖励)。

费率与门槛

出块者按费率给操作排序,而不只看手续费本身。一笔操作的成本,是以下两者中较大的一个:它编码后的字节数;按区块的数据与 gas 之比把 max_gas 换算成的字节数(max_gas * 81920 / 100000000)。它的费率是 fee * 1024 / cost,单位是每 KiB 多少个最小单位。所以预留了区块大量 gas 的操作,要为这部分份额付费,没法只多付一点手续费就把更便宜的操作挤出去。

一笔用 BIP340 签名的 KSA 转账是 216 字节。max_gas 设为 10,000 时(转账恰好用这么多),成本是 216 字节;用 ksana-cli 默认的 max_gas 1,000,000 时,成本是 819,同样的手续费换来的费率就更低。请把 max_gas 设得接近操作实际所需。

门槛由每个节点自己设定。它们是策略,不是共识:

选项 默认值 作用
--min-token-fee 1 个最小单位 打包门槛:本节点放进自己区块的最低手续费
--min-fee-per-kib 0 本节点打包的最低费率
--min-relay-token-fee 与打包门槛相同 中继门槛:本节点接纳并转发的最低手续费(两道门槛中较低的那道起作用)

手续费高于中继门槛、但低于打包门槛的操作,照样会进入条目池并继续转发,留给门槛更低的出块者。全网没有统一的价格:每个出块者都按自己的比特币成本给 KSA 定价。用 ksana-cli estimate-fee(RPC 方法 ksana_estimateFee)可以问节点的建议值:

Shell
export KSANA_RPC=https://signet.ksana.network/rpc
ksana-cli --chain signet estimate-fee
输出
{
  "at": {
    "hash": "edfddf5bd775b57b43f70830e2acb8e7c228bf96a37064540bc4210ed2998bbc",
    "height": "285"
  },
  "feerate_sat_vb": "2",
  "path_b": {
    "amount": "1",
    "per_kib": "1"
  }
}

path_b.per_kib 是这样算的:在最近 20 个主链区块里,凡是含有路径 B 操作的区块,各取其中最低的路径 B 费率,再取这些费率的中位数。path_b.amount 是把这个费率用到一笔 max_gas 为 10,000 的普通转账上得出的手续费,向上取整,且不低于打包门槛。feerate_sat_vb 是节点目前为自己的结算支付的比特币费率,可供路径 C 交易参考。

条目池

条目池同样是本地策略:默认情况下,节点的条目池最多容纳 10,000 笔操作或 32 MiB,每个账户最多 64 笔,从账户的 nonce 开始编号,其中最多 8 笔可以排在编号的缺口之后。账户和 nonce 都相同的新操作,只有手续费严格更高时才会替换正在等待的那笔。条目池满了以后,它不拒绝新来的操作,而是逐出排位最低的操作(先逐出排在 nonce 缺口之后的,再按费率)。被替换或被逐出的操作就此消失,用它的操作 ID 查询会得到 -32004。

路径 C:原生

路径 C 把操作放进你自己的比特币交易。操作带着 fee: BtcNative,放在一个以 KSN1 标记开头的 OP_RETURN 输出里;你用任意的币为这笔交易出资,付比特币手续费,再把它广播到比特币网络。Bitcoin Core 30 及以后的版本默认就会中继这么大的 OP_RETURN 输出,更早的版本需要设置 -datacarriersize=100000。

签名覆盖这笔交易第一个输入所花费的输出点。一个输出只能被花费一次,所以别人把你的操作复制进他自己的交易时,会通不过认证,也不会用掉你的 nonce。ksana-cli 和 TypeScript SDK 都能构造这类交易;用浏览器钱包时,钱包先签操作,再签交易。

强制收录

锚定在比特币高度 A 的区块,必须以所有符合下列条件的路径 C 交易开头,并按它们在比特币上的顺序排列:在高度 A − NATIVE_CONFIRMATIONS + 1 或更低处确认,且该区块的父块尚未收录。出块者锚定在自己比特币链顶之下 ANCHOR_DEPTH 个区块处,所以你的交易有了 NATIVE_CONFIRMATIONS + ANCHOR_DEPTH 个确认就会被收录:signet 上 2 个,主网上 5 个,devnet 上 1 个。漏掉其中任何一笔的区块都无效,所以没有出块者能把你的操作挡在外面。

强制收录条目不受区块 gas 上限的约束;每条按它的 max_gas 计,最多 1,000,000。要求超过 1,000,000 gas(NATIVE_MAX_GAS)的操作会失败,并用掉它的 nonce。

路径 C 的花费

路径 C 操作不给出块者付任何费用:你只付比特币手续费,操作失败时这笔费用也不退。用 SDK 构造的一笔 KSA 转账(一个输入、带找零)是 330 vB,按 2 sat/vB 算是 660 聪(钱包用 BIP322 签名操作时是 672 聪)。

在某个区块收录你的操作之前,节点并不知道它:在那之前 ksana_getOpStatus 都会返回 -32004,所以这段时间请跟踪那笔比特币交易。ksana-cli 会用你自己 bitcoind 里的一个钱包为交易出资并广播:

Shell
ksana-cli --chain signet transfer --key alice.key --to @bob.key --amount 100000000 \
  --native --btc-rpc-url http://127.0.0.1:38332 --btc-wallet <wallet> --wait --timeout 3600

--btc-rpc-user 和 --btc-rpc-pass(或 KSANA_BTC_RPC_PASS)提供你 bitcoind 的凭据。请保留较长的 --timeout:这笔操作要等两个 signet 区块,远远超过默认的 120 秒,而 --wait 超时后只会打印一条错误。经路径 C 调用的合约可以用宿主函数 native_tx 读取携带这次调用的交易,合约正是靠它来核验在同一笔交易里完成的付款(合约中的比特币)。

充值:用 BTC 换 KSA

充值通过一个兑换合约(比如示例合约 ksa-exchange),用一笔比特币交易拿比特币买 KSA。兑换合约是应用,不是协议规则,谁都可以部署。

  1. 商定:你和卖方在链下谈好数额、价格(每聪多少个 KSA 最小单位)以及要入账的账户。
  2. 锁定(卖方,走路径 B):卖方把 KSA 锁进合约,同时写明价格、一个只供这个锁定使用的收款脚本、作为受益账户的你的账户,以及到期的比特币高度。之后这些条款就不能再改。
  3. 付款并领取(你,走路径 C):一笔比特币交易既向收款脚本付款,又携带领取操作。领取操作由一次性密钥在 nonce 0 上签名,并绑定这笔交易的第一个输入,所以它永远不会和你账户的 nonce 冲突,复制件也抢不走它。
  4. 入账:交易被强制收录后,合约用 native_tx 读取它,按所付聪数乘以价格给受益账户入账,以仍处于锁定的数额为限。只买下一部分的付款,会让剩下的部分继续为你锁定。

到期 6 个比特币区块之后,卖方就能取回仍处于锁定的部分,所以你的付款最晚要在比特币高度“到期高度 + 6 − NATIVE_CONFIRMATIONS − 1”确认(即 SDK 的 payBy)。SDK 和 ksana-cli 会先读取锁定,遇到以下情况就拒绝付款:受益账户不是你,锁定不是开放状态,离到期已不足 3 个比特币区块,或者付款多于或少于锁定的 KSA 所值(可以用选项允许部分付款或多付)。一笔充值交易约 381 vB。signet 钱包和 ksana-cli 都能充值。

回执与 gas

每笔被收录的操作都有一张回执,包含 success、gas_used、output(调用返回的内容)、events 和 error。

gas 衡量的是执行量。普通的 WebAssembly 指令每条 1 gas,宿主函数有固定的价格,内置动作则从固定的数额起算:

动作 gas
AssetTransfer 10,000
Call 20,000,加上每个附带资产 10,000、合约代码每字节 1,以及执行本身的消耗
Deploy 100,000,加上代码每字节 20、每个附带资产 10,000,以及 init 的执行消耗

gas 上限是精确的:用量会超出自身 max_gas 的操作就会失败。在 signet 上部署示例代币用了 383,868 gas(代币与资金池教程)。一个区块只有在各条目的 max_gas 总和不超过 100,000,000 时才能收录路径 B 操作(路径 C 条目最多按 1,000,000 计),这也是让 max_gas 贴近实际所需的又一个理由。所有价格都列在宿主函数一页。

失败的操作与无效区块

可能出问题的地方有三种,只有第一种关乎你的操作本身:

  • 失败的操作已被收录,但执行失败:gas 耗尽、合约 trap 或 revert、附带资产无效、余额不足。它的回执里 success: false,gas_used 等于 max_gas;手续费照扣,nonce 照用,其余所有改动和事件都会回滚。没通过检查(版本、链 ID、长度、手续费模式、签名或 nonce)的路径 C 操作也会失败,但 nonce 保持不变。区块仍然有效。
  • 被跳过的条目位于一个被合并的区块里,在合并给它排定的位置上无法执行,比如另一个区块已经执行过同一笔操作。它不付任何费用,也不改变任何状态;仍然有效的操作会回到条目池。
  • 无效区块违反了某条共识规则:签名错误、它自己的条目里有不对的 nonce、缺了路径 C 条目,或者状态根错误。节点会拒收它,或者不让它进入主链,所以其中的操作只有在某个有效区块执行它时才算数。

本地故障,比如存储出错或 bitcoind 出错,不属于以上任何一种:节点会重试,绝不会因此判定一个区块无效。

完整规则见规范 §5(两条路径与充值)、§7.4(强制收录)和 §8(手续费与 gas)。