ic_agent 0.1.4 → 0.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 872963d331d311c6a940f64445ef0583397885eb9455464f4d865d325f24ee97
4
- data.tar.gz: 8b11f6cc71864468b52e66a6577fb3002bcf14cfde2ce93b1dfb6f819eb1df84
3
+ metadata.gz: b1bb3c395959ef0b33094ab3e34a6087b061a76287ce34b312286703c568fade
4
+ data.tar.gz: f71667c7d06d470c89afd7d9e8734bfded8abc2f7d85a89a77d84fa3ce7e210c
5
5
  SHA512:
6
- metadata.gz: 5ead16aeb03ac87caa4620ee9f813e7d79399f95d9cb2424cbedd2af828f9dd06f14a1f9d200ac596783ea3b47fdd88f500256608e972011e1dae01e69452cf4
7
- data.tar.gz: d6ebbda49c03f1e2df8fe3e28ffd2b53424a83e04b386841f8874c6b888f4089d8c0150194930ffdab836e81961cfc657168a6e225e0ddd32c665d03159e1cc3
6
+ metadata.gz: 437bab4be804cc57aa1098aa91275d88623e79c98f9190129956eb2ceb0447dd5dbd305ec7425b3a3fd7fc945035a79329b6a73052c876bc31118a94387cb61a
7
+ data.tar.gz: 05adc3829656980d0a833ac495cbfbf67e84fee5474dda75e39d6f94fb538ee22120de034ba7b81c36680ef4444a586409ecb2c6f0f43d53f9cd7aa28b51e394
data/Gemfile CHANGED
@@ -7,6 +7,7 @@ gemspec
7
7
 
8
8
  gem 'base32', '~> 0.3.4'
9
9
  gem 'bitcoin-ruby', '~> 0.0.20'
10
+ gem 'bls12-381', '~> 0.3.0'
10
11
  gem 'byebug', '~> 11.1', '>= 11.1.3'
11
12
  gem 'cbor', '~> 0.5.9.6'
12
13
  gem 'ctf-party', '~> 2.3'
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ic_agent (0.1.4)
4
+ ic_agent (0.2.1)
5
5
  base32 (~> 0.3.4)
6
6
  bitcoin-ruby (~> 0.0.20)
7
+ bls12-381 (~> 0.3.0)
7
8
  cbor (~> 0.5.9.6)
8
9
  ctf-party (~> 2.3)
9
10
  ecdsa (~> 1.2)
@@ -23,6 +24,8 @@ GEM
23
24
  eventmachine
24
25
  ffi
25
26
  scrypt
27
+ bls12-381 (0.3.0)
28
+ h2c (~> 0.2.0)
26
29
  byebug (11.1.3)
27
30
  cbor (0.5.9.6)
28
31
  coderay (1.1.3)
@@ -34,7 +37,7 @@ GEM
34
37
  ecdsa (1.2.0)
35
38
  ed25519 (1.3.0)
36
39
  eventmachine (1.2.7)
37
- faraday (2.7.4)
40
+ faraday (2.7.10)
38
41
  faraday-net_http (>= 2.0, < 3.1)
39
42
  ruby2_keywords (>= 0.0.4)
40
43
  faraday-net_http (3.0.2)
@@ -42,13 +45,15 @@ GEM
42
45
  ffi-compiler (1.0.1)
43
46
  ffi (>= 1.0.0)
44
47
  rake
45
- i18n (1.12.0)
48
+ h2c (0.2.0)
49
+ ecdsa (~> 1.2.0)
50
+ i18n (1.14.1)
46
51
  concurrent-ruby (~> 1.0)
47
52
  json (2.6.3)
48
53
  leb128 (1.0.0)
49
54
  method_source (1.0.0)
50
- mini_portile2 (2.8.2)
51
- pkg-config (1.5.1)
55
+ mini_portile2 (2.8.4)
56
+ pkg-config (1.5.2)
52
57
  polyglot (0.3.5)
53
58
  pry (0.14.2)
54
59
  coderay (~> 1.1)
@@ -62,19 +67,19 @@ GEM
62
67
  rspec-core (~> 3.12.0)
63
68
  rspec-expectations (~> 3.12.0)
64
69
  rspec-mocks (~> 3.12.0)
65
- rspec-core (3.12.1)
70
+ rspec-core (3.12.2)
66
71
  rspec-support (~> 3.12.0)
67
- rspec-expectations (3.12.2)
72
+ rspec-expectations (3.12.3)
68
73
  diff-lcs (>= 1.2.0, < 2.0)
69
74
  rspec-support (~> 3.12.0)
70
- rspec-mocks (3.12.5)
75
+ rspec-mocks (3.12.6)
71
76
  diff-lcs (>= 1.2.0, < 2.0)
72
77
  rspec-support (~> 3.12.0)
73
- rspec-support (3.12.0)
78
+ rspec-support (3.12.1)
74
79
  ruby-enum (0.9.0)
75
80
  i18n
76
81
  ruby2_keywords (0.0.5)
77
- rubytree (2.0.0)
82
+ rubytree (2.0.2)
78
83
  json (~> 2.0, > 2.3.1)
79
84
  rubyzip (2.3.2)
80
85
  scrypt (3.0.7)
@@ -88,6 +93,7 @@ PLATFORMS
88
93
  DEPENDENCIES
89
94
  base32 (~> 0.3.4)
90
95
  bitcoin-ruby (~> 0.0.20)
96
+ bls12-381 (~> 0.3.0)
91
97
  byebug (~> 11.1, >= 11.1.3)
92
98
  cbor (~> 0.5.9.6)
93
99
  ctf-party (~> 2.3)
data/README.md CHANGED
@@ -4,6 +4,8 @@
4
4
 
5
5
  `ic_agent` provides basic modules to interact with canisters on the DFINITY Internet Computer.
6
6
 
7
+ [![Gem Version](https://badge.fury.io/rb/ic_agent.svg)](https://badge.fury.io/rb/ic_agent)[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE)
8
+
7
9
 
8
10
  ## Installation
9
11
 
data/ic_agent.gemspec CHANGED
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.metadata['homepage_uri'] = spec.homepage
18
18
  spec.metadata['source_code_uri'] = spec.homepage
19
19
  spec.metadata['changelog_uri'] = spec.homepage
20
+ spec.metadata['documentation_uri'] = 'https://tuminfei.github.io/ic_agent.github.com/'
20
21
 
21
22
  # Specify which files should be added to the gem when it is released.
22
23
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -33,6 +34,7 @@ Gem::Specification.new do |spec|
33
34
  # spec.add_dependency "example-gem", "~> 1.0"
34
35
  spec.add_dependency 'base32', '~> 0.3.4'
35
36
  spec.add_dependency 'bitcoin-ruby', '~> 0.0.20'
37
+ spec.add_dependency 'bls12-381', '~> 0.3.0'
36
38
  spec.add_dependency 'cbor', '~> 0.5.9.6'
37
39
  spec.add_dependency 'ctf-party', '~> 2.3'
38
40
  spec.add_dependency 'ecdsa', '~> 1.2'
@@ -1,8 +1,15 @@
1
1
  require 'cbor'
2
+ require 'bls'
2
3
  require 'ctf_party'
4
+ require 'bitcoin'
3
5
 
4
6
  module IcAgent
5
7
  class Request
8
+ # Signs a request with an identity's signature and encodes it using CBOR.
9
+ #
10
+ # @param req [Hash] The request to be signed.
11
+ # @param iden [Identity] The identity used for signing.
12
+ # @return [Array] The request ID and the encoded signed request.
6
13
  def self.sign_request(req, iden)
7
14
  req_id = IcAgent::Utils.to_request_id(req)
8
15
  msg = IcAgent::IC_REQUEST_DOMAIN_SEPARATOR + req_id
@@ -26,6 +33,13 @@ module IcAgent
26
33
  class Agent
27
34
  attr_accessor :identity, :client, :ingress_expiry, :root_key, :nonce_factory
28
35
 
36
+ # Initializes a new IC agent.
37
+ #
38
+ # @param identity [Identity] The identity associated with the agent.
39
+ # @param client [Client] The client used for communication with the IC network.
40
+ # @param nonce_factory [NonceFactory] The factory for generating nonces.
41
+ # @param ingress_expiry [Integer] The expiration time for ingress requests.
42
+ # @param root_key [String] The IC root key used for verification.
29
43
  def initialize(identity, client, nonce_factory = nil, ingress_expiry = 300, root_key = IcAgent::IC_ROOT_KEY)
30
44
  @identity = identity
31
45
  @client = client
@@ -34,14 +48,25 @@ module IcAgent
34
48
  @nonce_factory = nonce_factory
35
49
  end
36
50
 
51
+ # Retrieves the principal associated with the agent's identity.
52
+ #
53
+ # @return [Principal] The principal associated with the agent.
37
54
  def get_principal
38
55
  @identity.sender
39
56
  end
40
57
 
58
+ # Calculates the expiration date for ingress requests.
59
+ #
60
+ # @return [Integer] The expiration date in nanoseconds.
41
61
  def get_expiry_date
42
62
  ((Time.now.to_i + @ingress_expiry) * 10**9).to_i
43
63
  end
44
64
 
65
+ # Sends a query request to a canister and decodes the response using CBOR.
66
+ #
67
+ # @param canister_id [String] The ID of the target canister.
68
+ # @param data [Hash] The data to be sent in the query request.
69
+ # @return [Object] The decoded response from the canister.
45
70
  def query_endpoint(canister_id, data)
46
71
  ret = @client.query(canister_id, data)
47
72
  decode_ret = nil
@@ -54,16 +79,34 @@ module IcAgent
54
79
  decode_ret
55
80
  end
56
81
 
82
+ # Calls a method on a canister and returns the request ID.
83
+ #
84
+ # @param canister_id [String] The ID of the target canister.
85
+ # @param request_id [String] The ID of the request.
86
+ # @param data [Hash] The data to be sent in the call request.
87
+ # @return [String] The request ID.
57
88
  def call_endpoint(canister_id, request_id, data)
58
89
  @client.call(canister_id, request_id, data)
59
90
  request_id
60
91
  end
61
92
 
93
+ # Reads the state of a canister.
94
+ #
95
+ # @param canister_id [String] The ID of the target canister.
96
+ # @param data [Hash] The data to be sent in the read state request.
97
+ # @return [Object] The response from the canister.
62
98
  def read_state_endpoint(canister_id, data)
63
- result = @client.read_state(canister_id, data)
64
- result
99
+ @client.read_state(canister_id, data)
65
100
  end
66
101
 
102
+ # Sends a raw query request to a canister and handles the response.
103
+ #
104
+ # @param canister_id [String] The ID of the target canister.
105
+ # @param method_name [String] The name of the method to be called.
106
+ # @param arg [String] The argument to be passed to the method.
107
+ # @param return_type [Object] The expected type of the return value.
108
+ # @param effective_canister_id [String] The effective canister ID (optional).
109
+ # @return [Object] The decoded response from the canister.
67
110
  def query_raw(canister_id, method_name, arg, return_type = nil, effective_canister_id = nil)
68
111
  req_canister_id = canister_id.is_a?(String) ? Principal.from_str(canister_id).bytes : canister_id.bytes
69
112
  req = {
@@ -92,6 +135,15 @@ module IcAgent
92
135
  end
93
136
  end
94
137
 
138
+ # Sends a raw update request to a canister and handles the response.
139
+ #
140
+ # @param canister_id [String] The ID of the target canister.
141
+ # @param method_name [String] The name of the method to be called.
142
+ # @param arg [String] The argument to be passed to the method.
143
+ # @param return_type [Object] The expected type of the return value.
144
+ # @param effective_canister_id [String] The effective canister ID (optional).
145
+ # @param kwargs [Hash] Additional keyword arguments.
146
+ # @return [Object] The decoded response from the canister.
95
147
  def update_raw(canister_id, method_name, arg, return_type = nil, effective_canister_id = nil, **kwargs)
96
148
  req_canister_id = canister_id.is_a?(String) ? Principal.from_str(canister_id).bytes : canister_id.bytes
97
149
  req = {
@@ -120,7 +172,13 @@ module IcAgent
120
172
  end
121
173
  end
122
174
 
123
- def read_state_raw(canister_id, paths)
175
+ # Sends a raw read state request to a canister and handles the response.
176
+ #
177
+ # @param canister_id [String] The ID of the target canister.
178
+ # @param paths [Array] The paths to read from the canister's state.
179
+ # @param [TrueClass] bls_verify
180
+ # @return [Object] The decoded response from the canister.
181
+ def read_state_raw(canister_id, paths, bls_verify = true)
124
182
  req = {
125
183
  'request_type' => 'read_state',
126
184
  'sender' => @identity.sender.bytes,
@@ -140,9 +198,20 @@ module IcAgent
140
198
  rescue StandardError
141
199
  raise ValueError, "Unable to decode cbor value: #{ret}"
142
200
  end
143
- CBOR.decode(d.value['certificate'])
201
+ cert = CBOR.decode(d.value['certificate'])
202
+
203
+ if bls_verify
204
+ verify(cert, canister_id) ? cert : false
205
+ else
206
+ cert
207
+ end
144
208
  end
145
209
 
210
+ # Retrieves the status and certificate of a request from a canister.
211
+ #
212
+ # @param canister_id [String] The ID of the target canister.
213
+ # @param req_id [String] The ID of the request.
214
+ # @return [Array] The status and certificate of the request.
146
215
  def request_status_raw(canister_id, req_id)
147
216
  paths = [['request_status', req_id]]
148
217
  cert = read_state_raw(canister_id, paths)
@@ -150,6 +219,12 @@ module IcAgent
150
219
  [status, cert]
151
220
  end
152
221
 
222
+ # Polls a canister for the status of a request.
223
+ #
224
+ # @param canister_id [String] The ID of the target canister.
225
+ # @param req_id [String] The ID of the request.
226
+ # @param delay [Integer] The delay between each poll attempt (in seconds).
227
+ # @param timeout [Integer] The maximum timeout for polling.
153
228
  def poll(canister_id, req_id, delay = 1, timeout = IcAgent::DEFAULT_POLL_TIMEOUT_SECS)
154
229
  status = nil
155
230
  cert = nil
@@ -172,5 +247,80 @@ module IcAgent
172
247
  [status, _]
173
248
  end
174
249
  end
250
+
251
+ # Verify a BLS signature
252
+ # The signature must be exactly 48 bytes (compressed G1 element)
253
+ # The key must be exactly 96 bytes (compressed G2 element)
254
+ def verify(cert, canister_id)
255
+ signature_hex = IcAgent::Certificate.signature(cert).str2hex
256
+ tree = IcAgent::Certificate.tree(cert)
257
+ delegation = IcAgent::Certificate.delegation(cert)
258
+ root_hash = IcAgent::Certificate.reconstruct(tree).str2hex
259
+ msg = IcAgent::IC_STATE_ROOT_DOMAIN_SEPARATOR + root_hash
260
+ der_key = check_delegation(delegation, canister_id, true)
261
+ public_key_hash = extract_der(der_key).str2hex
262
+
263
+ public_key = BLS::PointG2.from_hex(public_key_hash)
264
+ signature = BLS::PointG1.from_hex(signature_hex)
265
+ BLS.verify(signature, msg, public_key)
266
+ end
267
+
268
+ # Check the delegation and return the corresponding root key.
269
+ def check_delegation(delegation, effective_canister_id, disable_range_check)
270
+ return @root_key unless delegation
271
+
272
+ begin
273
+ cert = CBOR.decode(delegation['certificate'])
274
+ rescue CBOR::MalformedFormatError => e
275
+ raise TypeError, "certificate CBOR::MalformedFormatError: #{delegation['certificate']}"
276
+ end
277
+
278
+ path = ['subnet', delegation['subnet_id'], 'canister_ranges']
279
+ canister_range = IcAgent::Certificate.lookup(path, cert)
280
+
281
+ begin
282
+ ranges = []
283
+ ranges_json = CBOR.decode(canister_range).values[1]
284
+
285
+ ranges_json.each do |range_json|
286
+ range = {}
287
+ range['low'] = Principal.from_hex(range_json[0])
288
+ range['high'] = Principal.from_hex(range_json[1])
289
+ ranges << range
290
+ end
291
+
292
+ if !disable_range_check && !principal_is_within_ranges(effective_canister_id, ranges)
293
+ raise AgentError 'certificate CERTIFICATE_NOT_AUTHORIZED'
294
+ end
295
+ rescue Exception => e
296
+ raise AgentError "certificate INVALID_CBOR_DATA, canister_range: #{canister_range.to_s}"
297
+ end
298
+
299
+ path = ['subnet', delegation['subnet_id'], 'public_key']
300
+ IcAgent::Certificate.lookup(path, cert)
301
+ end
302
+
303
+ def principal_is_within_ranges(principal, ranges)
304
+ ranges.each do |range|
305
+ return true if range['low'].lt_eq(principal) && range['high'].gt_eq(principal)
306
+ end
307
+ false
308
+ end
309
+
310
+ # Extract the BLS public key from the DER buffer.
311
+ def extract_der(der_buf)
312
+ bls_der_prefix = OpenSSL::BN.from_hex(IcAgent::BLS_DER_PREFIX).to_s(2)
313
+ expected_length = bls_der_prefix.bytesize + IcAgent::BLS_KEY_LENGTH
314
+ if der_buf.bytesize != expected_length
315
+ raise TypeError, "BLS DER-encoded public key must be #{expected_length} bytes long"
316
+ end
317
+
318
+ prefix = der_buf.byteslice(0, bls_der_prefix.bytesize)
319
+ if prefix != bls_der_prefix
320
+ raise TypeError, "BLS DER-encoded public key is invalid. Expect the following prefix: #{bls_der_prefix}, but get #{prefix}"
321
+ end
322
+
323
+ der_buf.byteslice(bls_der_prefix.bytesize..-1)
324
+ end
175
325
  end
176
326
  end
@@ -5,24 +5,30 @@ module IcAgent
5
5
  class Assembler
6
6
  TYPE_MAPPING = {}
7
7
 
8
+ # Builds a single Candid type from a given child type.
8
9
  def self.build_single_type(child_type)
9
10
  IcAgent::Candid::BaseTypes.send(child_type)
10
11
  end
11
12
 
13
+
14
+ # Builds a Candid blob type.
12
15
  def self.build_blob
13
16
  IcAgent::Candid::BaseTypes.vec(IcAgent::Candid::BaseTypes.nat8)
14
17
  end
15
18
 
19
+ # Builds a Candid optional type from a given child type.
16
20
  def self.build_opt(child_type, key_types = {})
17
21
  child_type = key_types[child_type].nil? ? build_type(child_type, key_types) : key_types[child_type]
18
22
  IcAgent::Candid::BaseTypes.opt(child_type)
19
23
  end
20
24
 
25
+ # Builds a Candid vector type from a given child type.
21
26
  def self.build_vec(child_type, key_types = {})
22
27
  child_type = key_types[child_type].nil? ? build_type(child_type, key_types) : key_types[child_type]
23
28
  IcAgent::Candid::BaseTypes.vec(child_type)
24
29
  end
25
30
 
31
+ # Builds a Candid record type from a given hash of field names and types.
26
32
  def self.build_record(child_hash, multi_types = {}, key_types = {})
27
33
  child_types = {}
28
34
  child_hash.each_key do |key|
@@ -38,6 +44,7 @@ module IcAgent
38
44
  IcAgent::Candid::BaseTypes.record(child_types)
39
45
  end
40
46
 
47
+ # Builds a Candid variant type from a given hash of field names and types.
41
48
  def self.build_variant(child_hash, multi_types = {}, key_types = {})
42
49
  child_types = {}
43
50
  child_hash.each_key do |key|
@@ -53,6 +60,7 @@ module IcAgent
53
60
  IcAgent::Candid::BaseTypes.variant(child_types)
54
61
  end
55
62
 
63
+ # Builds a Candid type based on the given type string.
56
64
  def self.build_type(type_str, key_types = {}, multi_types = {})
57
65
  opt_code = get_opt_code(type_str)
58
66
 
@@ -91,6 +99,7 @@ module IcAgent
91
99
  end
92
100
  end
93
101
 
102
+ # Replaces the last occurrence of a pattern in a string with the given replacement.
94
103
  def self.replace_last_occurrence(string, pattern, replacement)
95
104
  last_index = string.rindex(pattern)
96
105
  return string unless last_index
@@ -99,18 +108,21 @@ module IcAgent
99
108
  string
100
109
  end
101
110
 
111
+ # Extracts the content of a Candid record type from the type string.
102
112
  def self.get_record_content(record_str)
103
113
  record_str = record_str.sub('record', '').sub('{', '')
104
114
  record_str = replace_last_occurrence(record_str, '}', '')
105
115
  record_str.strip
106
116
  end
107
117
 
118
+ # Extracts the content of a Candid variant type from the type string.
108
119
  def self.get_variant_content(variant_str)
109
120
  variant_str = variant_str.sub('variant', '').sub('{', '')
110
121
  variant_str = replace_last_occurrence(variant_str, '}', '')
111
122
  variant_str.strip
112
123
  end
113
124
 
125
+ # Extracts the key-value pairs from a Candid record item string.
114
126
  def self.get_record_key_value(item_str, index_str, key_index = 0)
115
127
  first_index = item_str.index(index_str)
116
128
  if first_index
@@ -123,16 +135,19 @@ module IcAgent
123
135
  return key, value
124
136
  end
125
137
 
138
+ # Extracts the Candid code (e.g., "record", "variant", "opt", etc.) from the type string.
126
139
  def self.get_opt_code(item_str)
127
140
  opt_code = item_str.strip
128
141
  opt_code.split(' ')[0]
129
142
  end
130
143
 
144
+ # Extracts the child Candid code from the type string.
131
145
  def self.get_child_code(item_str, index_str)
132
146
  first_index = item_str.index(index_str)
133
147
  item_str[(first_index + index_str.size)..].strip
134
148
  end
135
149
 
150
+ # Replaces occurrences of Candid record and variant types with unique type names.
136
151
  def self.replace_multi_type(type_str)
137
152
  replaced_hash = {}
138
153
  modified_str = type_str.gsub(/record\s*{[^{}]*}/) do |match|
@@ -152,6 +167,7 @@ module IcAgent
152
167
  return modified_str, replaced_hash
153
168
  end
154
169
 
170
+ # Gets the refer types used in the type string.
155
171
  def self.get_params_refer_values(type_str)
156
172
  parser = IcAgent::Ast::StatementParser.new
157
173
  parser.parse(type_str)
@@ -159,6 +175,7 @@ module IcAgent
159
175
  refer_type
160
176
  end
161
177
 
178
+ # Recovers the original type string from the multi_types hash.
162
179
  def self.recover_type(type_str, multi_types)
163
180
  multi_types.each_key do |key|
164
181
  type_str = type_str.gsub(key, multi_types[key])