BSV 脚本里被送到栈上的数据,其数据类型都是字节序列(数组)。对于 OP_ADD 这类算术运算操作码,是如何将字节序列转成有符号整数的呢?
简单来说,只有两点:
- 如果这个数据会被解释为整数,则使用带符号位的原码(signed magnitude)格式,编码成最短的小端字节序列
- 编码后的数据(字节序列)通过 pushdata 上栈
因为本质上还是使用 pushdata 并且这部分脚本会被执行,所以需要遵守“最小推送”(minimal push)规则。
原码
对于有符号整数的字节序列,可以用一个符号位来表示正负,用其它位来表示该数的绝对值。符号位一般是字节序列最高字节(the most significant byte)的最高位,为 0 表示正数,为 1 表示负数。
因此,1 个字节原码能表示的整数范围是 -127(1111 1111)~ -0(1000 0000)及 +0(0000 0000)~ +127(0111 1111)。
请注意,使用原码表示有符号整数会出现 -0 的情况,需要特殊处理。对于整数 0,一般只使用 0000 0000 来表示。
根据规则很容易写出实现代码:
def int_to_signed_magnitude(num: int, byteorder: Literal['big', 'little'] = 'big') -> bytes: |
变量 octets 是 num 绝对值对应的最短原码序列,如果其最高字节的最高位(符号位)被占用,就根据大小端规则在左侧或后侧追加一个字节,最后根据 num 的正负设置符号位。
脚本中的整数编码
具体规则为:
- 推送整数 0,必须使用 OP_0
- 推送其他整数,将该数按小端模式用原码格式编码成最短的字节序列(minimal encoding)后 pushdata(minimal push)
请注意,整数 0 的原码为 0x00,但栈上的整数 0 是用空的字节序列(OP_0)来表示的,这是一个特例。
整数 10 的原码为 0x0a,即推送 0x0a,“推送 0x01 ~ 0x10,必须使用操作码 OP_1 ~ OP_16”,所以推送整数 10 的脚本应为 { OP_10 },即
{ 0x5a } |
整数 -1 的原码为 0x81,即推送 0x81,“推送 0x81,必须使用操作码 OP_1NEGATE”,所以推送整数 -1 的脚本应为 { OP_1NEGATE },即
{ 0x4f } |
整数 128 的原码为 0x0080,请注意,需要使用小端模式,所以要推送的数据实际为 0x80 0x00,加上 pushdata 操作码,其对应的脚本应为
{ 0x02 0x80 0x00 } |
可以用脚本 { 0x03 0x80 0x00 0x00 } 推送整数 128 吗?不可以。因为 0x000080 不是 128 对应原码编码的最短序列(minimal encoding),节点会报错:
64: non-mandatory-script-verify-flag (Non-minimally encoded script number) |
下面是一个更直观的例子。如果你也想发送此类交易来测试,可以直接使用 bsvlib 里提供的示例代码。
交易 ea15f649f5512d3d671b216d8ecb3e0b9b276ac4e9bd633c9c53b35a5335f420 的第 0 个输出,创建了一个 UTXO,其锁定脚本为
-129 128 OP_ADD OP_EQUAL |
这个 UTXO 在交易 a930294d018d80f0878406d24c4d9320aded8bcd35b4be506eadb5249450dc0e 中作为第 0 个输入被花费,对应的解锁脚本为
-1 |
长度限制
阅读节点源码可以发现,脚本中整数的最大字节长度是通过配置项 maxscriptnumlengthpolicy 来限制的,其默认值在 v1.0.10 版本里是 10000 字节。
实现
通过调用 encode_pushdata,我们可以写出以下代码实现:
def encode_int(num: int) -> bytes: |