概念
操作、路径与手续费
你在 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。
两条路径
- 你签一条消息,用钱包或私钥都行。
- 节点把操作收进条目池;手续费用 KSA 支付。
- 出块者几秒内把它收进下一个区块。
- 这个区块的结算把它带进比特币。
- 几秒
- 用 KSA 付费
- 不需要比特币
- 你把操作放进一笔自己的比特币交易。
- 它在比特币上确认;手续费用 BTC 支付。
- 之后每个出块者都必须收录它,否则区块无效。
- 从此它和其他操作一样,快轨确认,再到最终。
- 比特币确认
- 用 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)可以问节点的建议值:
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 里的一个钱包为交易出资并广播:
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。兑换合约是应用,不是协议规则,谁都可以部署。
- 商定:你和卖方在链下谈好数额、价格(每聪多少个 KSA 最小单位)以及要入账的账户。
- 锁定(卖方,走路径 B):卖方把 KSA 锁进合约,同时写明价格、一个只供这个锁定使用的收款脚本、作为受益账户的你的账户,以及到期的比特币高度。之后这些条款就不能再改。
- 付款并领取(你,走路径 C):一笔比特币交易既向收款脚本付款,又携带领取操作。领取操作由一次性密钥在 nonce 0 上签名,并绑定这笔交易的第一个输入,所以它永远不会和你账户的 nonce 冲突,复制件也抢不走它。
- 入账:交易被强制收录后,合约用
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 出错,不属于以上任何一种:节点会重试,绝不会因此判定一个区块无效。