sidetree 0.1.1 → 0.1.2

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: 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