immudb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE.txt +202 -0
- data/README.md +187 -0
- data/lib/immudb/client.rb +366 -0
- data/lib/immudb/constants.rb +34 -0
- data/lib/immudb/dual_proof.rb +17 -0
- data/lib/immudb/grpc/schema_pb.rb +426 -0
- data/lib/immudb/grpc/schema_services_pb.rb +89 -0
- data/lib/immudb/htree.rb +120 -0
- data/lib/immudb/inclusion_proof.rb +21 -0
- data/lib/immudb/interceptor.rb +31 -0
- data/lib/immudb/kv.rb +25 -0
- data/lib/immudb/linear_proof.rb +23 -0
- data/lib/immudb/root_service.rb +36 -0
- data/lib/immudb/sql_result.rb +15 -0
- data/lib/immudb/state.rb +26 -0
- data/lib/immudb/store.rb +227 -0
- data/lib/immudb/tx.rb +56 -0
- data/lib/immudb/tx_metadata.rb +26 -0
- data/lib/immudb/txe.rb +27 -0
- data/lib/immudb/version.rb +3 -0
- data/lib/immudb.rb +35 -0
- metadata +78 -0
@@ -0,0 +1,366 @@
|
|
1
|
+
# Copyright 2021 CodeNotary, Inc. All rights reserved.
|
2
|
+
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
module Immudb
|
14
|
+
class Client
|
15
|
+
def initialize(url: nil, host: nil, port: nil, username: nil, password: nil, database: nil, timeout: nil, rs: nil)
|
16
|
+
url ||= ENV["IMMUDB_URL"]
|
17
|
+
if url
|
18
|
+
uri = URI.parse(url)
|
19
|
+
|
20
|
+
if uri.scheme != "immudb"
|
21
|
+
raise ArgumentError, "Expected url to start with immudb://"
|
22
|
+
end
|
23
|
+
|
24
|
+
host ||= uri.host
|
25
|
+
port ||= uri.port
|
26
|
+
username ||= uri.username
|
27
|
+
password ||= uri.password
|
28
|
+
database ||= uri.path.sub(/\A\//, "")
|
29
|
+
end
|
30
|
+
|
31
|
+
host ||= "localhost"
|
32
|
+
port ||= 3322
|
33
|
+
username ||= "immudb"
|
34
|
+
password ||= "immudb"
|
35
|
+
database ||= "defaultdb"
|
36
|
+
|
37
|
+
raise ArgumentError, "Invalid host" if host.include?(":")
|
38
|
+
|
39
|
+
@rs = rs || RootService.new
|
40
|
+
|
41
|
+
@interceptor = Interceptor.new
|
42
|
+
@grpc = Schema::ImmuService::Stub.new("#{host}:#{port.to_i}", :this_channel_is_insecure, timeout: timeout, interceptors: [interceptor])
|
43
|
+
|
44
|
+
login(username, password)
|
45
|
+
use_database(database)
|
46
|
+
end
|
47
|
+
|
48
|
+
def list_users
|
49
|
+
permission_map = PERMISSION.invert
|
50
|
+
grpc.list_users(Google::Protobuf::Empty.new).users.map do |user|
|
51
|
+
{
|
52
|
+
user: user.user,
|
53
|
+
permissions: user.permissions.map { |v| {database: v.database, permission: permission_map.fetch(v.permission)} },
|
54
|
+
created_by: user.createdby,
|
55
|
+
created_at: Time.parse(user.createdat),
|
56
|
+
active: user.active
|
57
|
+
}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_user(user, password:, permission:, database:)
|
62
|
+
req = Schema::CreateUserRequest.new(user: user, password: password, permission: PERMISSION.fetch(permission), database: database)
|
63
|
+
grpc.create_user(req)
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def change_password(user, old_password:, new_password:)
|
68
|
+
req = Schema::ChangePasswordRequest.new(user: user, oldPassword: old_password, newPassword: new_password)
|
69
|
+
grpc.change_password(req)
|
70
|
+
nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def set(key, value)
|
74
|
+
req = Schema::SetRequest.new(KVs: [Schema::KeyValue.new(key: key, value: value)])
|
75
|
+
grpc.set(req)
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def verified_set(key, value)
|
80
|
+
state = @rs.get
|
81
|
+
kv = Schema::KeyValue.new(key: key, value: value)
|
82
|
+
|
83
|
+
raw_request = Schema::VerifiableSetRequest.new(
|
84
|
+
setRequest: Schema::SetRequest.new(KVs: [kv]),
|
85
|
+
proveSinceTx: state.txId
|
86
|
+
)
|
87
|
+
verifiable_tx = grpc.verifiable_set(raw_request)
|
88
|
+
tx = Store.tx_from(verifiable_tx.tx)
|
89
|
+
inclusion_proof = tx.proof(SET_KEY_PREFIX + key)
|
90
|
+
ekv = Store.encode_kv(key, value)
|
91
|
+
verifies = Store.verify_inclusion(inclusion_proof, ekv.digest, tx.eh)
|
92
|
+
unless verifies
|
93
|
+
raise VerificationError
|
94
|
+
end
|
95
|
+
if tx.eh != Store.digest_from(verifiable_tx.dualProof.targetTxMetadata.eH)
|
96
|
+
raise VerificationError
|
97
|
+
end
|
98
|
+
if state.txId == 0
|
99
|
+
source_id = tx.ID
|
100
|
+
source_alh = tx.Alh
|
101
|
+
else
|
102
|
+
source_id = state.txId
|
103
|
+
source_alh = Store.digest_from(state.txHash)
|
104
|
+
end
|
105
|
+
target_id = tx.ID
|
106
|
+
target_alh = tx.Alh
|
107
|
+
|
108
|
+
verifies = Store.verify_dual_proof(
|
109
|
+
HTree.dual_proof_from(verifiable_tx.dualProof),
|
110
|
+
source_id,
|
111
|
+
target_id,
|
112
|
+
source_alh,
|
113
|
+
target_alh
|
114
|
+
)
|
115
|
+
if !verifies
|
116
|
+
raise VerificationError
|
117
|
+
end
|
118
|
+
newstate = State.new(
|
119
|
+
db: state.db,
|
120
|
+
txId: target_id,
|
121
|
+
txHash: target_alh,
|
122
|
+
publicKey: verifiable_tx.signature&.publicKey,
|
123
|
+
signature: verifiable_tx.signature&.signature,
|
124
|
+
)
|
125
|
+
if !verifying_key.nil?
|
126
|
+
newstate.verify(verifying_key)
|
127
|
+
end
|
128
|
+
@rs.set(newstate)
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def get(key)
|
133
|
+
req = Schema::KeyRequest.new(key: key)
|
134
|
+
grpc.get(req).value
|
135
|
+
end
|
136
|
+
|
137
|
+
def verified_get(key)
|
138
|
+
state = @rs.get
|
139
|
+
req = Schema::VerifiableGetRequest.new(
|
140
|
+
keyRequest: Schema::KeyRequest.new(key: key),
|
141
|
+
proveSinceTx: state.txId
|
142
|
+
)
|
143
|
+
ventry = grpc.verifiable_get(req)
|
144
|
+
|
145
|
+
inclusion_proof = HTree.inclusion_proof_from(ventry.inclusionProof)
|
146
|
+
dual_proof = HTree.dual_proof_from(ventry.verifiableTx.dualProof)
|
147
|
+
|
148
|
+
if ventry.entry.referencedBy.nil? || ventry.entry.referencedBy.key == ""
|
149
|
+
vTx = ventry.entry.tx
|
150
|
+
kv = Store.encode_kv(key, ventry.entry.value)
|
151
|
+
else
|
152
|
+
vTx = ventry.entry.referencedBy.tx
|
153
|
+
kv = store.encode_reference(ventry.entry.referencedBy.key, ventry.entry.key, ventry.entry.referencedBy.atTx)
|
154
|
+
end
|
155
|
+
|
156
|
+
if state.txId <= vTx
|
157
|
+
eh = Store.digest_from(ventry.verifiableTx.dualProof.targetTxMetadata.eH)
|
158
|
+
source_id = state.txId
|
159
|
+
source_alh = Store.digest_from(state.txHash)
|
160
|
+
target_id = vTx
|
161
|
+
target_alh = dual_proof.targetTxMetadata.alh
|
162
|
+
else
|
163
|
+
eh = Store.digest_from(ventry.verifiableTx.dualProof.sourceTxMetadata.eH)
|
164
|
+
source_id = vTx
|
165
|
+
source_alh = dual_proof.sourceTxMetadata.alh
|
166
|
+
target_id = state.txId
|
167
|
+
target_alh = store.digest_from(state.txHash)
|
168
|
+
end
|
169
|
+
|
170
|
+
verifies = Store.verify_inclusion(inclusion_proof, kv.digest, eh)
|
171
|
+
if !verifies
|
172
|
+
raise VerificationError
|
173
|
+
end
|
174
|
+
verifies = Store.verify_dual_proof(
|
175
|
+
dual_proof,
|
176
|
+
source_id,
|
177
|
+
target_id,
|
178
|
+
source_alh,
|
179
|
+
target_alh)
|
180
|
+
if !verifies
|
181
|
+
raise VerificationError
|
182
|
+
end
|
183
|
+
newstate = State.new(
|
184
|
+
db: state.db,
|
185
|
+
txId: target_id,
|
186
|
+
txHash: target_alh,
|
187
|
+
publicKey: ventry.verifiableTx.signature&.publicKey,
|
188
|
+
signature: ventry.verifiableTx.signature&.signature,
|
189
|
+
)
|
190
|
+
if !verifying_key.nil?
|
191
|
+
newstate.verify(verifying_key)
|
192
|
+
end
|
193
|
+
@rs.set(newstate)
|
194
|
+
|
195
|
+
ventry.entry.value
|
196
|
+
end
|
197
|
+
|
198
|
+
def set_all(values)
|
199
|
+
req = Schema::SetRequest.new(KVs: values.map { |k, v| Schema::KeyValue.new(key: k, value: v) })
|
200
|
+
grpc.set(req)
|
201
|
+
nil
|
202
|
+
end
|
203
|
+
|
204
|
+
def get_all(keys)
|
205
|
+
req = Schema::KeyListRequest.new(keys: keys)
|
206
|
+
grpc.get_all(req).entries.to_h { |v| [v.key, v.value] }
|
207
|
+
end
|
208
|
+
|
209
|
+
def create_database(name)
|
210
|
+
req = Schema::Database.new(databaseName: name)
|
211
|
+
grpc.create_database(req)
|
212
|
+
nil
|
213
|
+
end
|
214
|
+
|
215
|
+
def list_databases
|
216
|
+
grpc.database_list(Google::Protobuf::Empty.new).databases.map(&:databaseName)
|
217
|
+
end
|
218
|
+
|
219
|
+
def use_database(name)
|
220
|
+
req = Schema::Database.new(databaseName: name)
|
221
|
+
res = grpc.use_database(req)
|
222
|
+
interceptor.token = res.token
|
223
|
+
@rs.init(name, grpc)
|
224
|
+
nil
|
225
|
+
end
|
226
|
+
|
227
|
+
# history
|
228
|
+
|
229
|
+
def history(key, offset: nil, limit: nil, desc: false)
|
230
|
+
req = Schema::HistoryRequest.new(key: key, offset: offset, limit: limit, desc: desc)
|
231
|
+
grpc.history(req).entries.map do |entry|
|
232
|
+
{
|
233
|
+
tx: entry.tx,
|
234
|
+
value: entry.value
|
235
|
+
}
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def scan(seek_key: nil, prefix: nil, desc: false, limit: nil, since_tx: nil, no_wait: false)
|
240
|
+
req = Schema::ScanRequest.new(seekKey: seek_key, prefix: prefix, desc: desc, limit: limit, sinceTx: since_tx, noWait: no_wait)
|
241
|
+
grpc.scan(req).entries.map do |entry|
|
242
|
+
{
|
243
|
+
tx: entry.tx,
|
244
|
+
key: entry.key,
|
245
|
+
value: entry.value
|
246
|
+
}
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# management
|
251
|
+
|
252
|
+
def clean_index
|
253
|
+
grpc.compact_index(Google::Protobuf::Empty.new)
|
254
|
+
nil
|
255
|
+
end
|
256
|
+
|
257
|
+
def healthy?
|
258
|
+
grpc.health(Google::Protobuf::Empty.new).status
|
259
|
+
rescue Error
|
260
|
+
false
|
261
|
+
end
|
262
|
+
|
263
|
+
def version
|
264
|
+
grpc.health(Google::Protobuf::Empty.new).version
|
265
|
+
end
|
266
|
+
|
267
|
+
# sql
|
268
|
+
|
269
|
+
def sql_exec(sql, params = {})
|
270
|
+
req = Schema::SQLExecRequest.new(sql: sql, params: sql_params(params))
|
271
|
+
grpc.sql_exec(req)
|
272
|
+
nil
|
273
|
+
end
|
274
|
+
|
275
|
+
def sql_query(sql, params = {})
|
276
|
+
req = Schema::SQLQueryRequest.new(sql: sql, params: sql_params(params))
|
277
|
+
res = grpc.sql_query(req)
|
278
|
+
sql_result(res)
|
279
|
+
end
|
280
|
+
|
281
|
+
def list_tables
|
282
|
+
grpc.list_tables(Google::Protobuf::Empty.new).rows.map { |r| r.values.first.s }
|
283
|
+
end
|
284
|
+
|
285
|
+
def describe_table(name)
|
286
|
+
req = Schema::Table.new(tableName: name)
|
287
|
+
res = grpc.describe_table(req)
|
288
|
+
sql_result(res).to_a
|
289
|
+
end
|
290
|
+
|
291
|
+
# hide token
|
292
|
+
def inspect
|
293
|
+
to_s
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
def grpc
|
299
|
+
@grpc
|
300
|
+
end
|
301
|
+
|
302
|
+
def interceptor
|
303
|
+
@interceptor
|
304
|
+
end
|
305
|
+
|
306
|
+
def verifying_key
|
307
|
+
nil
|
308
|
+
end
|
309
|
+
|
310
|
+
# keep private for now
|
311
|
+
# if public, need to call use_database
|
312
|
+
def login(user, password)
|
313
|
+
req = Schema::LoginRequest.new(user: user, password: password)
|
314
|
+
res = grpc.login(req)
|
315
|
+
interceptor.token = res.token
|
316
|
+
|
317
|
+
# warn "[immudb] #{res.warning}" if res.warning
|
318
|
+
|
319
|
+
res
|
320
|
+
end
|
321
|
+
|
322
|
+
def logout
|
323
|
+
res = grpc.logout(Google::Protobuf::Empty.new)
|
324
|
+
interceptor.token = nil
|
325
|
+
res
|
326
|
+
end
|
327
|
+
|
328
|
+
def sql_params(params)
|
329
|
+
params.map do |k, v|
|
330
|
+
Schema::NamedParam.new(name: k, value: sql_value(v))
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
def sql_value(v)
|
335
|
+
opts =
|
336
|
+
case v
|
337
|
+
when nil
|
338
|
+
{null: :NULL_VALUE}
|
339
|
+
when Integer
|
340
|
+
{n: v}
|
341
|
+
when String
|
342
|
+
{s: v}
|
343
|
+
when true, false
|
344
|
+
{b: v}
|
345
|
+
else
|
346
|
+
raise ArgumentError, "Unsupported param type: #{v.class.name}"
|
347
|
+
end
|
348
|
+
Schema::SQLValue.new(**opts)
|
349
|
+
end
|
350
|
+
|
351
|
+
def sql_result(res)
|
352
|
+
columns = res.columns.map { |v| sql_column_name(v.name) }
|
353
|
+
rows = res.rows.map { |r| r.values.map { |v| v.value == :null ? nil : v.send(v.value) } }
|
354
|
+
column_types = res.columns.to_h { |v| [v.name, v.type] }
|
355
|
+
SqlResult.new(columns, rows, column_types)
|
356
|
+
end
|
357
|
+
|
358
|
+
def sql_column_name(name)
|
359
|
+
if name.start_with?("(") && name.end_with?(")")
|
360
|
+
name.split(".").last.chomp(")")
|
361
|
+
else
|
362
|
+
name
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
366
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Copyright 2021 CodeNotary, Inc. All rights reserved.
|
2
|
+
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
module Immudb
|
14
|
+
LEAF_PREFIX = "\x00".b
|
15
|
+
NODE_PREFIX = "\x01".b
|
16
|
+
ROOT_CACHE_PATH = ".immudbRoot"
|
17
|
+
|
18
|
+
PERMISSION = {
|
19
|
+
sys_admin: 255,
|
20
|
+
admin: 254,
|
21
|
+
none: 0,
|
22
|
+
read: 1,
|
23
|
+
read_write: 2
|
24
|
+
}
|
25
|
+
|
26
|
+
SET_KEY_PREFIX = "\x00".b
|
27
|
+
SORTED_KEY_PREFIX = "\x01".b
|
28
|
+
|
29
|
+
PLAIN_VALUE_PREFIX = "\x00".b
|
30
|
+
REFERENCE_VALUE_PREFIX = "\x01".b
|
31
|
+
|
32
|
+
OLDEST_FIRST = false
|
33
|
+
NEWEST_FIRST = true
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Copyright 2021 CodeNotary, Inc. All rights reserved.
|
2
|
+
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
7
|
+
# Unless required by applicable law or agreed to in writing, software
|
8
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
9
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
10
|
+
# See the License for the specific language governing permissions and
|
11
|
+
# limitations under the License.
|
12
|
+
|
13
|
+
module Immudb
|
14
|
+
class DualProof
|
15
|
+
attr_accessor :sourceTxMetadata, :targetTxMetadata, :inclusionProof, :consistencyProof, :targetBlTxAlh, :lastInclusionProof, :linearProof
|
16
|
+
end
|
17
|
+
end
|