Class: BSV::Script::ScriptNumber

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/bsv/script/interpreter/script_number.rb

Overview

Bitcoin script number: arbitrary-precision integer with sign-magnitude little-endian byte encoding.

Script numbers use a specialised encoding where the sign bit occupies the MSB of the last byte, and the magnitude is stored little-endian. This class handles encoding/decoding, minimal encoding validation, and arithmetic operations as required by the script interpreter.

Constant Summary collapse

MAX_BYTE_LENGTH =

Maximum byte length for script numbers (post-Genesis: 750 KB).

750_000
INT32_MIN =

Minimum 32-bit signed integer value.

-(2**31)
INT32_MAX =

Maximum 32-bit signed integer value.

(2**31) - 1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(value) ⇒ ScriptNumber

Returns a new instance of ScriptNumber.



29
30
31
# File 'lib/bsv/script/interpreter/script_number.rb', line 29

def initialize(value)
  @value = value
end

Instance Attribute Details

#valueInteger (readonly)

Returns the numeric value.

Returns:

  • (Integer)

    the numeric value



27
28
29
# File 'lib/bsv/script/interpreter/script_number.rb', line 27

def value
  @value
end

Class Method Details

.from_bytes(bytes, max_length: MAX_BYTE_LENGTH, require_minimal: false) ⇒ Object

Decode little-endian sign-magnitude bytes into a ScriptNumber.

Encoding: little-endian magnitude with sign bit in the MSB of the last byte.

 127 -> [0x7f]
-127 -> [0xff]
 128 -> [0x80 0x00]
-128 -> [0x80 0x80]
 256 -> [0x00 0x01]
-256 -> [0x00 0x81]


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/bsv/script/interpreter/script_number.rb', line 42

def self.from_bytes(bytes, max_length: MAX_BYTE_LENGTH, require_minimal: false)
  bytes = bytes.b if bytes.encoding != Encoding::ASCII_8BIT
  return new(0) if bytes.empty?

  if bytes.bytesize > max_length
    raise ScriptError.new ScriptErrorCode::NUMBER_TOO_BIG, "script number overflow: #{bytes.bytesize} > #{max_length}"
  end

  check_minimal_encoding!(bytes) if require_minimal

  # Accumulate little-endian magnitude
  val = 0
  bytes.each_byte.with_index do |byte, i|
    val |= byte << (8 * i)
  end

  # Extract sign from MSB of last byte
  if bytes.getbyte(bytes.bytesize - 1) & 0x80 != 0
    val ^= 0x80 << (8 * (bytes.bytesize - 1))
    val = -val
  end

  new(val)
end

.minimally_encode(data) ⇒ Object

Strip trailing zero-padding while preserving sign.



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
# File 'lib/bsv/script/interpreter/script_number.rb', line 93

def self.minimally_encode(data)
  return ''.b if data.nil? || data.empty?

  data = data.b
  last = data.getbyte(data.bytesize - 1)

  # If MSB has non-sign bits set, already minimal
  return data.dup if last.anybits?(0x7f)

  # Single byte with only sign/zero bits — encode as empty
  return ''.b if data.bytesize == 1

  # If second-to-last byte needs the sign extension, keep it
  return data.dup if data.getbyte(data.bytesize - 2) & 0x80 != 0

  # Scan backwards to find last non-zero byte
  i = data.bytesize - 2
  i -= 1 while i.positive? && data.getbyte(i).zero?

  if data.getbyte(i).zero?
    # All zeros
    ''.b
  elsif data.getbyte(i) & 0x80 != 0
    # This byte has high bit set — keep sign extension byte
    result = data.byteslice(0, i + 1)
    result << [last].pack('C')
    result
  else
    # Fold sign bit into this byte
    result = data.byteslice(0, i + 1).b
    result.setbyte(i, data.getbyte(i) | (last & 0x80))
    result
  end
end

Instance Method Details

#%(other) ⇒ Object

Remainder with sign of dividend (matching Bitcoin consensus).

Raises:



168
169
170
171
172
173
174
# File 'lib/bsv/script/interpreter/script_number.rb', line 168

def %(other)
  raise ScriptError.new(ScriptErrorCode::DIVIDE_BY_ZERO, 'modulo by zero') if other.value.zero?

  result = @value.abs % other.value.abs
  result = -result if @value.negative?
  self.class.new(result)
end

#*(other) ⇒ Object



154
155
156
# File 'lib/bsv/script/interpreter/script_number.rb', line 154

def *(other)
  self.class.new(@value * other.value)
end

#+(other) ⇒ Object

— Arithmetic (returns new ScriptNumber) —



146
147
148
# File 'lib/bsv/script/interpreter/script_number.rb', line 146

def +(other)
  self.class.new(@value + other.value)
end

#-(other) ⇒ Object



150
151
152
# File 'lib/bsv/script/interpreter/script_number.rb', line 150

def -(other)
  self.class.new(@value - other.value)
end

#-@Object



176
177
178
# File 'lib/bsv/script/interpreter/script_number.rb', line 176

def -@
  self.class.new(-@value)
end

#/(other) ⇒ Object

Truncated-toward-zero division (matching Bitcoin consensus).

Raises:



159
160
161
162
163
164
165
# File 'lib/bsv/script/interpreter/script_number.rb', line 159

def /(other)
  raise ScriptError.new(ScriptErrorCode::DIVIDE_BY_ZERO, 'division by zero') if other.value.zero?

  result = @value.abs / other.value.abs
  result = -result if @value.negative? ^ other.value.negative?
  self.class.new(result)
end

#<=>(other) ⇒ Object



184
185
186
187
188
189
190
191
# File 'lib/bsv/script/interpreter/script_number.rb', line 184

def <=>(other)
  case other
  when ScriptNumber
    @value <=> other.value
  when Integer
    @value <=> other
  end
end

#absObject



180
181
182
# File 'lib/bsv/script/interpreter/script_number.rb', line 180

def abs
  self.class.new(@value.abs)
end

#to_bytesObject

Encode as little-endian sign-magnitude bytes.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/bsv/script/interpreter/script_number.rb', line 68

def to_bytes
  return ''.b if @value.zero?

  negative = @value.negative?
  abs_val = @value.abs

  result = []
  v = abs_val
  while v.positive?
    result << (v & 0xff)
    v >>= 8
  end

  if result.last & 0x80 != 0
    # MSB conflicts with sign bit — add padding byte
    result << (negative ? 0x80 : 0x00)
  elsif negative
    # Set sign bit in MSB
    result[-1] |= 0x80
  end

  result.pack('C*')
end

#to_iObject



136
137
138
# File 'lib/bsv/script/interpreter/script_number.rb', line 136

def to_i
  @value
end

#to_i32Object

Clamp to int32 range (for opcodes that need bounded indices).



129
130
131
132
133
134
# File 'lib/bsv/script/interpreter/script_number.rb', line 129

def to_i32
  return INT32_MIN if @value < INT32_MIN
  return INT32_MAX if @value > INT32_MAX

  @value
end

#zero?Boolean

Returns:

  • (Boolean)


140
141
142
# File 'lib/bsv/script/interpreter/script_number.rb', line 140

def zero?
  @value.zero?
end