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

Class Method Details

.deserialize_state(state_fields, state_hex) ⇒ Hash

Decode state values from a raw hex string.

Parameters:

  • state_fields (Array<StateField>)

    field descriptors from the artifact

  • state_hex (String)

    hex-encoded state bytes (no push opcodes)

Returns:

  • (Hash)

    map of field name → decoded value



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

Parameters:

  • data_hex (String)

    hex-encoded bytes to push

Returns:

  • (String)

    hex-encoded push instruction + data



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

Parameters:

  • n (Integer)

    the integer to encode

Returns:

  • (String)

    hex-encoded push opcode + (optional) data



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.

Parameters:

  • artifact (RunarArtifact)

    compiled contract artifact

  • full_locking_script_hex (String)

    complete locking script as hex

Returns:

  • (Hash, nil)

    decoded state hash, or nil if no OP_RETURN / no state fields



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.

Parameters:

  • script_hex (String)

    full locking script as a hex string

Returns:

  • (Integer)

    hex-char offset of OP_RETURN, or -1 if not found



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.

Parameters:

  • state_fields (Array<StateField>)

    field descriptors from the artifact

  • values (Hash)

    map of field name → value

Returns:

  • (String)

    hex-encoded state bytes



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