惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
Webroot Blog
Webroot Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
T
Threat Research - Cisco Blogs
V2EX - 技术
V2EX - 技术
L
LINUX DO - 热门话题
Google DeepMind News
Google DeepMind News
Recorded Future
Recorded Future
S
Schneier on Security
I
InfoQ
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The GitHub Blog
The GitHub Blog
S
Security @ Cisco Blogs
O
OpenAI News
W
WeLiveSecurity
Vercel News
Vercel News
阮一峰的网络日志
阮一峰的网络日志
Simon Willison's Weblog
Simon Willison's Weblog
人人都是产品经理
人人都是产品经理
Cloudbric
Cloudbric
The Last Watchdog
The Last Watchdog
The Hacker News
The Hacker News
Google Online Security Blog
Google Online Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
NISL@THU
NISL@THU
T
Tailwind CSS Blog
V
Visual Studio Blog
PCI Perspectives
PCI Perspectives
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Jina AI
Jina AI
D
DataBreaches.Net
B
Blog RSS Feed
N
News and Events Feed by Topic
N
News and Events Feed by Topic
H
Heimdal Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
腾讯CDC
Latest news
Latest news
V
Vulnerabilities – Threatpost
Hacker News: Ask HN
Hacker News: Ask HN
WordPress大学
WordPress大学
V
V2EX
aimingoo的专栏
aimingoo的专栏
博客园 - 司徒正美
Apple Machine Learning Research
Apple Machine Learning Research
D
Darknet – Hacking Tools, Hacker News & Cyber Security
The Register - Security
The Register - Security
Help Net Security
Help Net Security

smallyu的博客

为什么买的加密货币一定要去中心化 RISC-V 虚拟机无法代替 EVM 虚拟机 Polkadot 的 Existential Deposit 机制错在哪里 利息,套利,交易策略,金融市场 LayerZero 从来不挑战比特币的地位 默认参数引起的以太坊节点运行错误 硬件钱包与资产安全 预测市场比币圈更赌场 为什么去中心化的跨链桥不可能实现 我对于 AI 时代的答案 不要投资任何隐私币 区块链技术世界的三大真理 以太坊 AA 钱包的致命问题是什么 基于 AI 语义执行的 MCP 区块链的设计 一个集成 Geth 和 CometBFT 的兼容层 我的加密货币定投策略(二) DeFi 基础: 理解 AMM 定价机制 Go 语言 GMP 调度器的原理是什么 Web3 项目分析计划 对 Psyche Network 项目的分析 continuation 教程: 理解 CPS 比特币脚本开发教程 我从王垠的计算机科学视频班学到了什么 区块链技术面试题(2025年版) Rust 语言容易让新手困惑的一个“过度优化” Solana 智能合约开发教程 (1) 尝试开发一个最小 EVM 虚拟机 基于 ZK 的链上身份系统设计 一个 Web3 打赏系统的设计 鼓吹 Cursor 的人技术能力都差 关于 Code Review 的礼节 假如启动一个新的以太坊 PoS 网络 发行加密货币的最好方式 所有 BFT 共识的区块链都是中心化的 对 2025 年区块链行业的预测 Restaking 项目的经济难题 如何看懂任意区块链项目的技术架构 为什么不要做以太坊的二次开发 为什么不要做智能合约开发和 DeFi 开发 我的加密货币定投策略(一) PoS 类型的区块链如何处理分叉 Ethereum Casper 为什么需要 EIP-7251 区块链中的 PBFT 不需要第二次投票 开发者的思维方式 发币的核心要点 炒币投资的小 tips 为什么说 PoW 比 PoS 更加去中心化 牛市对普通人来说意味着什么 如何健康地远程工作 为什么比特币不用概率加密函数 程序员的 “服从权威” 心理 区块链技术面试题 如何区分公有链和联盟链 对 Layer 2 项目创业想法的回复 对区块链共识机制的理解 Pebbling Game 鹅卵石游戏 PDP 文件证明的局限性 不要小瞧 ChatGPT 为什么炒币不是一个好主意 一种在区块链上生成随机数的机制 为什么以太坊的私钥计算不可逆 关于以太坊的私钥碰撞 “猜均值的2/3” dApp 游戏设计 Proof of Storage/Space/Replication 的区别 Proofs of Retrievability 文件证明的含义 对 S-PDP 文件证明的示例和解释 我的加密货币交易机器人 对区块链行业的见闻 随机确认块的共识机制 VRF + BFT 共识引起交易失败的问题 为什么要重视编程思想 对 Web 3.0 的理解 GitBook 好用吗? 一种区块链节点存储扩容的方式 一种基于“自我中心主义”的共识机制 链表常见算法题及解析 基于 Multi-Linked List 的区块链设想 理解哈希函数与序列化 联盟链比公有链差在哪儿 在 Dijkstra 算法中保存路径 为什么数字货币使用区块链是政治问题 区块链:下一代数字身份认证体系的基石 网页技术能实现 3D 建模吗? 给区块链一个定义 从 Erlang 开始了解 Actor 模型 一种侧边导航栏的交互方式 Rust 的 ownership 是什么? Haskell 中的 Monad 是什么? 浅析 Libra 背后的区块链技术 对区块链的理性认识 Rust 基础语法概述 基于 Java 的爬虫框架 WebCollector Kotlin:简化版的 Scala JavaScript 有关联数组吗? 主流编程语言的异常处理机制 Go 语言基本语法 Scala 语法基础 用 Scala 改写 Java 浅度实践 Python 获取海贼王更新信息 HTML5 音乐可视化
如何开发一个比特币符文(Runes)协议
2025-07-15 · via smallyu的博客

2025-07-15

比特币符文(Runes)在技术原理上比较简单,很容易理解,实现 Runes 只需要用到比特币脚本中的 OP_RETURN 操作符。也是正因为简单,所以在技术特性上, Runes 相对干净一点,没有铭文和 RGB 那么复杂的链下状态。而 Runes 厉害的地方在于,能把这样一个生态给玩起来,虽然现在也凉了,但是我们不那么关心市场表现,从技术的角度,非常切实的看一下 Runes 是如何实现的,并且我们自己会一步一步开发一个简化的 Runes 协议出来。当我们真正理解了 Runes 协议,就可以直接看懂更复杂的项目,比如 Alkanes,一个在比特币上支持 WASM 智能合约的协议。

这篇文章的操作基于《比特币脚本开发教程》中的知识,如果缺少对比特币脚本的基本了解,可以先看一下基础教程。

1. 定义数据结构

首先把 Runes 相关的操作定义为 json 格式的数据结构。用什么语言都可以,这里用的是 Rust。为了简化教程,可以省去一些实际中常用的元素,比如 transfer 的操作定义中,应该允许一次性转账给多个目标地址,但这里只有一个目标地址,没有用数组:

struct IssueRune
{
    op: u8,          // 固定为 0,代表发行
    symbol: String,  // Rune name
    supply: String,  // 发行总量
}

struct TransferRune
{
    op: u8,          // 固定为 1,代表转账
    id: u64,         // rune_id
    vout: u32,       // 转账目标
    amount: String,  // 转账金额
}

这个结构定义中,唯一可能有点迷惑的是 vout 字段,一般我们要转账给一个目标地址的时候,会使用目标地址的字符串作为值,但是 Runes 协议中,为了节省链上空间,使用 vout 也就是当前这笔交易、第几个输出的索引作为 Runes 转账的目标。因为每一笔交易的第 0 个输出会包含 OP_RETURN 的值,转账操作的字符全部会放到 OP_RETURN 的内容中,所以在一笔交易里,vout 只用数字就足以表明转账的目标地址是哪个。交易数据的结构大概是这样:

tx {
  vin:   [...]
  vout:  [
    { vout: 0, scriptPubKey: OP_RETURN <json数据> },
    { vout: 1, scriptPubKey: OP_0 <转账地址1> },
    { vout: 2, scriptPubKey: OP_0 <转账地址2> }
  ]
}

接着给数据结构定义一下序列化函数,让结构体可以转变为 json 字符串:

impl IssueRune
{
    fn toJson(&self) -> String
    {
        format!(
            "{{\"op\":{},\"symbol\":\"{}\",\"supply\":\"{}\"}}",
            self.op, self.symbol, self.supply
        )
    }
}

impl TransferRune
{
    fn toJson(&self) -> String
    {
        format!(
            "{{\"op\":{},\"id\":{},\"vout\":{},\"amount\":\"{}\"}}",
            self.op, self.id, self.vout, self.amount
        )
    }
}

2. 发行 Runes 代币

接下来的操作都基于本地启动的 regtest 节点,所以记得先启动起来,同时验证下有没有加载钱包、钱包里有没有余额。然后准备一下要发行 Rune 的 json 数据,比如这样,发行的 Rune 叫 Doge,总发行量 1000 个:

fn issue_rune()
{
    let issue = IssueRune 
    {
        op: 0,
        symbol: "Doge".to_string(),
        supply: "1000".to_string(),
    };
    println!("Issue Doge JSON: {}", issue.toJson());
}

运行这个函数,就会得到这样的输出,后面的 json 数据很重要,我们稍后会把这个数据发送到链上:

Issue Doge JSON: {"op":0,"symbol":"Doge","supply":"1000"}

然后运行这个命令行,把 json 数据转变为 16进制字符串:

echo -n "{"op":0,"symbol":"Doge","supply":"1000"}" | xxd -p -c 999

我得到了这样的输出:

7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d

这就是会放到 OP_RETURN 后面、用来上链的数据。注意 OP_RETURN 最多支持 80 个字节,所以这个数据不能太长。

接着查看并挑一笔未花费的输出,因为 Runes 所有的操作都必须绑定到 UTXO 上。用这个命令查看你的钱包有哪些 UTXO 可用,然后挑一个你喜欢的:

bitcoin-cli -datadir=./ -regtest listunspent

比如我要用的 UTXO 是这样:

{
  "txid": "8bfd524e9fc150dab11289d7e6d07860b2b5d6acb54b278a5dc1d1d7631bc8fa",
  "vout": 0,
  "address": "bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw",
  "amount": 50.00000000,
  // ...
}

然后生成一个找零地址,用来接收比特币余额,这里用了 legecy 格式的地址,这个不是强制的,用 SegWit 的地址也不影响:

bitcoin-cli -datadir=./ getrawchangeaddress legacy

我生成的地址是 n4Ybvvzm9vRQepuMpXBnTWWbYuTgsPSZCV,接下来可以用这个地址构建交易了:

bitcoin-cli -datadir=./ createrawtransaction \
  '[{"txid":"8bfd524e9fc150dab11289d7e6d07860b2b5d6acb54b278a5dc1d1d7631bc8fa","vout":0}]' \
  '[{"data":"7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d"},{"n4Ybvvzm9vRQepuMpXBnTWWbYuTgsPSZCV":49.99}]'

注意这个命令给找零地址的金额为 49.99,这种操作是不可以直接在主网使用的,需要精确计算余额和手续费的差值,然后给找零地址,不然会有很大的资金损失。这里只是懒得计算精确值。

生成交易数据后,对交易进行签名:

bitcoin-cli -datadir=./ signrawtransactionwithwallet 0200000001fac81b63d7d1c15d8a274bb5acd6b5b26078d0e6d78912b1da50c19f4e52fd8b0000000000fdffffff020000000000000000206a1e7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307dc0aff629010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac00000000

发送签名后的交易到链上:

bitcoin-cli -datadir=./ sendrawtransaction 02000000000101fac81b63d7d1c15d8a274bb5acd6b5b26078d0e6d78912b1da50c19f4e52fd8b0000000000fdffffff020000000000000000206a1e7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307dc0aff629010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac02473044022004a2553cc5348dd4521c093149b0ba5e5603fe4134d06a455e12abeac097ea19022076e72632b2488e1316e54559ed733b37de9ce7fd04119e78a59546a3d2c1faea0121020b396a9dfa1655feef066fe03b403d3e4bdee41ef9b26551497c0921acbf6bc100000000

要留意这个命令会输出一个 txid,这个 txid 比较重要,我们后续会从这个 txid 来转出 Doge 代币,所以要记得留下这个 txid 的记录,我的交易哈希是:e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a。挖一个区块来确认交易:

bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw

如果一切顺利,交易数据就应该已经上链了。那么链下的解析器在拿到这笔交易后,会看到发行 Rune 的操作,并且解析出来。如果想确认下 OP_RETURN 的数据是不是写对了,可以解码一下刚才广播的交易详情:

bitcoin-cli -datadir=./ decoderawtransaction 02000000000101fac81b63d7d1c15d8a274bb5acd6b5b26078d0e6d78912b1da50c19f4e52fd8b0000000000fdffffff020000000000000000206a1e7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307dc0aff629010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac02473044022004a2553cc5348dd4521c093149b0ba5e5603fe4134d06a455e12abeac097ea19022076e72632b2488e1316e54559ed733b37de9ce7fd04119e78a59546a3d2c1faea0121020b396a9dfa1655feef066fe03b403d3e4bdee41ef9b26551497c0921acbf6bc100000000

输出的结果是这样:

"vout": [
    {
      "value": 0.00000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "OP_RETURN 7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d",
        // ...
      }
    },
    {
      "value": 49.99000000,
      "n": 1,
      "scriptPubKey": { //... }
    }
  ]

很明显看到了在第 0 个输出中,有 OP_RETURN 十六进制数据 的内容,如果还是不放心,可以 decode 一下 16进制字符串:

echo -n "7b6f703a302c73796d626f6c3a446f67652c737570706c793a313030307d" | xxd -r -p

应该得到:

{op:0,symbol:Doge,supply:1000}

3. 转账 Runes 代币

刚才创建了 Doge 代币,发行量是 1000,我们接下来通过一笔转账交易,来转出这 1000 个 Doge。

首先得计算一下 rune_id,也就是我们刚才发行的 Doge 的唯一 ID 是什么,因为 Symbol 字符串是有可能重复的,而且占用字符空间也多,一般会做一些工程上的折中,比如对 txid 按照字节反序,然后取前 8 字节,得到一个 u64 长度的数字,像这样:

fn calc_run_idby_txid()
{
    let txid = "e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a".to_string();
    // 按字节反序,然后取前 8 字节
    let mut bytes = hex::decode(txid).unwrap();
    bytes.reverse();
    let run_id = u64::from_le_bytes(bytes[0..8].try_into().unwrap());
    println!("Run ID: {}", run_id);
}

这个函数运行后会得到 10367542271932362826,我们把这个数字作为 rune_id,去构建转账 rune 需要的 json 数据:

fn transfer_rune()
{
    let transfer = TransferRune
    {
        op: 1,
        id: 10367542271932362826,
        vout: 1,
        amount: "1000".to_string(),
    };
    println!("Transfer Rune JSON: {}", transfer.toJson());
}

这个参数里要留意 vout 的值,它是接下来构建交易的时候,要转出到某个地址的 vout 的索引,和创建代币时候的交易没有任何关系。代码运行后得到这样的结果:

Transfer Doge JSON: {"op":1,"id":10367542271932362826,"vout":1,"amount":"1000"}

接下来就可以重复之前的步骤,把 json 转为 16进制字符串:

echo -n "{"op":1,"id":10367542271932362826,"vout":1,"amount":"1000"}" | xxd -p -c 999

我得到 7b6f703a312c69643a31303336373534323237313933323336323832362c766f75743a312c616d6f756e743a313030307d

创建一个新地址用于接收 Doge:

bitcoin-cli -datadir=./ getnewaddress

我的新地址是:bcrt1qc250507tws9z9wkurfcv3jue2nls6npzaqt7ka

利用刚才得到的参数,组装一笔转账 Doge 的交易:

bitcoin-cli -datadir=./ createrawtransaction \
'[{"txid":"e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a","vout":1}]' \
'[{"data":"7b226f70223a312c226964223a31303336373534323237313933323336323832362c22766f7574223a312c22616d6f756e74223a2231303030227d"},{"bcrt1q0n2x7030x59j5ql9pp6mw0tps74ag0znrdp45r":0.01},{"n4Ybvvzm9vRQepuMpXBnTWWbYuTgsPSZCV":49.9798}]'

这里和之前的交易略有不同,包含两个输出,第一个是接收 Doge 的地址,金额随意,因为重点在于 Doge 余额,而不是 BTC 余额。第二个参数则是找零地址,我们前面的交易里用到过。

剩下的操作轻车熟路,对这笔交易签名、把交易广播出去、挖一个新区块让交易确认:

# 对交易签名
bitcoin-cli -datadir=./ signrawtransactionwithwallet 02000000014af05f48c4e8e08f277d61c7542343729840e7c16465ba47ee982f8b0b1d06e20100000000fdffffff0300000000000000003d6a3b7b226f70223a312c226964223a31303336373534323237313933323336323832362c22766f7574223a312c22616d6f756e74223a2231303030227d40420f00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c53601fe729010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac00000000

# 广播交易
bitcoin-cli -datadir=./ sendrawtransaction 02000000014af05f48c4e8e08f277d61c7542343729840e7c16465ba47ee982f8b0b1d06e2010000006a47304402201437a9e83ae0c6842ebd9d355af9c7be1f6f2eaa070b5d7a6e02e13ca8f2d13102206d05753c428f526b8c6636022991591517cc7d7982badfc633519cb44715957a0121026f441e8156148d0bb4963edaff187873f9800a37bb5f0731256e38d632031283fdffffff0300000000000000003d6a3b7b226f70223a312c226964223a31303336373534323237313933323336323832362c22766f7574223a312c22616d6f756e74223a2231303030227d40420f00000000001600147cd46f3e2f350b2a03e50875b73d6187abd43c53601fe729010000001976a914fc9ab9cd801c625c9fe323fe669e6a3e362eed8088ac00000000
# 得到交易哈希:80709a25e5355d51ee6d7fb625c40e9c4c49b049afa3aca18aeaa03bc685c1f0

# 确认交易
bitcoin-cli -datadir=./ generatetoaddress 1 bcrt1q6c8d9vw62rdee72xcqx3d97w8qh8mfg8ky8zjw

到这一步,转账 Doge 的交易就完成并且上链了。

4. 解析 Runes 交易

你也许有点纳闷,这不就是发了两笔普通的比特币交易吗,只是放了两个 json 数据到交易上。Runes 就是这样,所有的操作,就在 OP_RETURN 允许的那 80 个字节的空间里完成。链上只记录 Runes 的操作,而不维护 Runes 的最终状态。包括有哪些代币、代币余额等信息,全部需要链下程序根据协议进行解析,状态也全部在链下程序维护。

我们首先可以通过已知的交易哈希,获取到这两笔交易的全部详情数据。如果交易哈希不是已知,可以监听扫描全部区块的全部交易,然后按照协议约定解析就行了。这里简化一点。

Cargo.toml 文件中导入依赖包:

[dependencies]
hex              = "0.4"
bitcoin          = "0.31"          # Script & consensus decode
bitcoincore-rpc  = "0.18"          # RPC client
serde            = { version = "1.0", features = ["derive"] }
anyhow           = "1.0"

通过 RPC 查询节点上的交易数据:

// 导入必要依赖
use bitcoin::{Transaction, Txid};
use bitcoincore_rpc::{Auth, Client, RpcApi};
use serde::Deserialize;
use std::path::PathBuf;
use std::str::FromStr;

fn parse_tx()
{
    // 去启动比特币节点的数据目录下,找用来 rpc 鉴权的 cookie 文件
    let mut cookie = PathBuf::from("/Users/smallyu/work/github/bitcoin-regtest");
    cookie.push("regtest/.cookie");

    let rpc = Client::new(
        "http://127.0.0.1:18443",
        Auth::CookieFile(cookie),
    ).unwrap();

    // 我们已知的交易哈希
    let issue_txid    = Txid::from_str("e2061d0b8b2f98ee47ba6564c1e7409872432354c7617d278fe0e8c4485ff04a").unwrap();
    let transfer_txid = Txid::from_str("80709a25e5355d51ee6d7fb625c40e9c4c49b049afa3aca18aeaa03bc685c1f0").unwrap();

    // 这里会得到完整的交易数据
    let issue_hex    = rpc.get_raw_transaction_hex(&issue_txid, None).unwrap();
    let transfer_hex = rpc.get_raw_transaction_hex(&transfer_txid, None).unwrap();

    println!("Issue Hex: {}", issue_hex);
    println!("Transfer Hex: {}", transfer_hex);

    // 调用函数来解析交易
    parse_op_return(issue_hex);
    parse_op_return(transfer_hex);
}

这个函数在运行的时候,会从链上节点,查询出真实的已经上链的交易数据。接下来可以对这两笔交易的 Runes 操作做解析:

fn parse_op_return(tx_str: String)
{
    let tx: Transaction = bitcoin::consensus::deserialize(&hex::decode(tx_str).unwrap()).unwrap();
    let script = tx.output[0].script_pubkey.clone();
    // OP_RETURN, DATA
    let mut iter = script.instructions();
    let mut op_return = iter.next();
    let mut data = iter.next();
    // 解析数据
    match op_return
    {
        Some(Ok(op_return)) =>
            {
                match data
                {
                    Some(Ok(data)) =>
                        {
                            match (data)
                            {
                                bitcoin::blockdata::script::Instruction::PushBytes(bytes) =>
                                    {
                                        let json_str = std::str::from_utf8(bytes.as_ref()).unwrap();
                                        println!("{}", json_str);
                                    }
                                _ => panic!("Expected OP_RETURN with data"),
                            }
                        }
                    _ => panic!("No data found in OP_RETURN"),
                }
            }
        _ => panic!("No OP_RETURN found in script"),
    }
}

解析数据的代码部分,写法上嵌套有点多,只是因为我不喜欢用语法糖。想看起来更舒服的话,也可以在代码写法上做调整,变得更精简,不过无论写法如何,代码干的事情都一样。这个函数会从交易数据里,经过层层解析,打印出这样的结果:

{op:0,symbol:Doge,supply:1000}
{"op":1,"id":10367542271932362826,"vout":1,"amount":"1000"}

这样,我们就看到了期望的两个 Runes 动作,第一个是发行 Doge,第二个是对 Doge 进行转账。

以上过程就是 Runes 协议比较核心的内容,剩下的只需要把链下程序扩充一下,记录 Runes 状态、根据 rune_id 关联 Rune 操作、储存和显示余额变更等信息就可以了。