BSV 脚本里的整数

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:
"""
encode a signed integer with the signed-magnitude format
"""
negative: bool = num < 0
octets: bytearray = bytearray(unsigned_to_bytes(-num if negative else num, byteorder))
significant: int = 0 if byteorder == 'big' else -1 # index of the most significant byte in octets
if octets[significant] & 0x80:
if byteorder == 'big':
octets[0:0] = b'\x00' # insert at the beginning
else:
octets += b'\x00' # insert at the end
if negative:
octets[significant] |= 0x80
return octets

变量 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:
"""
encode a signed integer you want to push onto the stack in bitcoin script, following the minimal push rule
"""
if num == 0:
return OP.OP_0
negative: bool = num < 0
octets: bytearray = bytearray(unsigned_to_bytes(-num if negative else num, 'little'))
if octets[-1] & 0x80:
octets += b'\x00'
if negative:
octets[-1] |= 0x80
return encode_pushdata(octets)

参考