Class: BSV::Script::ScriptNumber
- Inherits:
-
Object
- Object
- BSV::Script::ScriptNumber
- 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
-
#value ⇒ Integer
readonly
The numeric value.
Class Method Summary collapse
-
.from_bytes(bytes, max_length: MAX_BYTE_LENGTH, require_minimal: false) ⇒ Object
Decode little-endian sign-magnitude bytes into a ScriptNumber.
-
.minimally_encode(data) ⇒ Object
Strip trailing zero-padding while preserving sign.
Instance Method Summary collapse
-
#%(other) ⇒ Object
Remainder with sign of dividend (matching Bitcoin consensus).
- #*(other) ⇒ Object
-
#+(other) ⇒ Object
— Arithmetic (returns new ScriptNumber) —.
- #-(other) ⇒ Object
- #-@ ⇒ Object
-
#/(other) ⇒ Object
Truncated-toward-zero division (matching Bitcoin consensus).
- #<=>(other) ⇒ Object
- #abs ⇒ Object
-
#initialize(value) ⇒ ScriptNumber
constructor
A new instance of ScriptNumber.
-
#to_bytes ⇒ Object
Encode as little-endian sign-magnitude bytes.
- #to_i ⇒ Object
-
#to_i32 ⇒ Object
Clamp to int32 range (for opcodes that need bounded indices).
- #zero? ⇒ Boolean
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
#value ⇒ Integer (readonly)
Returns 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).
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).
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 |
#abs ⇒ Object
180 181 182 |
# File 'lib/bsv/script/interpreter/script_number.rb', line 180 def abs self.class.new(@value.abs) end |
#to_bytes ⇒ Object
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_i ⇒ Object
136 137 138 |
# File 'lib/bsv/script/interpreter/script_number.rb', line 136 def to_i @value end |
#to_i32 ⇒ Object
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
140 141 142 |
# File 'lib/bsv/script/interpreter/script_number.rb', line 140 def zero? @value.zero? end |