拓展协议
Brc20

Brc20

Brc20 协议由 domo 基于 Ordinals 协议提出的, 用于在比特币网络上发行代币。其龙头代币 ORDI 市值一度接近20亿美金。本节将详细介绍 Brc20 协议的技术实现细节。

Brc20 数据存储基于 Taproot, 数据存储在脚本树的节点中。开始之前请确保已学习 P2TR 部分的内容。

由随机生成内部私钥, 计算出公钥的 XX 坐标作为内部公钥。结合脚本树的 tt 值, 计算出 Taproot 输出公钥 QQbech32m 编码后得到 Taproot 地址。

Brc20 交易首先会向该地址转账, 并输出一个 UTXO, 其锁定脚本为 OP_1 <Taproot 输出公钥>, 类型为 P2TR。但是, 该转账交易中并没有包含任何 Brc20 的数据。而要使 Brc20 数据显式包含在交易中, 必须要同时花费该 UTXO。并且花费路径只能选择脚本路径, 因为脚本路径花费时需要提供要花费脚本的完整数据, 如此便实现数据链上存储。

Brc20 交易都需要同时包含两笔交易:

  • 第一笔交易称作 commit 交易, 用于向生成的 Taproot 地址转账
  • 第二笔交易称作 reveal 交易, 用于花费 commit 交易生成的 UTXO

向生成的 Taproot 地址转账时, 需要转账足够的聪。避免花费时因为交易费不足导致的交易无法确认。

Taproot 地址计算

在向 Taproot 地址转账时先计算出要转账的地址。

计算 Commit 地址

随机生成的内部秘钥用于生成临时的 Taproot 地址。为了安全性, 每次 Brc20 交易通常需要生成一个新的 Taproot 地址。

脚本树中定义的锁定脚本只有一个, 内容部分如下所示:

OP_FALSE  // 等同于 OP_0
OP_IF
  OP_PUSHBYTES_3 ord
  OP_PUSHBYTES_1 01
  OP_PUSHBYTES_24 text/plain;charset=utf-8
  OP_0
  OP_PUSHBYTES_51 {"p":"brc-20","op":"mint","tick":"sats","amt":"10"}
OP_ENDIF
  • OP_FALSE: 等同于 OP_0, 作用是将数字 0 推入栈中

  • OP_IF: 条件块开始, 从栈顶弹出一个元素, 如果为真(非0)则执行条件块内的脚本, 否则跳过。由于栈顶元素是0, 所以条件块内的脚本永远不会被执行。脱离 Brc20 背景, 条件块内的脚本实际是一段冗余数据, 这也是 Brc20 经常被诟病的原因。

  • text/plain;charset=utf-8: 表示数据的 MIME 类型, 如果是图片数据, 则可以是 image/png

  • {"p":"brc-20","op":"mint","tick":"sats","amt":"10"}: 表示具体的数据内容, 对于 Brc20 代币数据, 各个字段分别表示:

    • p: 表示协议, brc-20 表示 Brc20 协议
    • op: 表示操作, mint 表示铸币, deploy 表示部署, transfer 表示转账
    • tick: 表示代币名称
    • amt: 表示数量
{
  "p": "brc-20",
  "op": "deploy", // 部署
  "tick": "sats", // 代币名称
  "max": 21000000, // 最大发行量
  "lim": 1000 // 每次铸币数量
}

在内容部分之上的部分实际是一个 P2PK 锁定脚本

OP_PUSHBYTES_32 InternalPubKeyX // 32 字节公钥 X 坐标
OP_CHECKSIG

不同的是公钥部分用的是 XX 坐标。一般来说, 椭圆曲线中相同的 XX 坐标会有两个 YY 坐标, 但在 BIP340 Schnorr 签名时总会选择 YY 坐标为偶数的那个, 所以只需要提供 XX 坐标即可。签名交易时, 则使用存储的公钥配对的私钥进行签名。

因此, 花费脚本中的 InternalPubKeyX 并不一定需要是生成 Taproot 地址时使用的内部公钥。但为了方便, 通常会使用相同的公钥。

Commit 交易

生成 Taproot 地址后, 需要向该地址转账。即花费自己的 UTXO 向生成的 Taproot 地址转账, 转账交易输出一个 P2TR 类型的 UTXOTaproot 地址。

一个 P2TR 类型的 UTXO 包含的字段有

{
  "amount": "0000000000000000", // 8字节
  "scriptPubKeySize": "22", // scriptPubKey 字节长度, 1 字节
  "scriptPubKey": "OP_1 OP_PUSHBYTES_32 <32 字节Taproot 输出公钥>" // 34字节
}

三个字段中唯一没有确定的是转账金额, 转账金额需要根据当前比特币网络的费率以及花费该 UTXO 所在交易(Reveal 交易)的字节大小确定。

Reveal 交易的输入只有该 P2TR 类型的 UTXO, 并且提供的解锁脚本在 witness 字段中, 以及输出一个包含546聪的新 UTXOBrc20 数据的所有者。

Reveal 交易动态字段 witness 包含三个部分:

witness: [
  <signature>, // 64 字节签名数据
  <花费脚本>, // 可计算出字节大小
  <controlblock> // 33 字节控制块
]

witness 字段数据的大小可以预先确定, 交易输出的大小也可以预先确定, 整个 Reveal 交易的大小便可以确定。根据当前网络费率, 转账金额可以确定。

例如有如下 Reveal 交易

02000000000101d436e9128462ab6fe08e1b0c95d9ce45e29d84d2f64af518d6ef2f7bd13c81a40000000000fdffffff012202000000000000160014bac6feee762f19adbd72e2714774e738f7b8b8cb0340944b955d9042b4b4f737f050358e62cbacd734606ba23dc8f051833472b843fd7ae5226c387aa0794403d1b0833467a9627efa795008a30035bfdd3d0f73308e79208ac66a3c51520a65f500edf36516a51e0f6ee3929f795c4495cdaddb0bed4424ac0063036f7264010118746578742f706c61696e3b636861727365743d7574662d3800337b2270223a226272632d3230222c226f70223a226d696e74222c227469636b223a2273617473222c22616d74223a223130227d6821c0b48573b93f15ed2aa0ec40caa44fcbdffddfe072ecebdfa56fae8396e8db18eb00000000

通过预先计算 Reveal 交易的大小为 138vB, 假设当前网络的费率是 612.3 sat/vB。总手续费需要 138612.384500sat138 * 612.3 \approx 84500 sat, 加上需要最低的转账金额 546 sat。所以 Commit 交易需要给生成的 Taproot 地址转 84500 + 546 = 85046 sat

Commit 交易中如果所有输入的 UTXO 金额扣除当前交易费和转账金额 85046 sat 后的结果大于或等于 546 sat, 还需要找零给原地址。

为什么是 546 sat ?

546 sat 在比特币交易中被称为 dust limit, 是 UTXO 包含的的最小金额。 低于这个金额的交易, 可能会被比特币节点拒绝转发。

Reveal 交易

Reveal 交易用于花费 Commit 交易输出的 UTXO, 输出 546 聪给 Brc20 数据的所有者。

示例交易:

commit: https://mempool.space/testnet/tx/a4813cd17b2fefd618f54af6d2849de245ced9950c1b8ee06fab628412e936d4 reveal: https://mempool.space/testnet/tx/b159d03058115115b209b4dbff51d75175225320349762011fdefda66a3cbe2d

索引器

索引器用于监听比特币网络, 并解析交易数据, 并将 Brc20 数据存储到数据库中。

索引器是完全的链下服务, 可以存在不同的实现, 无法保证数据的一致性。目前使用较多的是 libbrc20-indexer

Copyright © 2024 HeapUp