比特币序数理论

中本聪(satoshis),这里简称,是比特币网络的原子原生货币。一个比特币可以细分为1亿个,并且不能再继续细分。

序数理论关注的是中本聪,赋予个体身份,并允许它们被追踪、转移和赋予意义。

序数理论的优势

  • 序数理论不依赖比特币之外的侧链或代币,并且可以在不对比特币网络进行任何更改的情况下使用。
  • 序数理论赋予每个独一无二的价值,使他们能够作为收藏品被收集和交易。
  • 任意资产,例如 NFT、安全代币、账户或稳定币,可以使用序数作为稳定标识符附加到上。

序数理论的劣势

  • 序数理论给单独的赋予意义,但是比特币网络不鼓励太小的UTXO(Unspend transaction output)存在,如果一个交易的输出太小(一般而言,小于330),网络不会对该交易进行打包。因此,无法仅转移单个。另外,如果想要把连续的3个分开,需要精心设计额外的交易来达成。

  • 序数理论的盛行会导致大量很小的UTXO存在,这加重了比特币网络对UTXO的管理负担,并且这些UTXO作为比特币而言价值很低,归集成本已经超出了本身的价值,因此这部分可以认为从比特币的流通中燃烧了,某种意义上减少了比特币的供应量。(也许这并不是劣势:))

的稀缺性

序数理论给每个编号,因此,某些特殊意义编号的便有了稀缺性。比特币网络中存在一些周期性的事件,有些很频繁,有些很久才出现一次,自然而然就给赋予了稀缺性。

下面是比特币网络中的周期性事件:

  • 区块 比特币网络平均大约每10分钟出一次块。
  • 难度调整 每2016个区块,大约每2周,比特币网络进行一次挖矿的难度调整。
  • 减半 每21万个区块,大约每4年,每个区块挖矿的奖励减半。
  • 循环 每6次减半,大约每24年,减半的事件会和难度调整事件重合,成为循环,第一次难度重合事件会发生在2032年。

这些周期性事件给了我们下面的稀有性定义:

  • 普通聪 每一个区块里的除了第一个聪的其它聪(索引不为0的聪)。
  • 罕见聪 每一个区块的第一个聪。
  • 稀有聪 每个难度调整周期的第一个聪。
  • 史诗聪 每个减半周期的第一个聪。
  • 传奇聪 每个循环周期的第一个聪。
  • 神话聪 创世区块的第一个聪,只有1个。

的命名

根据的稀有性,可以使用度数表示法给命名:

1
2
3
4
5
A°B′C″D‴
│ │ │ ╰─ 聪在区块中的索引
│ │ ╰─── 区块在难度调整周期中的索引
│ ╰───── 区块减半周期中的索引
╰─────── 位于第几个循环,数字从0开始

下面是一些例子。

普通聪:

1
2
3
4
5
1°1′1″1‴
│ │ │ ╰─ 不是区块中的第一个聪
│ │ ╰─── 所在区块不是难度调整周期中的第一个区块
│ ╰───── 所在区块不是减半周期里的第一个区块
╰─────── 位于第二个循环

罕见聪:

1
2
3
4
5
1°1′1″0‴
│ │ │ ╰─ 区块中的第一个聪
│ │ ╰─── 所在区块不是难度调整周期中的第一个区块
│ ╰───── 所在区块不是减半周期里的第一个区块
╰─────── 位于第二个循环

稀有聪:

1
2
3
4
5
1°1′0″0‴
│ │ │ ╰─ 区块中的第一个聪
│ │ ╰─── 难度调整周期中的第一个区块
│ ╰───── 所在区块不是减半周期里的第一个区块
╰─────── 位于第二个循环

史诗聪:

1
2
3
4
5
1°0′1″0‴
│ │ │ ╰─ 区块中的第一个聪
│ │ ╰─── 所在区块不是难度调整周期中的第一个区块
│ ╰───── 减半周期里的第一个区块
╰─────── 位于第二个循环

传奇聪:

1
2
3
4
5
1°0′0″0‴
│ │ │ ╰─ 区块中的第一个聪
│ │ ╰─── 难度调整周期中的第一个区块
│ ╰───── 减半周期里的第一个区块
╰─────── 位于第二个循环

神话聪:

1
2
3
4
5
0°0′0″0‴
│ │ │ ╰─ 区块中的第一个聪
│ │ ╰─── 难度调整周期中的第一个区块
│ ╰───── 减半周期里的第一个区块
╰─────── 位于第一个循环

如果区块索引为0,可以忽略,下面是罕见聪的表示:

1
2
3
4
1°1′1″
│ │ ╰─── 所在区块不是难度调整周期中的第一个区块
│ ╰───── 所在区块不是减半周期里的第一个区块
╰─────── 位于第二个循环

除了按照度的表示方法命名,聪最直接的命名方式是用序数来表示,每个聪的序数如何获取将在后面说明。另外还可以用a-z的字符对的序号进行编码得到字符名称等。

其它稀缺性的定义

除了根据比特币的周期性事件来定义的稀缺性,还有其它的维度,比如:

  • 中本聪本人挖掘出来的
  • 序数对称或者回文
  • 序数末尾有多个0
  • 序数在数学上有特殊意义的
  • 和特定历史事件关联的
  • 每个区块的最后一个
  • 等等

如何给编号

每个都按其开采顺序从0开始进行序列编号。这些数字被称为序数,给出了总供应中每个聪的顺序。 序数这个词非常明确,因为它在比特币协议的其它地方没有使用。

根据交易中输入和输出的大小和顺序,交易输入中的聪序号以先进先出的顺序传输到输出。在普通交易中,并没有创建新的,因此只是的转移。

如果使用与当前UTXO集合中的输出相同的交易ID来挖掘交易,则遵循比特币核心的行为,新的交易输出将取代旧的UTXO集合条目,从而破坏第一笔交易的任何未使用输出中包含的。该规则需要处理具有重复交易ID的两对主网交易,即区块91812/91842和91722/91880的coinbase交易,这些交易是在BIP-34使得不可能创建具有重复ID的交易之前开采的。

出于算法实现的目的,Coinbase 交易被认为具有大小等于区块奖励的隐式输入,然后是块中每个付费交易的输入,按照这些交易出现在块中的顺序。隐试区块奖励输入包含该区块新创建的。隐试费用输入则包含在区块交易中作为费用支付的

Coinbase中少付区块奖励不会改变后续区块中开采的序数。序号仅取决于可以开采的数量,而不是实际开采的数量。

源码阅读

首先是一些基本结构的定义:

  • 减半周期(Epoch)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 减半周期用u32表示
pub struct Epoch(pub u32);

impl Epoch {
// 34个减半周期的第一个聪的序号
// 第一个减半周期挖出来的聪 = 每个周期2100万个区块 * 每个区块50个比特币奖励;随后每个减半周期奖励逐次减半...
pub const STARTING_SATS: [Sat; 34] = [
Sat(0),
Sat(1050000000000000),
Sat(1575000000000000),
Sat(1837500000000000),
// ......
Sat(2099999997060000),
Sat(2099999997480000),
Sat(Sat::SUPPLY),
];

// 奖励结束后的第一个周期
pub const FIRST_POST_SUBSIDY: Epoch = Self(33);

// 减半周期的奖励
pub fn subsidy(self) -> u64 {
if self < Self::FIRST_POST_SUBSIDY {
// 每次减半,奖励减半
// COIN_VALUE = 100,000,000
(50 * COIN_VALUE) >> self.0
} else {
// 33次减半后,区块奖励为0
0
}
}

// 减半周期的第一个聪
pub fn starting_sat(self) -> Sat {
*Self::STARTING_SATS.get(usize::try_from(self.0).unwrap()).unwrap_or_else(|| Self::STARTING_SATS.last().unwrap())
}

// 减半周期的第一个区块高度
pub fn starting_height(self) -> Height {
Height(self.0 * SUBSIDY_HALVING_INTERVAL)
}
}

// 类型转换 Sat -> Epoch
// 聪所位于的减半周期
impl From<Sat> for Epoch {
fn from(sat: Sat) -> Self {
if sat < Self::STARTING_SATS[1] {
Epoch(0)
} else if sat < Self::STARTING_SATS[2] {
Epoch(1)
}
// ......
else if sat < Self::STARTING_SATS[33] {
Epoch(32)
} else {
Epoch(33)
}
}
}

// 类型转换 Height -> Epoch
// 区块所位于的减半周期
impl From<Height> for Epoch {
// SUBSIDY_HALVING_INTERVAL = 210,000
Self(height.0 / SUBSIDY_HALVING_INTERVAL)
}
  • 区块高度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pub struct Height(pub u32);

impl Height {
// 区块高度的u32表示
pub fn n(self) -> u32 {
self.0
}

// 区块奖励
pub fn subsidy(self) -> u64 {
Epoch::from(self).subsidy()
}

// 区块的第一个聪
pub fn starting_sat(self) -> Sat {
let epoch = Epoch:::from(self);
let epoch_starting_sat = epoch.starting_sat();
let epoch_starting_hegiht = epoch.starting_height();
// 减半周期第一个聪索引 + 减半周期内的区块数 * 区块奖励
epoch_starting_sat + u64::from(self.n() - epoch_starting_height.n()) * epoch.subsidy()
}

// 区块在难度调整周期内的索引
pub fn period_offset(self) -> u32 {
// DIFFCHANGE_INTERVAL = 2016
self.0 % DIFFCHANGE_INTERVAL
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 使用整数序数来表示聪
pub struct Sat(pub u64);

impl Sat {
// 比特币总量无限接近2100万个
pub const SUPPLY: u64 = 2099999997690000;

// 最后一个聪的索引
pub const LAST: Self = Self(Self::SUPPLY - 1);

// 聪的序数表示
pub fn n(self) -> u64 {
self.0
}

// 聪的度表示
pub fn degree(self) -> Degree {
self.into()
}

// 聪在当前减半周期中的位置
pub fn epoch_position(self) -> u64 {
self.0 - self.epoch().starting_sat().0
}

// 聪所位于的区块高度
pub fn height(self) -> Height {
// 当前减半周期起始区块索引 + 聪在当前减半周期索引/每个区块奖励
self.epoch().starting_height()
+ u32::try_from(self.epoch_position() / self.epoch().subsidy()).unwrap()
}

// 聪位于第几个循环
pub fn cycle(self) -> u32 {
Epoch::from(self).0 / CYCLE_EPOCHS
}

// 聪位于第几个减半周期
pub fn epoch(self) -> Epoch {
self.into()
}

// 聪位于第几个难度调整周期
pub fn period(self) -> u32 {
self.height().n() / DIFFCHANGE_INTERVAL
}

// 聪在区块中的索引
pub fn third(self) -> u64 {
self.epoch_position() % self.epoch().subsidy()
}
}
  • 的度表示法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pub struct Degree {
pub hour: u32,
pub minute: u32,
pub second: u32,
pub third: u64,
}

impl From<Sat> for Degree {
fn from(sat: Sat) -> Self {
// 区块高度
let height = sat.height().n();
Degree {
// CYCLE_EPOCHS = 6
hour: height / (CYCLE_EPOCHS * SUBSIDY_HALVING_INTERVAL),
minute: height % SUBSIDY_HALVING_INTERVAL,
second: height % DIFFCHANGE_INTERVAL,
third: sat.third(),
}
}
}
  • 稀有性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pub enum Rarity {
Common,
Uncommon,
Rare,
Epic,
Legendary,
Mythic,
}

impl From<Sat> from Rarity {
fn from(sat: Sat) -> Self {
let Degree {
hour,
minute,
second,
third
} = sat.degree();
}

if hour == 0 && minute == 0 && second == 0 && third == 0 {
Self::Mythic
} else if minute == 0 && second == 0 && third == 0 {
Self::Legendary
} else if minute == 0 && third == 0 {
self::Epic
} else if second == 0 && third == 0 {
Self::Rare
} else if third == 0 {
Self::Uncommon
} else {
Self::Common
}
}

为比特币网络建立序数索引

在比特币网络中,位于UTXO中,很自然地,我们想要能够快速检索某个在哪个UTXO中,以及某个UTXO中包含哪些
对于前者,如果要给每个索引的话,会消耗巨大的存储空间,一般是记录稀有对应的UTXO。下面是ord的具体实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
impl<'index> Updater<'index> {
pub(crate) fn update_index(&mut self, mut wtx: WriteTransaction) -> Result {
// 省略部分初始化代码...

// rx是区块数据接收channel
let rx: Receiver<BlockData> = Self::fetch_block_from(self.index. self.height)?;

// output_sender是OutPoint发送channel
// txout_receiver是TxOut接收channel
// spawn_fetcher接收output_sender发送的OutPoint,从btc网络中下载对应的TxOut
let (mut output_sender: Sender<OutPoint>, mut txout_receiver: Receiver<TxOut>) = Self::spawn_fetcher(self.index);

// utxo_cache是我们最终想得到的,OutPoint是每个交易的输出(utxo),而UtxoEntryBuf则记录了该输出包含哪些聪
let mut utxo_cache: HashMap<OutPoint, UtxoEntryBuf> = HashMap::new();
while let Ok(block: BlockData) = rx.recv() {
self.index_block(
&mut output_sender,
&mut txout_receiver,
// 数据库写操作事务
&mut wtx,
block,
// 返回结果
&mut utxo_cache,
)?;

// 省略后续持久化代码...
}
}
}

index_block函数调用了index_utxo_entries,下面是其实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
fn index_utxo_entrie(
&mut self,
block: BlockData,
txout_receiver: &mut broadcast::Receiver<TxOut>,
output_sender: &mut mpsc::Sender<OutPoint>,
wtx: &mut WriteTransaction,
utxo_cache: &mut HashMap<OutPoint, UtxoEntryBuf>,
...
) -> Result<(), Error> {
// 省略部分代码...

// coinbase交易的输入
let mut coinbase_inputs = Vec::new();

// 丢失的聪
let mut lost_sat_ranges = Vec::new();

// 初始化coinbase的输入聪
let h = Height(self.height);
if h.subsidy() > 0 {
let start: Sat = h.starting_sat();
coinbase_inputs.extend(SatRange::store((start.n(), (start + h.subsidy()).n())));
}

// 遍历区块内的所有交易
for (tx_offset, (tx, txid)) in block
.txdata
.iter()
.enumerate()
// 把coinbase交易放到最后,之所以放到最后处理是因为coinbase交易的输入包含其它交易的输出(手续费),
// 因此需要先处理其它交易,收集到手续费输出后再处理coinbase交易
.skip(1)
.chain(block.txdata.iter().enumerate().take(1)) {
let input_utxo_entries: Vec<UtxoEntryBuf> = if tx_offset == 0 {
Vec::new()
} else {
tx.input.iter().map(|input| {
let outpoint = input.previous_output.store();
// 移除作为交易输入的utxo
let entry = if let Some(entry) = utxo_cache.remove(&OutPoint::load(output)) {
entry
} else if let Some(entry) = outpoint_to_utxo_entry.remove(&outpoint)? {
entry.value().to_buf()
}
Ok(entry)
}).collect::<Result<Vec<UtxoEntryBuf>>>()?
};

// 编码后的UtxoEntry转未编码
let input_utxo_entries: Vec<ParsedUtoxEntry<'_>> = input_utxo_entries
.iter()
.map(|entry: &UtxoEntryBuf| entry.parse(self.index))
.collect::<Vec<ParsedUtxoEntry>>();

// sat_range 本来是(u64, u64)结构,被编码为11字节的[u8]来表示
let input_sat_ranges: Option<Vec<&[u8]>>;

// 输入sats - 输出sats
let leftover_sat_ranges: &mut Vec<u8>;

if tx_offset = 0 {
// coinbase交易
input_sat_ranges = Some(vec![coinbase_inputs.as_slice()]);
// coinbase交易把所有的普通交易燃烧掉的sats都当成了手续费,如果coinbase交易本身
// 还有燃烧的话,就会记录到lost_sat_ranges里面去,这时就真的燃烧了
leftover_sat_ranges = &mut lost_sat_ranges;
} else {
input_sat_ranges = Some(
input_utxo_entrie
.iter()
.map(|entry| entry.sat_ranges())
.collect(),
);
// 对于普通交易来说,未使用的聪会作为coinbase的手续费,
// 这里通过leftover_sat_ranges来收集这些手续费
leftover_sat_ranges = &mut coinbase_inputs;
}

self.index_transaction_sats(
tx,
*txid,
&mut output_utxo_entries,
input_sat_ranges.as_ref().unwrap(),
leftover_sat_ranges,
...
)?;

// 更新燃烧的聪
if !lost_sat_ranges.is_empty() {
// lost-sats是特殊的utxo,要和之前的合并
let utxo_entry = utxo_cache
.entry(OutPoint::null())
.or_insert(UtxoEntryBuf::empty(self.index));
for chunk in lost_sat_ranges.chunks_exact(11) {
let (start, end) = SatRange::load(chunk.try_into().unwrap());
if !Sat(start).common() {
// 特殊聪索引
sat_to_satpoint.insert(
&start,
&SatPoint {
outpoint: OutPoint::null(),
offset: lost_sats,
}.store()
)?;
}
lost_sats += end - start;
}
let mut new_utxo_entry = UtxoEntryBuf::new();
new_utxo_entry.push_sat_ranges(&lost_sat_ranges, self.index);
*utxo_entry = UtxoEntryBuf::merged(utxo_entry, &new_utxo_entry, self.index);
}
}
}


fn index_transaction_sats(
&mut self,
tx: &Transaction,
txid: Txid,
output_utxo_entries: &mut [UtxoEntryBuf],
input_sat_ranges: &[&[u8]],
leftover_sat_ranges: &mut Vec<u8>,
...
) -> Result {
// 当前正在处理的输入聪范围
let mut pending_input_sat_range = None;
// 聪范围转为1维
let mut input_sat_ranges_iter = input_sat_ranges
.iter()
.flat_map(|slice| slice.chunks_exact(11));

// 预分配输出的聪数组,单个输出最大不可能超过输入之和
let mut sats = Vec::with_capacity(
input_sat_ranges
.iter()
.map(|slice| slice.len())
.sum::<usize>(),
);

// 处理每一个输出,分配聪序号
for (vout, output) in tx.output.iter().enumerate() {
// 新的 utxo
let outpoint = OutPoint {
vout: vout.try_into().unwrap(),
txid,
};

// 剩余需要分配的聪
let mut remaining = output.value.to_sat();
while remaining > 0 {
// 从当前正在处理的聪范围进行分配
let range = pending_input_sat_range.take().unwrap_or_else(|| {
// 当前聪范围从输入的聪范围数组里来
SatRange::load(
input_sat_ranges_iter
.next()
.expec("insufficient inputs for transaction outputs")
.try_into()
.unwrap(),
)
});

if !Sat(range.0).common() {
// 特殊聪索引
sat_to_satpoint.insert(
key: &range.0,
value: &SatPoint {
outpoint,
offset: output.value.to_sat() - remaining,
}.store(),
)?;
}

// 当前聪范围包含的聪有多少
if count = range.1 - range.0;
// 当前聪范围使用了多少
let assigned = if count > remaining {
let middle = range.0 + remaining;
pending_input_sat_range = Some((middle, range.1));
// 使用部分
(range.0, middle)
} else {
// 使用全部
range
};
// 更新总的输出聪
sats.extend_from_slice(&assigned.store());
// 更新当前utxo剩余需要分配的聪
remaining -= assigned.1 - assigned.0;
}

output_utxo_entries[vout].push_sat_ranges(&sats, self.index);
// 清空当前输出的聪范围对
sats.clear();
}

// 还有哪些聪剩下?剩下的要么作为手续费,要么就永久燃烧了
if let Some(range) = pending_input_sat_range {
leftover_sat_ranges.extend(&range.store());
}
leftover_sat_ranges.extend(input_sat_ranges_iter.flatten());

Ok(())
}