sidetree 0.1.1 → 0.1.4

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