sidetree 0.1.1 → 0.1.4

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.
@@ -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
@@ -35,6 +35,12 @@ module Sidetree
35
35
  def to_h
36
36
  { publicKeys: public_keys.map(&:to_h), services: services.map(&:to_h) }
37
37
  end
38
+
39
+ # Generate replace patch.
40
+ # @return [Hash]
41
+ def to_replace_patch
42
+ { action: OP::PatchAction::REPLACE, document: to_h }
43
+ end
38
44
  end
39
45
  end
40
46
  end
@@ -0,0 +1,129 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#provisional-index-file
4
+ class ProvisionalIndexFile < CASFileBase
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
+ decompress(
48
+ index_data,
49
+ Sidetree::Params::MAX_PROVISIONAL_INDEX_FILE_SIZE
50
+ )
51
+ else
52
+ index_data
53
+ end
54
+ )
55
+ begin
56
+ json = JSON.parse(decompressed, symbolize_names: true)
57
+ operations = []
58
+ chunks = []
59
+ json.each do |k, v|
60
+ case k
61
+ when :chunks
62
+ chunks =
63
+ v
64
+ .map do |chunk|
65
+ chunk.map do |c_k, c_v|
66
+ case c_k
67
+ when :chunkFileUri
68
+ Sidetree::Model::Chunk.new(c_v)
69
+ else
70
+ raise Sidetree::Error,
71
+ "Provisional Index File chunk has missing or unknown property"
72
+ end
73
+ end
74
+ end
75
+ .flatten
76
+ when :provisionalProofFileUri
77
+ when :operations
78
+ v.each do |o_k, o_v|
79
+ case o_k
80
+ when :update
81
+ unless o_v.is_a?(Array)
82
+ raise Sidetree::Error,
83
+ "Provisional Index File update operation not array"
84
+ end
85
+ operations =
86
+ o_v.map do |update|
87
+ Sidetree::OP::Update.from_json(update.to_json_c14n)
88
+ end
89
+ else
90
+ raise Sidetree::Error,
91
+ "Unexpected property '#{o_k.to_s}' in update operation"
92
+ end
93
+ end
94
+ else
95
+ raise Sidetree::Error,
96
+ "Unexpected property #{k.to_s} in provisional index file"
97
+ end
98
+ end
99
+ ProvisionalIndexFile.new(
100
+ chunks: chunks,
101
+ operations: operations,
102
+ proof_file_uri: json[:provisionalProofFileUri]
103
+ )
104
+ rescue JSON::ParserError
105
+ raise Sidetree::Error, "Provisional index file is not json"
106
+ end
107
+ end
108
+
109
+ # Build json string to be stored in CAS.
110
+ # @return [String] json string.
111
+ def to_json
112
+ params = { chunks: chunks.map(&:to_h) }
113
+ unless operations.empty?
114
+ params[:operations] = {
115
+ update:
116
+ operations.map do |update|
117
+ {
118
+ didSuffix: update.did_suffix,
119
+ revealValue: update.revealed_value
120
+ }
121
+ end
122
+ }
123
+ params[:provisionalProofFileUri] = provisional_proof_file_uri
124
+ end
125
+ params.to_json
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,80 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#provisional-proof-file
4
+ class ProvisionalProofFile < CASFileBase
5
+ attr_reader :update_proofs
6
+
7
+ # Initialize
8
+ # @param [Array[JSON::JWS]] update_proofs Array of update proof.
9
+ def initialize(update_proofs)
10
+ @update_proofs = update_proofs
11
+ end
12
+
13
+ # Parse provisional proof file from compressed data.
14
+ # @param [String] proof_file compressed provisional proof file.
15
+ # @param [Boolean] compressed Whether the proof_file is compressed or not, default: true.
16
+ # @return [Sidetree::Model::ProvisionalProofFile]
17
+ # @raise [Sidetree::Error]
18
+ def self.parse(proof_file, compressed: true)
19
+ decompressed =
20
+ (
21
+ if compressed
22
+ decompress(proof_file, Sidetree::Params::MAX_PROOF_FILE_SIZE)
23
+ else
24
+ proof_file
25
+ end
26
+ )
27
+ begin
28
+ json = JSON.parse(decompressed, symbolize_names: true)
29
+ json.keys.each do |k|
30
+ unless k == :operations
31
+ raise Sidetree::Error,
32
+ "Unexpected property #{k.to_s} in provisional proof file"
33
+ end
34
+ end
35
+ unless json[:operations]
36
+ raise Sidetree::Error,
37
+ "Provisional proof file does not have any operation proofs"
38
+ end
39
+ json[:operations].keys.each do |k|
40
+ unless k == :update
41
+ raise Sidetree::Error,
42
+ "Unexpected property #{k.to_s} in provisional proof file"
43
+ end
44
+ end
45
+ unless json[:operations][:update].is_a?(Array)
46
+ raise Sidetree::Error,
47
+ "Provisional proof file update property not array"
48
+ end
49
+ update_proofs =
50
+ json[:operations][:update].each.map do |update|
51
+ update.keys.each do |k|
52
+ unless k == :signedData
53
+ raise Sidetree::Error,
54
+ "Unexpected property #{k.to_s} in provisional proof file"
55
+ end
56
+ end
57
+ Sidetree::Util::JWS.parse(update[:signedData])
58
+ end
59
+ if update_proofs.empty?
60
+ raise Sidetree::Error, "Provisional proof file has no proof"
61
+ end
62
+ ProvisionalProofFile.new(update_proofs)
63
+ rescue JSON::ParserError
64
+ raise Sidetree::Error, "Provisional proof file is not json"
65
+ end
66
+ end
67
+
68
+ # Build json string to be stored in CAS.
69
+ # @return [String] json string.
70
+ def to_json
71
+ params = {
72
+ operations: {
73
+ update: update_proofs.map { |u| { signedData: u.to_s } }
74
+ }
75
+ }
76
+ params.to_json
77
+ end
78
+ end
79
+ end
80
+ 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,12 @@ 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 :CASFileBase, "sidetree/model/cas_file_base"
8
+ autoload :CoreIndexFile, "sidetree/model/core_index_file"
9
+ autoload :ChunkFile, "sidetree/model/chunk_file"
10
+ autoload :Chunk, "sidetree/model/chunk"
11
+ autoload :ProvisionalIndexFile, "sidetree/model/provisional_index_file"
12
+ autoload :ProvisionalProofFile, "sidetree/model/provisional_proof_file"
13
+ autoload :CoreProofFile, "sidetree/model/core_proof_file"
7
14
  end
8
15
  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,79 @@
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 = Sidetree::Util::JWS.parse(v)
48
+ unless jws.keys.length == 2
49
+ raise Sidetree::Error,
50
+ "Deactivate operation signed data missing or unknown property"
51
+ end
52
+ Sidetree::Util::JWK.validate!(
53
+ Sidetree::Util::JWK.parse(jws["recoveryKey"])
54
+ )
55
+ else
56
+ raise Sidetree::Error,
57
+ "Unexpected property #{k.to_s} in deactivate operation"
58
+ end
59
+ end
60
+ if jws
61
+ Validator.validate_canonicalize_object_hash!(
62
+ jws["recoveryKey"],
63
+ revealed_value,
64
+ "Deactivate key"
65
+ )
66
+ end
67
+ unless did_suffix
68
+ raise Sidetree::Error, "The deactivate didSuffix must be a string"
69
+ end
70
+ Deactivate.new(did_suffix, jws, revealed_value)
71
+ rescue JSON::ParserError
72
+ raise Sidetree::Error, "deactivate_data not json"
73
+ rescue JSON::JWS::InvalidFormat
74
+ raise Sidetree::Error, "Invalid signedData"
75
+ end
76
+ end
5
77
  def type
6
78
  Sidetree::OP::Type::DEACTIVATE
7
79
  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,98 @@
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
+ # @raise [Sidetree::Error]
15
+ def initialize(did_suffix, delta, signed_data, revealed_value)
16
+ Sidetree::Validator.validate_encoded_multi_hash!(
17
+ did_suffix,
18
+ "#{type} didSuffix"
19
+ )
20
+ Sidetree::Validator.validate_encoded_multi_hash!(
21
+ revealed_value,
22
+ "#{type} revealValue"
23
+ )
24
+ if signed_data
25
+ Validator.validate_canonicalize_object_hash!(
26
+ signed_data[key_name],
27
+ revealed_value,
28
+ type
29
+ )
30
+ end
31
+ @did_suffix = did_suffix
32
+ @delta = delta
33
+ @signed_data = signed_data
34
+ @revealed_value = revealed_value
35
+ end
36
+
37
+ def self.parse_json(data, type)
38
+ begin
39
+ json = JSON.parse(data, symbolize_names: true)
40
+ delta, jws, revealed_value, did_suffix = nil, nil, nil, nil
41
+ json.each do |k, v|
42
+ case k
43
+ when :type
44
+ unless v == type
45
+ raise Sidetree::Error,
46
+ "#{type.capitalize} operation type incorrect"
47
+ end
48
+ when :didSuffix
49
+ did_suffix = v
50
+ when :revealValue
51
+ revealed_value = v
52
+ when :signedData
53
+ jws = Sidetree::Util::JWS.parse(v)
54
+ unless jws.keys.length ==
55
+ (type == Sidetree::OP::Type::RECOVER ? 3 : 2)
56
+ raise Sidetree::Error,
57
+ "#{type.capitalize} operation signed data missing or unknown property"
58
+ end
59
+ key_name =
60
+ (
61
+ if type == Sidetree::OP::Type::RECOVER
62
+ "recoveryKey"
63
+ else
64
+ "updateKey"
65
+ end
66
+ )
67
+ Sidetree::Util::JWK.validate!(
68
+ Sidetree::Util::JWK.parse(jws[key_name])
69
+ )
70
+ when :delta
71
+ delta = Sidetree::Model::Delta.from_object(v)
72
+ else
73
+ raise Sidetree::Error,
74
+ "Unexpected property #{k.to_s} in #{type} operation"
75
+ end
76
+ end
77
+ unless did_suffix
78
+ raise Sidetree::Error, "The #{type} didSuffix must be a string"
79
+ end
80
+ Module.const_get("Sidetree::OP::#{type.capitalize}").new(
81
+ did_suffix,
82
+ delta,
83
+ jws,
84
+ revealed_value
85
+ )
86
+ rescue JSON::ParserError
87
+ raise Sidetree::Error, "data dose not json"
88
+ rescue JSON::JWS::InvalidFormat
89
+ raise Sidetree::Error, "Invalid signedData"
90
+ end
91
+ end
92
+
93
+ def key_name
94
+ type == Sidetree::OP::Type::UPDATE ? "updateKey" : "recoveryKey"
95
+ end
96
+ end
97
+ end
98
+ 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