Module: Runar::SDK::State
- Included in:
- Runar::SDK
- Defined in:
- lib/runar/sdk/state.rb
Constant Summary collapse
- TYPE_WIDTHS =
Fixed byte widths for known fixed-size types.
{ 'PubKey' => 33, 'Addr' => 20, 'Ripemd160' => 20, 'Sha256' => 32, 'Point' => 64 }.freeze
Class Method Summary collapse
-
.deserialize_state(state_fields, state_hex) ⇒ Hash
Decode state values from a raw hex string.
-
.encode_push_data(data_hex) ⇒ String
Wrap hex-encoded data in a Bitcoin Script push data opcode.
-
.encode_script_int(n) ⇒ String
Encode an integer as a minimally-encoded Bitcoin Script number push.
-
.extract_state_from_script(artifact, full_locking_script_hex) ⇒ Hash?
Extract and decode state from a full locking script.
-
.find_last_op_return(script_hex) ⇒ Integer
Find the hex-char offset of the last OP_RETURN (0x6a) at a real opcode boundary.
-
.serialize_state(state_fields, values) ⇒ String
Encode a list of state field values into a raw hex string.
Class Method Details
.deserialize_state(state_fields, state_hex) ⇒ Hash
Decode state values from a raw hex string.
156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/runar/sdk/state.rb', line 156 def deserialize_state(state_fields, state_hex) sorted_fields = state_fields.sort_by(&:index) result = {} offset = 0 sorted_fields.each do |field| value, chars_read = decode_state_value(state_hex, offset, field.type) result[field.name] = value offset += chars_read end result end |
.encode_push_data(data_hex) ⇒ String
Wrap hex-encoded data in a Bitcoin Script push data opcode.
Uses minimal encoding:
- ≤75 bytes — direct push (single length byte)
- ≤255 bytes — OP_PUSHDATA1 (0x4c) + 1-byte length
- ≤65535 bytes — OP_PUSHDATA2 (0x4d) + 2-byte LE length
- otherwise — OP_PUSHDATA4 (0x4e) + 4-byte LE length
37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/runar/sdk/state.rb', line 37 def encode_push_data(data_hex) data_len = data_hex.length / 2 if data_len <= 75 format('%02x', data_len) + data_hex elsif data_len <= 0xFF '4c' + format('%02x', data_len) + data_hex elsif data_len <= 0xFFFF '4d' + [data_len].pack('v').unpack1('H*') + data_hex else '4e' + [data_len].pack('V').unpack1('H*') + data_hex end end |
.encode_script_int(n) ⇒ String
Encode an integer as a minimally-encoded Bitcoin Script number push.
Special cases:
0 → OP_0 (0x00)
1–16 → OP_1–OP_16 (0x51–0x60)
otherwise → sign-magnitude little-endian bytes with a direct push prefix
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 |
# File 'lib/runar/sdk/state.rb', line 60 def encode_script_int(n) return '00' if n.zero? if n >= 1 && n <= 16 return format('%02x', 0x50 + n) end # Sign-magnitude little-endian encoding. negative = n.negative? abs_val = n.abs bytes = [] while abs_val.positive? bytes << (abs_val & 0xFF) abs_val >>= 8 end # If the top bit of the last byte is set, append an extra byte to hold # the sign flag without ambiguity. if (bytes.last & 0x80).nonzero? bytes << (negative ? 0x80 : 0x00) elsif negative bytes[-1] |= 0x80 end data_hex = bytes.map { |b| format('%02x', b) }.join format('%02x', bytes.length) + data_hex end |
.extract_state_from_script(artifact, full_locking_script_hex) ⇒ Hash?
Extract and decode state from a full locking script.
Locates the OP_RETURN separator, then decodes everything after it as raw state bytes.
178 179 180 181 182 183 184 185 186 187 |
# File 'lib/runar/sdk/state.rb', line 178 def extract_state_from_script(artifact, full_locking_script_hex) return nil if artifact.state_fields.nil? || artifact.state_fields.empty? op_return_pos = find_last_op_return(full_locking_script_hex) return nil if op_return_pos == -1 # Skip past the OP_RETURN byte (2 hex chars) to reach raw state data. state_hex = full_locking_script_hex[op_return_pos + 2..] deserialize_state(artifact.state_fields, state_hex) end |
.find_last_op_return(script_hex) ⇒ Integer
Find the hex-char offset of the last OP_RETURN (0x6a) at a real opcode boundary.
Walks opcodes correctly so that 0x6a bytes embedded inside push data are not mistaken for OP_RETURN. In practice a Runar stateful contract has exactly one OP_RETURN; the walk stops immediately when it finds it.
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 |
# File 'lib/runar/sdk/state.rb', line 98 def find_last_op_return(script_hex) last_pos = -1 offset = 0 length = script_hex.length while offset + 2 <= length opcode = script_hex[offset, 2].to_i(16) if opcode == 0x6A # OP_RETURN at a real opcode boundary. Everything after is raw state # data, so stop walking immediately. return offset elsif opcode >= 0x01 && opcode <= 0x4B offset += 2 + opcode * 2 elsif opcode == 0x4C break if offset + 4 > length push_len = script_hex[offset + 2, 2].to_i(16) offset += 4 + push_len * 2 elsif opcode == 0x4D break if offset + 6 > length lo = script_hex[offset + 2, 2].to_i(16) hi = script_hex[offset + 4, 2].to_i(16) push_len = lo | (hi << 8) offset += 6 + push_len * 2 elsif opcode == 0x4E break if offset + 10 > length push_len = [script_hex[offset + 2, 8]].pack('H*').unpack1('V') offset += 10 + push_len * 2 else offset += 2 end end last_pos end |
.serialize_state(state_fields, values) ⇒ String
Encode a list of state field values into a raw hex string.
Fields are sorted by their index before encoding so the order always matches what the compiler emits. No push opcodes are added; the result is raw bytes suitable for appending after OP_RETURN.
146 147 148 149 |
# File 'lib/runar/sdk/state.rb', line 146 def serialize_state(state_fields, values) sorted_fields = state_fields.sort_by(&:index) sorted_fields.map { |field| encode_state_value(values[field.name], field.type) }.join end |