Module: Solana::Borsh

Defined in:
lib/solana/borsh.rb

Defined Under Namespace

Classes: DecodedFieldTooLarge

Constant Summary collapse

MAX_DECODED_FIELD_BYTES =

Cap on the bytes any single length-prefixed decode (vec, string) can claim. Malicious / corrupt RPC responses can carry a length field of e.g. 4_000_000_000, which without this guard would OOM the process when the caller allocates accordingly. 10MB is more than any sane Solana account / instruction payload — adjust per consumer needs.

10 * 1024 * 1024

Class Method Summary collapse

Class Method Details

.check_field_length!(length, kind) ⇒ Object



107
108
109
110
111
112
113
114
# File 'lib/solana/borsh.rb', line 107

def check_field_length!(length, kind)
  if length > MAX_DECODED_FIELD_BYTES
    raise DecodedFieldTooLarge,
          "Borsh #{kind} declared length #{length} exceeds cap " \
          "(MAX_DECODED_FIELD_BYTES=#{MAX_DECODED_FIELD_BYTES}). " \
          "Likely a corrupt or malicious RPC response."
  end
end

.decode_pubkey(bytes, offset = 0) ⇒ Object



79
80
81
# File 'lib/solana/borsh.rb', line 79

def decode_pubkey(bytes, offset = 0)
  [bytes.byteslice(offset, 32), offset + 32]
end

.decode_string(bytes, offset = 0) ⇒ Object

Length-prefixed string. Reads u32 length then ‘length` bytes of UTF-8. Raises DecodedFieldTooLarge if the declared length exceeds the cap —protects callers from allocation-bomb DoS via crafted RPC responses.



86
87
88
89
90
91
# File 'lib/solana/borsh.rb', line 86

def decode_string(bytes, offset = 0)
  length, offset = decode_u32(bytes, offset)
  check_field_length!(length, "string")
  str = bytes.byteslice(offset, length).to_s.force_encoding("UTF-8")
  [str, offset + length]
end

.decode_u16(bytes, offset = 0) ⇒ Object



67
68
69
# File 'lib/solana/borsh.rb', line 67

def decode_u16(bytes, offset = 0)
  [bytes.byteslice(offset, 2).unpack1("v"), offset + 2]
end

.decode_u32(bytes, offset = 0) ⇒ Object



71
72
73
# File 'lib/solana/borsh.rb', line 71

def decode_u32(bytes, offset = 0)
  [bytes.byteslice(offset, 4).unpack1("V"), offset + 4]
end

.decode_u64(bytes, offset = 0) ⇒ Object



75
76
77
# File 'lib/solana/borsh.rb', line 75

def decode_u64(bytes, offset = 0)
  [bytes.byteslice(offset, 8).unpack1("Q<"), offset + 8]
end

.decode_u8(bytes, offset = 0) ⇒ Object

Decode helpers



63
64
65
# File 'lib/solana/borsh.rb', line 63

def decode_u8(bytes, offset = 0)
  [bytes.byteslice(offset, 1).unpack1("C"), offset + 1]
end

.decode_vec(bytes, offset = 0, &block) ⇒ Object

Length-prefixed array. block is called per element with (bytes, offset) and must return [value, new_offset]. Bounded by MAX_DECODED_FIELD_BYTES on the declared count to prevent allocation-bomb DoS.



96
97
98
99
100
101
102
103
104
105
# File 'lib/solana/borsh.rb', line 96

def decode_vec(bytes, offset = 0, &block)
  length, offset = decode_u32(bytes, offset)
  check_field_length!(length, "vec")
  items = []
  length.times do
    item, offset = block.call(bytes, offset)
    items << item
  end
  [items, offset]
end

.encode_bool(value) ⇒ Object



57
58
59
# File 'lib/solana/borsh.rb', line 57

def encode_bool(value)
  encode_u8(value ? 1 : 0)
end

.encode_bytes32(bytes) ⇒ Object



41
42
43
44
45
# File 'lib/solana/borsh.rb', line 41

def encode_bytes32(bytes)
  bytes = bytes.b if bytes.is_a?(String)
  raise "Expected 32 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 32
  bytes
end

.encode_i64(value) ⇒ Object



30
31
32
# File 'lib/solana/borsh.rb', line 30

def encode_i64(value)
  [value].pack("q<") # little-endian i64
end

.encode_pubkey(pubkey_bytes) ⇒ Object



34
35
36
37
38
39
# File 'lib/solana/borsh.rb', line 34

def encode_pubkey(pubkey_bytes)
  pubkey_bytes = Keypair.decode_base58(pubkey_bytes) if pubkey_bytes.is_a?(String) && pubkey_bytes.length != 32
  pubkey_bytes = pubkey_bytes.b if pubkey_bytes.is_a?(String)
  raise "Pubkey must be 32 bytes, got #{pubkey_bytes.bytesize}" unless pubkey_bytes.bytesize == 32
  pubkey_bytes
end

.encode_string(str) ⇒ Object



52
53
54
55
# File 'lib/solana/borsh.rb', line 52

def encode_string(str)
  bytes = str.encode("UTF-8").b
  encode_u32(bytes.bytesize) + bytes
end

.encode_u16(value) ⇒ Object



18
19
20
# File 'lib/solana/borsh.rb', line 18

def encode_u16(value)
  [value].pack("v") # little-endian u16
end

.encode_u32(value) ⇒ Object



22
23
24
# File 'lib/solana/borsh.rb', line 22

def encode_u32(value)
  [value].pack("V") # little-endian u32
end

.encode_u64(value) ⇒ Object



26
27
28
# File 'lib/solana/borsh.rb', line 26

def encode_u64(value)
  [value].pack("Q<") # little-endian u64
end

.encode_u8(value) ⇒ Object



14
15
16
# File 'lib/solana/borsh.rb', line 14

def encode_u8(value)
  [value].pack("C")
end

.encode_vec(items, &block) ⇒ Object



47
48
49
50
# File 'lib/solana/borsh.rb', line 47

def encode_vec(items, &block)
  encoded_items = items.map { |item| block.call(item) }.join
  encode_u32(items.length) + encoded_items
end