solana-ruby-kit 8.4.0 → 8.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +8 -0
- data/lib/solana/ruby/kit/codecs/data_structures.rb +15 -0
- data/lib/solana/ruby/kit/codecs/strings.rb +11 -2
- data/lib/solana/ruby/kit/errors.rb +5 -0
- data/lib/solana/ruby/kit/keys/signatures.rb +13 -0
- data/lib/solana/ruby/kit/keys/small_order.rb +46 -0
- data/lib/solana/ruby/kit/offchain_messages/codec.rb +3 -3
- data/lib/solana/ruby/kit/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3067fc0036448f0aef241f14f4e0b95f0faf6d6cedd4ed8aae5206d6e3169e28
|
|
4
|
+
data.tar.gz: a689c349c2e41d519b6e06814319d9760b2816bf59f48109d8d839790c4e6312
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a02f7ce5743af32d1d6401f2023fc7293e33abfe1c0f495ec91f00a7ca4399e6fd654c7baa3f3c966ed34d6495514607432315323fb3425f294e8bf1713035ae
|
|
7
|
+
data.tar.gz: bfd06cf91f1fc1652cb5a578b822595c5f976537561fddbad22e04e6dc5567f1284b211b4e41be6472426005f3f4b0197c486fe5d87a046e2dac8989e30acb61
|
data/README.md
CHANGED
|
@@ -133,6 +133,9 @@ kp = Keys.create_key_pair_from_private_key_bytes(seed_bytes)
|
|
|
133
133
|
# Sign / verify
|
|
134
134
|
sig = Keys.sign_bytes(kp.signing_key, data)
|
|
135
135
|
ok = Keys.verify_signature(kp.verify_key, sig, data)
|
|
136
|
+
# verify_signature is strict: a small-order public key or signature R
|
|
137
|
+
# component returns false (as the Solana runtime does), and passing anything
|
|
138
|
+
# other than an RbNaCl::VerifyKey raises KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY.
|
|
136
139
|
|
|
137
140
|
# Base58-encode a signature
|
|
138
141
|
Keys.encode_signature(sig) # => Signature string
|
|
@@ -477,6 +480,11 @@ may *begin* with the sentinel's bytes, or decoding stops early at that item.
|
|
|
477
480
|
Second, under `:optional` / `:omitted` the sentinel must be no wider than the
|
|
478
481
|
smallest item, or a short trailing item is silently dropped.
|
|
479
482
|
|
|
483
|
+
An item codec with a fixed size of zero (e.g. `struct_codec([])`) can never
|
|
484
|
+
reach a sentinel or the end of the bytes, so pairing one with a `SentinelSize`
|
|
485
|
+
or `:remainder` raises `CODECS__UNEXPECTED_ZERO_FIXED_SIZE_ITEM_FOR_ARRAY_LIKE_SIZE_STRATEGY`
|
|
486
|
+
when the codec is built. Explicit counts and prefixes still allow it.
|
|
487
|
+
|
|
480
488
|
#### Tap combinators
|
|
481
489
|
|
|
482
490
|
Wrap a codec in one that observes a value - or the bytes - without changing it,
|
|
@@ -132,6 +132,7 @@ module Solana::Ruby::Kit
|
|
|
132
132
|
end
|
|
133
133
|
def array_codec(element_codec, size: nil, require_size_prefix: false, description: nil)
|
|
134
134
|
description ||= 'array'
|
|
135
|
+
ArrayLikeSize.assert_item_codec_supports_size_strategy(description, size, element_codec.fixed_size)
|
|
135
136
|
case size
|
|
136
137
|
when nil then ArrayLikeSize.prefixed(element_codec, Numbers.u32_codec, require_size_prefix)
|
|
137
138
|
when Codec then ArrayLikeSize.prefixed(element_codec, size, require_size_prefix)
|
|
@@ -244,6 +245,20 @@ module Solana::Ruby::Kit
|
|
|
244
245
|
extend T::Sig
|
|
245
246
|
extend self
|
|
246
247
|
|
|
248
|
+
# Raises when a zero-byte item codec is paired with a strategy that relies
|
|
249
|
+
# on items consuming bytes to make progress: a sentinel could never be
|
|
250
|
+
# reached, nor could the end of the bytes under +:remainder+.
|
|
251
|
+
sig { params(description: String, size: T.untyped, item_fixed_size: T.nilable(Integer)).void }
|
|
252
|
+
def assert_item_codec_supports_size_strategy(description, size, item_fixed_size)
|
|
253
|
+
return unless item_fixed_size&.zero?
|
|
254
|
+
return unless size == :remainder || size.is_a?(SentinelSize)
|
|
255
|
+
|
|
256
|
+
Kernel.raise SolanaError.new(
|
|
257
|
+
SolanaError::CODECS__UNEXPECTED_ZERO_FIXED_SIZE_ITEM_FOR_ARRAY_LIKE_SIZE_STRATEGY,
|
|
258
|
+
{ codec_description: description, size_strategy: size == :remainder ? 'remainder' : 'sentinel' }
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
247
262
|
# A count prefix written and read with +prefix+.
|
|
248
263
|
sig { params(element_codec: Codec, prefix: Codec, require_size_prefix: T::Boolean).returns(Codec) }
|
|
249
264
|
def prefixed(element_codec, prefix, require_size_prefix)
|
|
@@ -191,11 +191,20 @@ module Solana::Ruby::Kit
|
|
|
191
191
|
Codec.new(enc, dec)
|
|
192
192
|
end
|
|
193
193
|
|
|
194
|
-
# Hex codec — lower-case hex string ↔ binary bytes.
|
|
194
|
+
# Hex codec — lower-case hex string ↔ binary bytes. Spaces are ignored on
|
|
195
|
+
# encode; any other non-hex character raises. An odd-length string keeps
|
|
196
|
+
# its last digit as a final low-nibble byte (+'abc'+ → +0xab 0x0c+), as
|
|
197
|
+
# upstream's base16 encoder does — +pack('H*')+ alone would pad it into
|
|
198
|
+
# the high nibble (+0xc0+).
|
|
195
199
|
sig { returns(Codec) }
|
|
196
200
|
def hex_codec
|
|
197
201
|
enc = Encoder.new do |v|
|
|
198
|
-
|
|
202
|
+
value = v.to_s.tr(' ', '')
|
|
203
|
+
unless value.match?(/\A\h*\z/)
|
|
204
|
+
Kernel.raise SolanaError.new(SolanaError::CODECS__INVALID_STRING_FOR_HEX_CODEC, { value: v.to_s })
|
|
205
|
+
end
|
|
206
|
+
value = "#{value[0...-1]}0#{value[-1]}" if value.length.odd?
|
|
207
|
+
[value].pack('H*')
|
|
199
208
|
end
|
|
200
209
|
dec = Decoder.new do |bytes, offset|
|
|
201
210
|
remaining = bytes.b.byteslice(offset..) || ''.b
|
|
@@ -36,6 +36,7 @@ module Solana::Ruby::Kit
|
|
|
36
36
|
KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY = :SOLANA_ERROR__KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY
|
|
37
37
|
KEYS__INVALID_SIGNATURE_BYTE_LENGTH = :SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH
|
|
38
38
|
KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE = :SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE
|
|
39
|
+
KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY = :SOLANA_ERROR__KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY
|
|
39
40
|
|
|
40
41
|
# ── Instructions ──────────────────────────────────────────────────────────
|
|
41
42
|
INSTRUCTIONS__EXPECTED_TO_HAVE_ACCOUNTS = :SOLANA_ERROR__INSTRUCTIONS__EXPECTED_TO_HAVE_ACCOUNTS
|
|
@@ -104,6 +105,8 @@ module Solana::Ruby::Kit
|
|
|
104
105
|
# context: { codec_description:, hex_sentinel:, sentinel: }
|
|
105
106
|
CODECS__SENTINEL_MISSING_AT_END_OF_BYTES = :SOLANA_ERROR__CODECS__SENTINEL_MISSING_AT_END_OF_BYTES
|
|
106
107
|
CODECS__SENTINEL_MUST_NOT_BE_EMPTY = :SOLANA_ERROR__CODECS__SENTINEL_MUST_NOT_BE_EMPTY
|
|
108
|
+
# context: { codec_description:, size_strategy: }
|
|
109
|
+
CODECS__UNEXPECTED_ZERO_FIXED_SIZE_ITEM_FOR_ARRAY_LIKE_SIZE_STRATEGY = :SOLANA_ERROR__CODECS__UNEXPECTED_ZERO_FIXED_SIZE_ITEM_FOR_ARRAY_LIKE_SIZE_STRATEGY
|
|
107
110
|
|
|
108
111
|
# ── RPC / JSON-RPC ────────────────────────────────────────────────────────
|
|
109
112
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER = :SOLANA_ERROR__RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER
|
|
@@ -200,6 +203,7 @@ module Solana::Ruby::Kit
|
|
|
200
203
|
KEYS__PUBLIC_KEY_MUST_MATCH_PRIVATE_KEY => 'The public key does not match the private key',
|
|
201
204
|
KEYS__INVALID_SIGNATURE_BYTE_LENGTH => 'Signature must be 64 bytes, got %{actual_length}',
|
|
202
205
|
KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE => 'Signature string length out of range (64–88 chars), got %{actual_length}',
|
|
206
|
+
KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY => 'Signatures can only be verified using a public key (an RbNaCl::VerifyKey).',
|
|
203
207
|
|
|
204
208
|
# Instructions
|
|
205
209
|
INSTRUCTIONS__EXPECTED_TO_HAVE_ACCOUNTS => 'Expected instruction to have accounts',
|
|
@@ -260,6 +264,7 @@ module Solana::Ruby::Kit
|
|
|
260
264
|
CODECS__NUMBER_OUT_OF_RANGE => 'Codec [%{codec_description}] expected number to be in the range [%{min}, %{max}], got %{value}.',
|
|
261
265
|
CODECS__SENTINEL_MISSING_AT_END_OF_BYTES => 'Codec [%{codec_description}] expected sentinel [%{hex_sentinel}] to terminate the collection, but reached the end of the byte array without it.',
|
|
262
266
|
CODECS__SENTINEL_MUST_NOT_BE_EMPTY => 'The sentinel must not be empty.',
|
|
267
|
+
CODECS__UNEXPECTED_ZERO_FIXED_SIZE_ITEM_FOR_ARRAY_LIKE_SIZE_STRATEGY => 'Codec [%{codec_description}] cannot use the [%{size_strategy}] size strategy: the item codec has a fixed size of zero and consumes no bytes.',
|
|
263
268
|
|
|
264
269
|
# RPC
|
|
265
270
|
RPC__INTEGER_OVERFLOW_WHILE_SERIALIZING_LARGE_INTEGER => 'Integer overflow while serializing large integer %{value}',
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require 'rbnacl'
|
|
5
5
|
require_relative '../errors'
|
|
6
6
|
require_relative '../encoding/base58'
|
|
7
|
+
require_relative 'small_order'
|
|
7
8
|
|
|
8
9
|
module Solana::Ruby::Kit
|
|
9
10
|
module Keys
|
|
@@ -144,11 +145,23 @@ module Solana::Ruby::Kit
|
|
|
144
145
|
# Mirrors `verifySignature(key, signature, data)` in TypeScript.
|
|
145
146
|
# Returns false rather than raising on mismatch, matching the TS return type.
|
|
146
147
|
#
|
|
148
|
+
# Verification is strict: a public key or a signature `R` component that is
|
|
149
|
+
# a point of small order is rejected without consulting RbNaCl, matching the
|
|
150
|
+
# Solana runtime whatever the linked libsodium does itself. Upstream raises
|
|
151
|
+
# for a non-extractable or non-public CryptoKey; an RbNaCl::VerifyKey always
|
|
152
|
+
# exposes its bytes, so here only a non-VerifyKey (e.g. a SigningKey) raises.
|
|
153
|
+
#
|
|
147
154
|
# @param verify_key [RbNaCl::VerifyKey]
|
|
148
155
|
# @param sig_bytes [SignatureBytes]
|
|
149
156
|
# @param data [String] binary string that was signed
|
|
150
157
|
sig { params(verify_key: T.untyped, sig_bytes: SignatureBytes, data: String).returns(T::Boolean) }
|
|
151
158
|
def verify_signature(verify_key, sig_bytes, data)
|
|
159
|
+
unless verify_key.is_a?(RbNaCl::VerifyKey)
|
|
160
|
+
Kernel.raise SolanaError.new(SolanaError::KEYS__VERIFICATION_REQUIRES_EXTRACTABLE_PUBLIC_KEY)
|
|
161
|
+
end
|
|
162
|
+
return false if SmallOrder.small_order_point_encoding?(verify_key.to_bytes)
|
|
163
|
+
return false if SmallOrder.small_order_point_encoding?(sig_bytes.value.byteslice(0, 32).to_s)
|
|
164
|
+
|
|
152
165
|
verify_key.verify(sig_bytes.value, data.b)
|
|
153
166
|
true
|
|
154
167
|
rescue RbNaCl::BadSignatureError
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Solana::Ruby::Kit
|
|
5
|
+
module Keys
|
|
6
|
+
extend T::Sig
|
|
7
|
+
|
|
8
|
+
# Mirrors the private `isSmallOrderPointEncoding` in @solana/keys
|
|
9
|
+
# (`small-order.ts`). An Ed25519 public key or signature `R` that is a point
|
|
10
|
+
# of small order lets trivially forged signatures verify against any data;
|
|
11
|
+
# the Solana runtime rejects them, so `verify_signature` does too.
|
|
12
|
+
module SmallOrder
|
|
13
|
+
extend T::Sig
|
|
14
|
+
extend self
|
|
15
|
+
|
|
16
|
+
P = T.let((1 << 255) - 19, Integer)
|
|
17
|
+
|
|
18
|
+
# The y-coordinates of the eight small-order points (the x sign bit picks
|
|
19
|
+
# between each pair, so it is masked off before the lookup). The two large
|
|
20
|
+
# ones are parsed from Strings because Sorbet rejects bignum literals.
|
|
21
|
+
SMALL_ORDER_Y_COORDINATES = T.let(
|
|
22
|
+
[
|
|
23
|
+
0,
|
|
24
|
+
1,
|
|
25
|
+
P - 1,
|
|
26
|
+
Integer('2707385501144840649318225287225658788936804267575313519463743609750303402022'),
|
|
27
|
+
Integer('55188659117513257062467267217118295137698188065244968500265048394206261417927')
|
|
28
|
+
].freeze,
|
|
29
|
+
T::Array[Integer]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# True when +bytes+ (a 32-byte binary String) encodes a point of small
|
|
33
|
+
# order, canonically or not (a y-coordinate >= p is reduced mod p first).
|
|
34
|
+
# Any other length returns false.
|
|
35
|
+
sig { params(bytes: String).returns(T::Boolean) }
|
|
36
|
+
def small_order_point_encoding?(bytes)
|
|
37
|
+
bytes = bytes.b
|
|
38
|
+
return false unless bytes.bytesize == 32
|
|
39
|
+
|
|
40
|
+
last = bytes.getbyte(31).to_i & 0x7f
|
|
41
|
+
y = (bytes.byteslice(0, 31).to_s + last.chr).reverse.unpack1('H*').to_i(16)
|
|
42
|
+
SMALL_ORDER_Y_COORDINATES.include?(y % P)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -105,9 +105,9 @@ module Solana::Ruby::Kit
|
|
|
105
105
|
# `Signature` holds a base58 string (that is what `sign_offchain_message` returns),
|
|
106
106
|
# so it has to be decoded back to the 64 raw bytes RbNaCl expects.
|
|
107
107
|
sig_bytes = Solana::Ruby::Kit::Encoding::Base58.decode(signature.to_s)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
rescue RbNaCl::
|
|
108
|
+
# Through Keys.verify_signature for its small-order key/`R` rejection.
|
|
109
|
+
Keys.verify_signature(vk, Keys.signature_bytes(sig_bytes), payload)
|
|
110
|
+
rescue RbNaCl::LengthError, SolanaError
|
|
111
111
|
false
|
|
112
112
|
end
|
|
113
113
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solana-ruby-kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 8.
|
|
4
|
+
version: 8.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paul Zupan, Idhra Inc.
|
|
@@ -246,6 +246,7 @@ files:
|
|
|
246
246
|
- lib/solana/ruby/kit/keys/private_key.rb
|
|
247
247
|
- lib/solana/ruby/kit/keys/public_key.rb
|
|
248
248
|
- lib/solana/ruby/kit/keys/signatures.rb
|
|
249
|
+
- lib/solana/ruby/kit/keys/small_order.rb
|
|
249
250
|
- lib/solana/ruby/kit/offchain_messages.rb
|
|
250
251
|
- lib/solana/ruby/kit/offchain_messages/codec.rb
|
|
251
252
|
- lib/solana/ruby/kit/offchain_messages/message.rb
|