sidetree 0.1.1 → 0.1.2

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: 5a4b7ce3f8ab94c646d21e2083664437b75877ee43264b172fdcadd8f8c9fffc
4
- data.tar.gz: 3ed1dee9aaba4e13f3122d57f9b875c5279c2148a3cbf1e6818d2073e9d7759e
3
+ metadata.gz: 0c2645dbb06ae761fe9e966134e7b824a2d3832f9972cd0808f92d46b3f6056e
4
+ data.tar.gz: bce1dce70b331c7b7093ceffa8c1af4e6953914f12aae2543cf6bcc9839ba97e
5
5
  SHA512:
6
- metadata.gz: 4d87cd2fae36188a3e24d37f73f99637a6a55ef8d2e300261f46595811194d2cbccc2ea9b6da3ad77564f3312ce2ee51561814a9cf02c261031b35438b2df0d5
7
- data.tar.gz: f9fbb814e7536a672f3f12b2c83fbf9af4f5072a0ee210e9935ce8b627972d659d28d36df9f7425e5b3c44edd7589a8fec7b68cb1b1d34c2af711c3eedd02316
6
+ metadata.gz: ff5f5050ec0097e8a6f9a46d7f56c1bf43c08844d0b74a438d9daf07e508b67868537b8ab788658894a9b086673e697cab9aa9d8872c0437a61c169081986719
7
+ data.tar.gz: 7252b89b1c99ee2a5cc4bcb15a6f10cf08c556881665b0d5132a2aafbb414d9a51e0aafc8180b4dd3dc2e96efb40f01d9334ae5dcc493043a318002bc4b3e699
@@ -0,0 +1,20 @@
1
+ module Sidetree
2
+ module CAS
3
+ class FetchResult
4
+ CODE_CAS_NOT_REACHABLE = "cas_not_reachable"
5
+ CODE_INVALID_HASH = "content_hash_invalid"
6
+ CODE_MAX_SIZE_EXCEEDED = "content_exceeds_maximum_allowed_size"
7
+ CODE_NOT_FILE = "content_not_a_file"
8
+ CODE_NOT_FOUND = "content_not_found"
9
+ CODE_SUCCESS = "success"
10
+
11
+ attr_reader :code
12
+ attr_reader :content
13
+
14
+ def initialize(code, content = nil)
15
+ @code = code
16
+ @content = content
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,101 @@
1
+ require "securerandom"
2
+ require "uri"
3
+ require "net/http"
4
+
5
+ module Sidetree
6
+ module CAS
7
+ class IPFS
8
+ attr_reader :base_url
9
+ attr_reader :fetch_timeout
10
+
11
+ # @raise [Sidetree::Error]
12
+ def initialize(
13
+ schema: "http",
14
+ host: "localhost",
15
+ port: 5001,
16
+ base_path: "/api/v0",
17
+ fetch_timeout: nil
18
+ )
19
+ @base_url = "#{schema}://#{host}:#{port}#{base_path}"
20
+ @fetch_timeout = fetch_timeout
21
+ raise Sidetree::Error, "Failed to connect to IPFS endpoint." unless up?
22
+ end
23
+
24
+ # Writes the given content to CAS.
25
+ # @param [String] content content to be stored.
26
+ # @return [String] SHA256 hash in base64url encoding which represents the address of the content.
27
+ # @raise [Sidetree::Error] If IPFS write fails
28
+ def write(content)
29
+ multipart_boundary = SecureRandom.hex(32)
30
+ uri = URI("#{base_url}/add")
31
+ http = Net::HTTP.new(uri.host, uri.port)
32
+ http.use_ssl = uri.scheme == "https"
33
+ req = Net::HTTP::Post.new(uri)
34
+ req[
35
+ "Content-Type"
36
+ ] = "multipart/form-data; boundary=#{multipart_boundary}"
37
+ req["Content-Disposition"] = 'form-data; name=; filename=""'
38
+ req.body = build_body(multipart_boundary, content)
39
+ res = http.request(req)
40
+ if res.is_a?(Net::HTTPSuccess)
41
+ results = JSON.parse(res.body)
42
+ results["Hash"]
43
+ else
44
+ raise Sidetree::Error, "Failed writing content. #{res.body}"
45
+ end
46
+ end
47
+
48
+ # Get content from IPFS.
49
+ # @param [String] addr cas uri.
50
+ # @return [Sidetree::CAS::FetchResult] Fetch result containing the content if found.
51
+ # The result code is set to FetchResultCode.MaxSizeExceeded if the content exceeds the +max_bytesize+.
52
+ def read(addr)
53
+ fetch_url = "#{base_url}/cat?arg=#{addr}"
54
+ begin
55
+ res = Net::HTTP.post_form(URI(fetch_url), {})
56
+ if res.is_a?(Net::HTTPSuccess)
57
+ FetchResult.new(FetchResult::CODE_SUCCESS, res.body)
58
+ else
59
+ FetchResult.new(FetchResult::CODE_NOT_FOUND)
60
+ end
61
+ rescue Errno::ECONNREFUSED
62
+ FetchResult.new(FetchResult::CODE_CAS_NOT_REACHABLE)
63
+ rescue StandardError
64
+ FetchResult.new(FetchResult::CODE_NOT_FOUND)
65
+ end
66
+ end
67
+
68
+ # Get node information from IPFS endpoint.
69
+ # @return [String] node information.
70
+ def id
71
+ res = Net::HTTP.post_form(URI("#{base_url}/id"), {})
72
+ res.body if res.is_a?(Net::HTTPSuccess)
73
+ end
74
+
75
+ # Check IPFS endpoint are up and running.
76
+ # @return [Boolean]
77
+ def up?
78
+ begin
79
+ id
80
+ true
81
+ rescue Errno::ECONNREFUSED
82
+ false
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Fetch content from IPFS.
89
+ def fetch(uri, max_bytesize: nil)
90
+ end
91
+
92
+ def build_body(boundary, content)
93
+ begin_boundary = "--#{boundary}\n"
94
+ first_part_content_type = "Content-Disposition: form-data;\n"
95
+ first_part_content_type += "Content-Type: application/octet-stream\n\n"
96
+ end_boundary = "\n--#{boundary}--"
97
+ begin_boundary + first_part_content_type + content + end_boundary
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,6 @@
1
+ module Sidetree
2
+ module CAS
3
+ autoload :FetchResult, "sidetree/cas/fetch_result"
4
+ autoload :IPFS, "sidetree/cas/ipfs"
5
+ end
6
+ end
data/lib/sidetree/key.rb CHANGED
@@ -12,7 +12,7 @@ module Sidetree
12
12
  public_key: nil,
13
13
  id: nil,
14
14
  purposes: [],
15
- type: nil
15
+ type: Sidetree::Params::DEFAULT_PUBKEY_TYPE
16
16
  )
17
17
  if private_key
18
18
  unless Key.valid_private_key?(private_key)
@@ -56,21 +56,26 @@ module Sidetree
56
56
  end
57
57
 
58
58
  # Generate Secp256k1 key.
59
- # @option [String] id Public key ID.
60
- # @option [String] purpose Purpose for public key. Supported values defined by [Sidetree::PublicKeyPurpose].
59
+ # @param [String] id Public key ID.
60
+ # @param [String] purpose Purpose for public key. Supported values defined by [Sidetree::PublicKeyPurpose].
61
+ # @param [String] type The type of public key defined by https://w3c-ccg.github.io/ld-cryptosuite-registry/.
61
62
  # @return [Sidetree::Key]
62
63
  # @raise [Sidetree::Error]
63
- def self.generate(id: nil, purposes: [])
64
+ def self.generate(
65
+ id: nil,
66
+ purposes: [],
67
+ type: Sidetree::Params::DEFAULT_PUBKEY_TYPE
68
+ )
64
69
  private_key =
65
70
  1 + SecureRandom.random_number(ECDSA::Group::Secp256k1.order - 1)
66
- Key.new(private_key: private_key, purposes: purposes, id: id)
71
+ Key.new(private_key: private_key, purposes: purposes, id: id, type: type)
67
72
  end
68
73
 
69
74
  # Generate key instance from jwk Hash.
70
75
  # @param [Hash] data jwk Hash object.
71
76
  # @return [Sidetree::Key]
72
77
  # @raise [Sidetree::Error]
73
- def self.from_hash(data)
78
+ def self.from_jwk(data)
74
79
  key_data = data["publicKeyJwk"] ? data["publicKeyJwk"] : data
75
80
  key_type = key_data["kty"]
76
81
  curve = key_data["crv"]
@@ -0,0 +1,21 @@
1
+ module Sidetree
2
+ module Model
3
+ class Chunk
4
+ attr_reader :chunk_file_uri
5
+
6
+ # Initializer
7
+ # @param [String] chunk_file_uri chunk file uri.
8
+ # @raise [Sidetree::Error]
9
+ def initialize(chunk_file_uri)
10
+ unless chunk_file_uri.is_a?(String)
11
+ raise Sidetree::Error, "chunk_file_uri must be String"
12
+ end
13
+ @chunk_file_uri = chunk_file_uri
14
+ end
15
+
16
+ def to_h
17
+ { chunkFileUri: chunk_file_uri }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,80 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#chunk-files
4
+ class ChunkFile
5
+ attr_reader :deltas # Array of Sidetree::Model::Delta
6
+
7
+ def initialize(deltas = [])
8
+ deltas.each do |delta|
9
+ unless delta.is_a?(Sidetree::Model::Delta)
10
+ raise Sidetree::Error,
11
+ "deltas contains data that is not Sidetree::Model::Delta object."
12
+ end
13
+ end
14
+ @deltas = deltas
15
+ end
16
+
17
+ # Generate chunk file from operations.
18
+ # @param [Array[Sidetree::OP::Create]] create_ops
19
+ # @param [Array[Sidetree::OP::Recover]] recover_ops
20
+ # @param [Array[Sidetree::OP::Update]] update_ops
21
+ def self.create_from_ops(create_ops: [], recover_ops: [], update_ops: [])
22
+ deltas = create_ops.map(&:delta)
23
+ # TODO add update and recover operation delta
24
+ ChunkFile.new(deltas)
25
+ end
26
+
27
+ # Parse chunk file from compressed data.
28
+ # @param [String] chunk_file compressed chunk file data.
29
+ # @param [Boolean] compressed Whether the chunk_file is compressed or not, default: true.
30
+ # @return [Sidetree::Model::ChunkFile]
31
+ # @raise [Sidetree::Error]
32
+ def self.parse(chunk_file, compressed: true)
33
+ max_bytes =
34
+ Sidetree::Params::MAX_CHUNK_FILE_SIZE *
35
+ Sidetree::Util::Compressor::ESTIMATE_DECOMPRESSION_MULTIPLIER
36
+ decompressed =
37
+ (
38
+ if compressed
39
+ Sidetree::Util::Compressor.decompress(
40
+ chunk_file,
41
+ max_bytes: max_bytes
42
+ )
43
+ else
44
+ chunk_file
45
+ end
46
+ )
47
+ json = JSON.parse(decompressed, symbolize_names: true)
48
+ json.keys.each do |k|
49
+ unless k == :deltas
50
+ raise Sidetree::Error,
51
+ "Unexpected property #{k.to_s} in chunk file."
52
+ end
53
+ end
54
+ unless json[:deltas].is_a?(Array)
55
+ raise Sidetree::Error,
56
+ "Invalid chunk file, deltas property is not an array."
57
+ end
58
+ ChunkFile.new(
59
+ json[:deltas].map do |delta|
60
+ Sidetree::Model::Delta.from_object(delta)
61
+ end
62
+ )
63
+ end
64
+
65
+ # Compress this chunk file
66
+ # @return [String] compressed data.
67
+ def to_compress
68
+ params = { deltas: deltas.map(&:to_h) }
69
+ Sidetree::Util::Compressor.compress(params.to_json)
70
+ end
71
+
72
+ # Check if the +other+ object have the same chunk data.
73
+ # @return [Boolean]
74
+ def ==(other)
75
+ return false unless other.is_a?(ChunkFile)
76
+ deltas == other.deltas
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,204 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#core-index-file
4
+ class CoreIndexFile
5
+ attr_reader :core_proof_file_uri
6
+ attr_reader :provisional_index_file_uri
7
+ attr_reader :writer_lock_id
8
+ attr_reader :create_ops
9
+ attr_reader :recover_ops
10
+ attr_reader :deactivate_ops
11
+
12
+ # @param [Array[Sidetree::OP::Create]] create_ops
13
+ # @param [Array[Sidetree::OP::Recover]] recover_ops
14
+ # @param [Array[Sidetree::OP::Deactivate]] deactivate_ops
15
+ # @param [String] provisional_index_file_uri
16
+ # @param [String] core_proof_file_uri
17
+ # @raise [Sidetree::Error]
18
+ def initialize(
19
+ create_ops: [],
20
+ recover_ops: [],
21
+ deactivate_ops: [],
22
+ provisional_index_file_uri: nil,
23
+ core_proof_file_uri: nil,
24
+ writer_lock_id: nil
25
+ )
26
+ (create_ops + recover_ops + deactivate_ops).each do |operation|
27
+ unless operation.is_a?(Sidetree::OP::Base)
28
+ raise Sidetree::Error, "Invalid operation class specified."
29
+ end
30
+ end
31
+ @create_ops = create_ops
32
+ @recover_ops = recover_ops
33
+ @deactivate_ops = deactivate_ops
34
+ @provisional_index_file_uri = provisional_index_file_uri
35
+ @core_proof_file_uri = core_proof_file_uri
36
+ @writer_lock_id = writer_lock_id
37
+ unless did_suffixes.length == did_suffixes.uniq.length
38
+ raise Sidetree::Error,
39
+ "Core index file multiple operations for the same DID"
40
+ end
41
+ if writer_lock_id &&
42
+ writer_lock_id.bytesize > Sidetree::Params::MAX_WRITER_LOCK_ID_SIZE
43
+ raise Sidetree::Error,
44
+ "Writer lock ID of #{writer_lock_id.bytesize} bytes exceeded the maximum size of #{Sidetree::Params::MAX_WRITER_LOCK_ID_SIZE} bytes"
45
+ end
46
+ if provisional_index_file_uri
47
+ Validator.validate_cas_file_uri!(
48
+ provisional_index_file_uri,
49
+ "provisional index file URI"
50
+ )
51
+ else
52
+ if (create_ops.length + recover_ops.length) > 0
53
+ raise Sidetree::Error, "Provisional index file uri missing"
54
+ end
55
+ end
56
+ if recover_ops.length > 0 || deactivate_ops.length > 0
57
+ Validator.validate_cas_file_uri!(
58
+ core_proof_file_uri,
59
+ "core proof file URI"
60
+ )
61
+ else
62
+ if core_proof_file_uri
63
+ raise Sidetree::Error,
64
+ "Core proof file is specified but there is no recover and no deactivate operation"
65
+ end
66
+ end
67
+ end
68
+
69
+ # Parse core index file.
70
+ # @param [String] index_data core index file data.
71
+ # @param [Boolean] compressed Whether the index_data is compressed or not, default: true.
72
+ # @return [Sidetree::Model::ProvisionalIndexFile]
73
+ def self.parse(index_data, compressed: true)
74
+ decompressed =
75
+ (
76
+ if compressed
77
+ begin
78
+ Sidetree::Util::Compressor.decompress(
79
+ index_data,
80
+ max_bytes: Sidetree::Params::MAX_CORE_INDEX_FILE_SIZE
81
+ )
82
+ rescue Zlib::GzipFile::Error
83
+ raise Sidetree::Error, "Core index file decompression failure"
84
+ end
85
+ else
86
+ index_data
87
+ end
88
+ )
89
+ begin
90
+ json = JSON.parse(decompressed, symbolize_names: true)
91
+ create_ops, recover_ops, deactivate_ops = [], [], []
92
+ core_proof_uri, provisional_index_uri = nil, nil
93
+ json.each do |k, v|
94
+ case k
95
+ when :provisionalIndexFileUri
96
+ provisional_index_uri = v
97
+ when :coreProofFileUri
98
+ core_proof_uri = v
99
+ when :operations
100
+ create_ops, recover_ops, deactivate_ops = parse_operations(v)
101
+ when :writerLockId
102
+ unless v.is_a?(String)
103
+ raise Sidetree::Error, "Core index file writerLockId not string"
104
+ end
105
+ else
106
+ raise Sidetree::Error,
107
+ "Unexpected property #{k.to_s} in core index file"
108
+ end
109
+ end
110
+ CoreIndexFile.new(
111
+ create_ops: create_ops,
112
+ recover_ops: recover_ops,
113
+ deactivate_ops: deactivate_ops,
114
+ provisional_index_file_uri: provisional_index_uri,
115
+ core_proof_file_uri: core_proof_uri,
116
+ writer_lock_id: json[:writerLockId]
117
+ )
118
+ rescue JSON::ParserError
119
+ raise Sidetree::Error, "Core index file is not json"
120
+ end
121
+ end
122
+
123
+ def self.parse_operations(operations)
124
+ create_ops, recover_ops, deactivate_ops = [], [], []
125
+ operations.each do |o_k, o_v|
126
+ case o_k
127
+ when :create
128
+ unless o_v.is_a?(Array)
129
+ raise Sidetree::Error, "Core index file create property not array"
130
+ end
131
+ create_ops =
132
+ o_v.map do |create|
133
+ Sidetree::OP::Create.from_json(create.to_json_c14n)
134
+ end
135
+ when :recover
136
+ unless o_v.is_a?(Array)
137
+ raise Sidetree::Error,
138
+ "Core index file recover property not array"
139
+ end
140
+ recover_ops =
141
+ o_v.map do |recover|
142
+ Sidetree::OP::Recover.from_json(recover.to_json_c14n)
143
+ end
144
+ when :deactivate
145
+ unless o_v.is_a?(Array)
146
+ raise Sidetree::Error,
147
+ "Core index file deactivate property not array"
148
+ end
149
+ deactivate_ops =
150
+ o_v.map do |deactivate|
151
+ Sidetree::OP::Deactivate.from_json(deactivate.to_json_c14n)
152
+ end
153
+ else
154
+ raise Sidetree::Error,
155
+ "Unexpected property #{o_k.to_s} in operations property in core index file"
156
+ end
157
+ end
158
+ [create_ops, recover_ops, deactivate_ops]
159
+ end
160
+
161
+ private_class_method :parse_operations
162
+
163
+ def did_suffixes
164
+ create_ops.map { |o| o.suffix.unique_suffix } +
165
+ (recover_ops + deactivate_ops).map { |o| o.did_suffix }
166
+ end
167
+
168
+ # Compress this core index file
169
+ # @return [String] compressed data.
170
+ def to_compress
171
+ params = {}
172
+ params[
173
+ :provisionalIndexFileUri
174
+ ] = provisional_index_file_uri if provisional_index_file_uri
175
+ operations = {}
176
+ unless create_ops.empty?
177
+ operations[:create] = create_ops.map do |create|
178
+ { suffixData: create.suffix.to_h }
179
+ end
180
+ end
181
+ unless recover_ops.empty?
182
+ operations[:recover] = recover_ops.map do |recover|
183
+ {
184
+ didSuffix: recover.did_suffix,
185
+ revealValue: recover.revealed_value
186
+ }
187
+ end
188
+ end
189
+ unless deactivate_ops.empty?
190
+ operations[:deactivate] = deactivate_ops.map do |deactivate|
191
+ {
192
+ didSuffix: deactivate.did_suffix,
193
+ revealValue: deactivate.revealed_value
194
+ }
195
+ end
196
+ end
197
+ params[:operations] = operations
198
+ params[:coreProofFileUri] = core_proof_file_uri if core_proof_file_uri
199
+ params[:writerLockId] = writer_lock_id if writer_lock_id
200
+ Sidetree::Util::Compressor.compress(params.to_json)
201
+ end
202
+ end
203
+ end
204
+ end
@@ -3,6 +3,7 @@ module Sidetree
3
3
  class Delta
4
4
  attr_reader :patches, :update_commitment
5
5
 
6
+ # Initializer
6
7
  # @param [Array[Hash]] patches
7
8
  # @param [String] update_commitment
8
9
  # @raise [Sidetree::Error]
@@ -11,7 +12,10 @@ module Sidetree
11
12
  @update_commitment = update_commitment
12
13
  end
13
14
 
14
- def self.parse(object)
15
+ # Create delta object from hash object.
16
+ # @param [Hash] object
17
+ # @return [Sidetree::Model::Delta]
18
+ def self.from_object(object)
15
19
  Sidetree::Validator.validate_delta!(object)
16
20
  Delta.new(object[:patches], object[:updateCommitment])
17
21
  end
@@ -23,6 +27,11 @@ module Sidetree
23
27
  def to_hash
24
28
  Sidetree.to_hash(to_h)
25
29
  end
30
+
31
+ def ==(other)
32
+ return false unless other.is_a?(Delta)
33
+ to_hash == other.to_hash
34
+ end
26
35
  end
27
36
  end
28
37
  end
@@ -0,0 +1,134 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#provisional-index-file
4
+ class ProvisionalIndexFile
5
+ attr_reader :provisional_proof_file_uri
6
+ attr_reader :chunks
7
+ attr_reader :operations
8
+
9
+ # Initialize
10
+ # @param [String] proof_file_uri Provisional Proof File URI.
11
+ # @param [Array[Sidetree::Model::Chunk]] chunks
12
+ # @param [Array[Sidetree::OP::Update]] operations Update operations
13
+ # @raise [Sidetree::Error]
14
+ def initialize(proof_file_uri: nil, chunks: [], operations: [])
15
+ if !chunks.is_a?(Array) || chunks.empty?
16
+ raise Sidetree::Error,
17
+ "Provisional Index File chunk property missing or incorrect type"
18
+ elsif chunks.length > 1
19
+ raise Sidetree::Error,
20
+ "Provisional Index File chunks does not have exactly one element"
21
+ end
22
+ @provisional_proof_file_uri = proof_file_uri
23
+ @chunks = chunks
24
+ @operations = operations
25
+ did_suffixes = operations.map(&:did_suffix).compact
26
+ if did_suffixes.length > 0
27
+ unless did_suffixes.length == did_suffixes.uniq.length
28
+ raise Sidetree::Error,
29
+ "Provisional Index File has multiple operations for same DID"
30
+ end
31
+ else
32
+ if proof_file_uri
33
+ raise Sidetree::Error,
34
+ "Provisional proof file '#{proof_file_uri}' not allowed in a provisional index file with no updates"
35
+ end
36
+ end
37
+ end
38
+
39
+ # Parse provisional index file.
40
+ # @param [String] index_data provisional index file data.
41
+ # @param [Boolean] compressed Whether the +index_data+ is compressed or not, default: true.
42
+ # @return [Sidetree::Model::ProvisionalIndexFile]
43
+ def self.parse(index_data, compressed: true)
44
+ decompressed =
45
+ (
46
+ if compressed
47
+ begin
48
+ Sidetree::Util::Compressor.decompress(
49
+ index_data,
50
+ max_bytes: Sidetree::Params::MAX_PROVISIONAL_INDEX_FILE_SIZE
51
+ )
52
+ rescue Zlib::GzipFile::Error
53
+ raise Sidetree::Error,
54
+ "Provisional Index File decompression failure"
55
+ end
56
+ else
57
+ index_data
58
+ end
59
+ )
60
+ begin
61
+ json = JSON.parse(decompressed, symbolize_names: true)
62
+ operations = []
63
+ chunks = []
64
+ json.each do |k, v|
65
+ case k
66
+ when :chunks
67
+ chunks =
68
+ v
69
+ .map do |chunk|
70
+ chunk.map do |c_k, c_v|
71
+ case c_k
72
+ when :chunkFileUri
73
+ Sidetree::Model::Chunk.new(c_v)
74
+ else
75
+ raise Sidetree::Error,
76
+ "Provisional Index File chunk has missing or unknown property"
77
+ end
78
+ end
79
+ end
80
+ .flatten
81
+ when :provisionalProofFileUri
82
+ when :operations
83
+ v.each do |o_k, o_v|
84
+ case o_k
85
+ when :update
86
+ unless o_v.is_a?(Array)
87
+ raise Sidetree::Error,
88
+ "Provisional Index File update operation not array"
89
+ end
90
+ operations =
91
+ o_v.map do |update|
92
+ Sidetree::OP::Update.from_json(update.to_json_c14n)
93
+ end
94
+ else
95
+ raise Sidetree::Error,
96
+ "Unexpected property '#{o_k.to_s}' in update operation"
97
+ end
98
+ end
99
+ else
100
+ raise Sidetree::Error,
101
+ "Unexpected property #{k.to_s} in provisional index file"
102
+ end
103
+ end
104
+ ProvisionalIndexFile.new(
105
+ chunks: chunks,
106
+ operations: operations,
107
+ proof_file_uri: json[:provisionalProofFileUri]
108
+ )
109
+ rescue JSON::ParserError
110
+ raise Sidetree::Error, "Provisional index file is not json"
111
+ end
112
+ end
113
+
114
+ # Compress this provisional index file
115
+ # @return [String] compressed data.
116
+ def to_compress
117
+ params = { chunks: chunks.map(&:to_h) }
118
+ unless operations.empty?
119
+ params[:operations] = {
120
+ update:
121
+ operations.map do |update|
122
+ {
123
+ didSuffix: update.did_suffix,
124
+ revealValue: update.revealed_value
125
+ }
126
+ end
127
+ }
128
+ params[:provisionalProofFileUri] = provisional_proof_file_uri
129
+ end
130
+ Sidetree::Util::Compressor.compress(params.to_json)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -10,10 +10,10 @@ module Sidetree
10
10
  @recovery_commitment = recovery_commitment
11
11
  end
12
12
 
13
- # Generate Suffix object from Hash object.
13
+ # Create Suffix object from hash object.
14
14
  # @return [Sidetree::Model::Suffix]
15
15
  # @raise [Sidetree::Error]
16
- def self.parse(object)
16
+ def self.from_object(object)
17
17
  Sidetree::Validator.validate_suffix_data!(object)
18
18
  Suffix.new(object[:deltaHash], object[:recoveryCommitment])
19
19
  end
@@ -4,5 +4,9 @@ module Sidetree
4
4
  autoload :Delta, "sidetree/model/delta"
5
5
  autoload :Document, "sidetree/model/document"
6
6
  autoload :Service, "sidetree/model/service"
7
+ autoload :CoreIndexFile, "sidetree/model/core_index_file"
8
+ autoload :ChunkFile, "sidetree/model/chunk_file"
9
+ autoload :Chunk, "sidetree/model/chunk"
10
+ autoload :ProvisionalIndexFile, "sidetree/model/provisional_index_file"
7
11
  end
8
12
  end
@@ -29,6 +29,39 @@ module Sidetree
29
29
  did.create_op
30
30
  end
31
31
 
32
+ # Parse create operation data from json string
33
+ # @param [String] create_data create operation data(json string).
34
+ # @return [Sidetree::OP::Create]
35
+ # @raise [Sidetree::Error]
36
+ def self.from_json(create_data)
37
+ begin
38
+ json = JSON.parse(create_data, symbolize_names: true)
39
+ json.each do |k, v|
40
+ case k
41
+ when :type, :suffixData, :delta
42
+ else
43
+ raise Sidetree::Error,
44
+ "Create operation missing or unknown property"
45
+ end
46
+ end
47
+ if json[:type] && json[:type] != Sidetree::OP::Type::CREATE
48
+ raise Sidetree::Error, "Create operation type incorrect"
49
+ end
50
+ suffix = Sidetree::Model::Suffix.from_object(json[:suffixData])
51
+ delta = nil
52
+ begin
53
+ # For compatibility with data pruning
54
+ delta = Sidetree::Model::Delta.from_object(json[:delta]) if json[
55
+ :delta
56
+ ]
57
+ rescue Sidetree::Error
58
+ end
59
+ Create.new(suffix, delta)
60
+ rescue JSON::ParserError
61
+ raise Sidetree::Error, "create_data not json"
62
+ end
63
+ end
64
+
32
65
  def type
33
66
  Sidetree::OP::Type::CREATE
34
67
  end
@@ -53,8 +86,8 @@ module Sidetree
53
86
  end
54
87
 
55
88
  Create.new(
56
- Sidetree::Model::Suffix.parse(json[:suffixData]),
57
- Sidetree::Model::Delta.parse(json[:delta])
89
+ Sidetree::Model::Suffix.from_object(json[:suffixData]),
90
+ Sidetree::Model::Delta.from_object(json[:delta])
58
91
  )
59
92
  rescue JSON::ParserError
60
93
  raise Error, "Long form initial state should be encoded jcs."
@@ -62,7 +95,7 @@ module Sidetree
62
95
  end
63
96
 
64
97
  def to_h
65
- { suffixData: suffix.to_h, delta: delta.to_h }
98
+ { suffixData: suffix.to_h, delta: delta&.to_h }
66
99
  end
67
100
 
68
101
  # Generate long_suffix for DID.
@@ -1,7 +1,76 @@
1
1
  module Sidetree
2
2
  module OP
3
- # Deactivate operation class TODO implementation
3
+ # Deactivate operation class
4
+ # https://identity.foundation/sidetree/spec/#deactivate
4
5
  class Deactivate < Base
6
+ attr_reader :did_suffix
7
+ attr_reader :signed_data
8
+ attr_reader :revealed_value
9
+
10
+ # Initialize
11
+ # @param [String] did_suffix
12
+ # @param [JSON::JWS] signed_data
13
+ # @param [String] revealed_value
14
+ def initialize(did_suffix, signed_data, revealed_value)
15
+ Sidetree::Validator.validate_encoded_multi_hash!(
16
+ did_suffix,
17
+ "#{type} didSuffix"
18
+ )
19
+ Sidetree::Validator.validate_encoded_multi_hash!(
20
+ revealed_value,
21
+ "#{type} revealValue"
22
+ )
23
+ @did_suffix = did_suffix
24
+ @signed_data = signed_data
25
+ @revealed_value = revealed_value
26
+ end
27
+
28
+ # Parse Deactivate operation data from json string
29
+ # @param [String] deactivate_data deactivate operation data(json string).
30
+ # @return [Sidetree::OP::Deactivate]
31
+ # @raise [Sidetree::Error]
32
+ def self.from_json(deactivate_data)
33
+ begin
34
+ json = JSON.parse(deactivate_data, symbolize_names: true)
35
+ jws, revealed_value, did_suffix = nil, nil, nil
36
+ json.each do |k, v|
37
+ case k
38
+ when :type
39
+ unless v == Sidetree::OP::Type::DEACTIVATE
40
+ raise Sidetree::Error, "Deactivate operation type incorrect"
41
+ end
42
+ when :didSuffix
43
+ did_suffix = v
44
+ when :revealValue
45
+ revealed_value = v
46
+ when :signedData
47
+ jws = JSON::JWS.decode_compact_serialized(v, :skip_verification)
48
+ unless jws.keys.length == 2
49
+ raise Sidetree::Error,
50
+ "Deactivate operation signed data missing or unknown property"
51
+ end
52
+ else
53
+ raise Sidetree::Error,
54
+ "Unexpected property #{k.to_s} in deactivate operation"
55
+ end
56
+ end
57
+ if jws
58
+ Validator.validate_canonicalize_object_hash!(
59
+ jws["recoveryKey"],
60
+ revealed_value,
61
+ "Deactivate key"
62
+ )
63
+ end
64
+ unless did_suffix
65
+ raise Sidetree::Error, "The deactivate didSuffix must be a string"
66
+ end
67
+ Deactivate.new(did_suffix, jws, revealed_value)
68
+ rescue JSON::ParserError
69
+ raise Sidetree::Error, "deactivate_data not json"
70
+ rescue JSON::JWS::InvalidFormat
71
+ raise Sidetree::Error, "Invalid signedData"
72
+ end
73
+ end
5
74
  def type
6
75
  Sidetree::OP::Type::DEACTIVATE
7
76
  end
@@ -1,7 +1,16 @@
1
1
  module Sidetree
2
2
  module OP
3
- # Recover operation class. TODO implementation
4
- class Recover < Base
3
+ # Recover operation class.
4
+ # https://identity.foundation/sidetree/spec/#recover
5
+ class Recover < Updatable
6
+ # Parse Recover operation data from json string
7
+ # @param [String] recover_data recover operation data(json string).
8
+ # @return [Sidetree::OP::Recover]
9
+ # @raise [Sidetree::Error]
10
+ def self.from_json(recover_data)
11
+ parse_json(recover_data, Sidetree::OP::Type::RECOVER)
12
+ end
13
+
5
14
  def type
6
15
  Sidetree::OP::Type::RECOVER
7
16
  end
@@ -0,0 +1,84 @@
1
+ module Sidetree
2
+ module OP
3
+ class Updatable < Base
4
+ attr_reader :did_suffix
5
+ attr_reader :delta
6
+ attr_reader :signed_data
7
+ attr_reader :revealed_value
8
+
9
+ # Initialize
10
+ # @param [String] did_suffix
11
+ # @param [Sidetree::Model::Delta] delta
12
+ # @param [JSON::JWS] signed_data
13
+ # @param [String] revealed_value
14
+ def initialize(did_suffix, delta, signed_data, revealed_value)
15
+ Sidetree::Validator.validate_encoded_multi_hash!(
16
+ did_suffix,
17
+ "#{type} didSuffix"
18
+ )
19
+ Sidetree::Validator.validate_encoded_multi_hash!(
20
+ revealed_value,
21
+ "#{type} revealValue"
22
+ )
23
+ @did_suffix = did_suffix
24
+ @delta = delta
25
+ @signed_data = signed_data
26
+ @revealed_value = revealed_value
27
+ end
28
+
29
+ def self.parse_json(data, type)
30
+ begin
31
+ json = JSON.parse(data, symbolize_names: true)
32
+ delta, jws, revealed_value, did_suffix = nil, nil, nil, nil
33
+ json.each do |k, v|
34
+ case k
35
+ when :type
36
+ unless v == type
37
+ raise Sidetree::Error,
38
+ "#{type.capitalize} operation type incorrect"
39
+ end
40
+ when :didSuffix
41
+ did_suffix = v
42
+ when :revealValue
43
+ revealed_value = v
44
+ when :signedData
45
+ jws = JSON::JWS.decode_compact_serialized(v, :skip_verification)
46
+ unless jws.keys.length ==
47
+ (type == Sidetree::OP::Type::RECOVER ? 3 : 2)
48
+ raise Sidetree::Error,
49
+ "#{type.capitalize} operation signed data missing or unknown property"
50
+ end
51
+ when :delta
52
+ delta = Sidetree::Model::Delta.from_object(v)
53
+ else
54
+ raise Sidetree::Error,
55
+ "Unexpected property #{k.to_s} in #{type} operation"
56
+ end
57
+ end
58
+ if jws
59
+ key =
60
+ type == Sidetree::OP::Type::UPDATE ? "updateKey" : "recoveryKey"
61
+ Validator.validate_canonicalize_object_hash!(
62
+ jws[key],
63
+ revealed_value,
64
+ "#{type.capitalize} key"
65
+ )
66
+ end
67
+ unless did_suffix
68
+ raise Sidetree::Error, "The #{type} didSuffix must be a string"
69
+ end
70
+ Module.const_get("Sidetree::OP::#{type.capitalize}").new(
71
+ did_suffix,
72
+ delta,
73
+ jws,
74
+ revealed_value
75
+ )
76
+ rescue JSON::ParserError
77
+ raise Sidetree::Error, "data dose not json"
78
+ rescue JSON::JWS::InvalidFormat
79
+ raise Sidetree::Error, "Invalid signedData"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,19 @@
1
+ module Sidetree
2
+ module OP
3
+ # Update operation class.
4
+ # https://identity.foundation/sidetree/spec/#update
5
+ class Update < Updatable
6
+ # Parse update operation data from json string
7
+ # @param [String] update_data update operation data(json string).
8
+ # @return [Sidetree::OP::Update]
9
+ # @raise [Sidetree::Error]
10
+ def self.from_json(update_data)
11
+ parse_json(update_data, Sidetree::OP::Type::UPDATE)
12
+ end
13
+
14
+ def type
15
+ Sidetree::OP::Type::UPDATE
16
+ end
17
+ end
18
+ end
19
+ end
data/lib/sidetree/op.rb CHANGED
@@ -32,8 +32,10 @@ module Sidetree
32
32
  end
33
33
 
34
34
  autoload :Base, "sidetree/op/base"
35
+ autoload :Updatable, "sidetree/op/updatable"
35
36
  autoload :Create, "sidetree/op/create"
36
37
  autoload :Recover, "sidetree/op/recover"
38
+ autoload :Update, "sidetree/op/update"
37
39
  autoload :Deactivate, "sidetree/op/deactivate"
38
40
  end
39
41
  end
@@ -0,0 +1,41 @@
1
+ module Sidetree
2
+ module Util
3
+ module AnchoredDataSerializer
4
+ DELIMITER = "."
5
+
6
+ module_function
7
+
8
+ # Serialize given data as Anchor String.
9
+ # @param [Integer] op_count Number of operations
10
+ # @param [String] uri Core Index File uri.
11
+ # @return [String] Anchor String
12
+ # @raise [Sidetree::Error]
13
+ def serialize(op_count, uri)
14
+ if op_count > Sidetree::Params::MAX_OPERATION_COUNT
15
+ raise Sidetree::Error, "Number of operations greater than max"
16
+ end
17
+ "#{op_count}#{DELIMITER}#{uri}"
18
+ end
19
+
20
+ # Deserializes the given string that is read from the blockchain into data.
21
+ # @param [String] anchor_str Anchor String
22
+ # @return [Array[Integer, String]]
23
+ # @raise [Sidetree::Error]
24
+ def deserialize(anchor_str)
25
+ data = anchor_str.split(DELIMITER)
26
+ raise Sidetree::Error, "Invalid anchor string" unless data.length == 2
27
+ unless data[0] =~ /^[1-9]\d*$/
28
+ raise Sidetree::Error,
29
+ "Number of operations in anchor string is not positive number"
30
+ end
31
+
32
+ count = data[0].to_i
33
+ if count > Sidetree::Params::MAX_OPERATION_COUNT
34
+ raise Sidetree::Error,
35
+ "Number of operations in anchor string greater than max"
36
+ end
37
+ [count, data[1]]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,38 @@
1
+ require "zlib"
2
+
3
+ module Sidetree
4
+ module Util
5
+ module Compressor
6
+ # The estimated ratio/multiplier of decompressed Sidetree CAS file size compared against the compressed file size.
7
+ ESTIMATE_DECOMPRESSION_MULTIPLIER = 3
8
+
9
+ module_function
10
+
11
+ # Compresses teh data in gzip and return it as buffer.
12
+ # @param [String] data Data to be compressed.
13
+ # @return [String] compressed data.
14
+ def compress(data)
15
+ io = StringIO.new("w")
16
+ Zlib::GzipWriter.wrap(io) do |w|
17
+ w.mtime = 0
18
+ w.write data
19
+ end
20
+ io.string.force_encoding("binary")
21
+ end
22
+
23
+ # Decompresses +compressed+.
24
+ # @param [String] compressed compressed data.
25
+ # @return [String] decompressed data.
26
+ # @raise [Sidetree::Error] raise if data exceeds max_bytes size.
27
+ def decompress(compressed, max_bytes: nil)
28
+ if max_bytes && compressed.bytesize > max_bytes
29
+ raise Sidetree::Error, "Exceed maximum compressed chunk file size."
30
+ end
31
+ io = StringIO.new(compressed)
32
+ result = StringIO.new
33
+ Zlib::GzipReader.wrap(io) { |gz| result << gz.read }
34
+ result.string
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,6 @@
1
+ module Sidetree
2
+ module Util
3
+ autoload :Compressor, "sidetree/util/compressor"
4
+ autoload :AnchoredDataSerializer, "sidetree/util/anchored_data_serializer"
5
+ end
6
+ end
@@ -215,6 +215,18 @@ module Sidetree
215
215
  end
216
216
  end
217
217
 
218
+ # Verify that the Multihash of +content+ matches that of +multihash+.
219
+ # @param [String] content content to be hashed
220
+ # @param [String] multihash Hash value to be checked
221
+ # @param [String] target Name of the target object to include in the error message
222
+ # @raise [Sidetree::Error]
223
+ def validate_canonicalize_object_hash!(content, multihash, target)
224
+ unless Sidetree.to_hash(content) == multihash
225
+ raise Sidetree::Error,
226
+ "Canonicalized #{target} object hash does not match expected hash '#{multihash}'"
227
+ end
228
+ end
229
+
218
230
  def validate_did_type!(type)
219
231
  return unless type
220
232
  raise Error, "DID type must be a string." unless type.instance_of?(String)
@@ -245,5 +257,16 @@ module Sidetree
245
257
  validate_encoded_multi_hash!(suffix[:deltaHash], "delta hash")
246
258
  validate_did_type!(suffix[:type])
247
259
  end
260
+
261
+ def validate_cas_file_uri!(uri, target)
262
+ unless uri.is_a?(String)
263
+ raise Sidetree::Error,
264
+ "Input #{target} CAS file URI '#{uri}' needs to be of string type"
265
+ end
266
+ if uri.bytesize > Sidetree::Params::MAX_CAS_URI_LENGTH
267
+ raise Sidetree::Error,
268
+ "Input #{target} CAS file URI byte size cannot exceed #{Sidetree::Params::MAX_CAS_URI_LENGTH}"
269
+ end
270
+ end
248
271
  end
249
272
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sidetree
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/sidetree.rb CHANGED
@@ -13,11 +13,13 @@ module Sidetree
13
13
  class Error < StandardError
14
14
  end
15
15
 
16
+ autoload :Util, "sidetree/util"
16
17
  autoload :Key, "sidetree/key"
17
18
  autoload :DID, "sidetree/did"
18
19
  autoload :Model, "sidetree/model"
19
20
  autoload :OP, "sidetree/op"
20
21
  autoload :Validator, "sidetree/validator"
22
+ autoload :CAS, "sidetree/cas"
21
23
 
22
24
  module Params
23
25
  # Algorithm for generating hashes of protocol-related values. 0x12 = sha2-256
@@ -26,7 +28,20 @@ module Sidetree
26
28
 
27
29
  # Maximum canonicalized operation delta buffer size.
28
30
  MAX_DELTA_SIZE = 1000
29
-
31
+ # Maximum compressed chunk file size. 10MB.
32
+ MAX_CHUNK_FILE_SIZE = 10_000_000
33
+ # Maximum compressed Provisional Index File size. 1 MB (zipped)
34
+ MAX_PROVISIONAL_INDEX_FILE_SIZE = 1_000_000
35
+ # Maximum compressed Core Index File size. 1 MB (zipped)
36
+ MAX_CORE_INDEX_FILE_SIZE = 1_000_000
37
+ # Maximum writer lock ID size
38
+ MAX_WRITER_LOCK_ID_SIZE = 200
39
+ # Maximum length of CAS URIs. 100 bytes
40
+ MAX_CAS_URI_LENGTH = 100
41
+ # Maximum number of operations per batch. 10000 ops.
42
+ MAX_OPERATION_COUNT = 10_000
43
+ # Default public key type
44
+ DEFAULT_PUBKEY_TYPE = "EcdsaSecp256k1VerificationKey2019"
30
45
  # Default DID method
31
46
  DEFAULT_METHOD = "sidetree"
32
47
 
data/sidetree.gemspec CHANGED
@@ -39,5 +39,6 @@ Gem::Specification.new do |spec|
39
39
  # For more information and examples about making a new gem, checkout our
40
40
  # guide at: https://bundler.io/guides/creating_gem.html
41
41
  spec.add_development_dependency 'rspec', '~> 3.0'
42
- spec.add_development_dependency 'prettier', '~> 3.1.2'
42
+ spec.add_development_dependency 'prettier', '~> 3.2.0'
43
+ spec.add_development_dependency 'webmock', '~> 3.14.0'
43
44
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sidetree
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-06-29 00:00:00.000000000 Z
11
+ date: 2022-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ecdsa
@@ -86,14 +86,28 @@ dependencies:
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: 3.1.2
89
+ version: 3.2.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: 3.1.2
96
+ version: 3.2.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 3.14.0
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 3.14.0
97
111
  description: Ruby implementation for Sidetree protocol.
98
112
  email:
99
113
  - azuchi@chaintope.com
@@ -116,11 +130,18 @@ files:
116
130
  - bin/console
117
131
  - bin/setup
118
132
  - lib/sidetree.rb
133
+ - lib/sidetree/cas.rb
134
+ - lib/sidetree/cas/fetch_result.rb
135
+ - lib/sidetree/cas/ipfs.rb
119
136
  - lib/sidetree/did.rb
120
137
  - lib/sidetree/key.rb
121
138
  - lib/sidetree/model.rb
139
+ - lib/sidetree/model/chunk.rb
140
+ - lib/sidetree/model/chunk_file.rb
141
+ - lib/sidetree/model/core_index_file.rb
122
142
  - lib/sidetree/model/delta.rb
123
143
  - lib/sidetree/model/document.rb
144
+ - lib/sidetree/model/provisional_index_file.rb
124
145
  - lib/sidetree/model/service.rb
125
146
  - lib/sidetree/model/suffix.rb
126
147
  - lib/sidetree/op.rb
@@ -128,6 +149,11 @@ files:
128
149
  - lib/sidetree/op/create.rb
129
150
  - lib/sidetree/op/deactivate.rb
130
151
  - lib/sidetree/op/recover.rb
152
+ - lib/sidetree/op/updatable.rb
153
+ - lib/sidetree/op/update.rb
154
+ - lib/sidetree/util.rb
155
+ - lib/sidetree/util/anchored_data_serializer.rb
156
+ - lib/sidetree/util/compressor.rb
131
157
  - lib/sidetree/validator.rb
132
158
  - lib/sidetree/version.rb
133
159
  - sidetree.gemspec