ic_agent 0.1.4 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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])