-
Notifications
You must be signed in to change notification settings - Fork 0
/
decoder-antelope.js
356 lines (298 loc) · 11.8 KB
/
decoder-antelope.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
/*
Copyright (c) 2023 Firmware Modules Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*
* The 'Antelope' platform is a blockchain system described at https://antelope.io/
*
* In a nutshell, applications are built and deployed onto the blockchain and these present
* a callable API in the form of 'Actions'. The Actions are defined as familiar function calls
* with standard arguments like integers, floating points values, arrays of bytes and other types,
* and so on.
*
* Being blockchain, though, means that such Actions can only be called by verified entities,
* that is, entities that possess the required private key. These entities must digitally 'sign' the Action's
* arguments plus details about themselves, the time, and information about the particular
* Antelope blockchain instance the Action is being called on, with this key. The entire signed data
* blob, including the signature, is called a Transaction.
*
* A suitably constructed Action will prevent the invocation of this Action by anyone except those
* in the possession of the secret key. Measurement{Earth} Trusted Sensor Platform (ME-TSP) hardware
* modules securely embed a secret key that allows the device to invoke an Action to send
* measured environmental sensor data to the application hosted on an Antelope blockchain.
*
* An ME-TSP module is often located remotely and unable to access the internet directly.
* In these cases, wireless data transports such as LoRaWAN and satellite are utilized.
* These transports impose certain restrictions on the amount of data that can be transferred,
* and transferred at one time. The Measurement{Earth} Low Power Protocol is designed to
* send an Antelope blockchain's transction in smaller chunks. It is the job of this
* Antelope platform decoder to piece together these chunks into a complete Transaction,
* filling in any missing gaps, and then to submit this Transaction to a normal blockchain API endpoint.
*
* The benefit to sending in smaller chunks is that smaller payloads can be used (albeit more of them).
*
*/
/* The goal of this platform processor is to create the transaction JSON object
* that can be pushed to the '/send_transaction' API from the data sent through the ELPP protocol.
*
{
"signatures": [
"SIG_K1_KozSvCUNwYXCPPoEK59qfvuPsBqLPMAdRJbKi3NLKrP6qXeyhKLTMJCUwzReo97KRJeSD6jHDom8vKMXey6C6hLhyvXpap"
],
"compression": false,
"packed_context_free_data": "",
"packed_trx": "e5f23660ec64123a7f1d000000000180b1ba1967ea30550000000064278fc601a0129d2164ea3055000000004887337526a0129d2164ea30550b4d452d5453502d524649442d0200000d636238636564642d646972747900"
}
To use this module, the user must supply to the decode function an object to record state for *each device* across messages.
The object must have an empty object called 'trx_map'
E.g.:
var decoder_state = {
trx_map: { }
}
*/
const log = console.log
const log_obj = function (obj) { console.dir(obj, { depth: null }) }
const crypto = require('crypto')
const elpp = require('./decoder')
function new_trx() {
return {
chain : 0,
signature : null, /* 'SIG_K1...' */
tapos: null, /* raw bytes */
action: null,
data : null
}
}
function get_trx(obj, id) {
var trx
if (id in obj.state.trx_map) {
trx = obj.state.trx_map[id]
} else {
trx = obj.state.trx_map[id] = new_trx()
}
trx.last_epoch = Date.now() / 1000 >> 0
return trx
}
function check_dispatch(obj, trx_id, trx) {
/* If have all componenents, can convert to JSON and dispatch it to the appropropriate
* chain dispatcher. The dispatcher should handle selection of API endpoint, retries, etc.
*/
log('checking for trx id ' + trx_id + ' complete...')
log(' => ' + get_status_trx(trx))
if (trx.signature && trx.tapos && trx.action && trx.data) {
var transaction = {
signatures: [trx.signature],
compression: false,
packed_context_free_data: '',
/* To note that there is also a 'context free array' that comes after TAPOS
* and before ACTION in the serialized data section as well, this must be
* set to 0.
*/
packed_trx: Buffer.concat([trx.tapos, Buffer.from([0]), trx.action, trx.data]).toString('hex')
}
log('transaction complete:')
let json = JSON.stringify(transaction, null, 2)
log(json)
if (0) {
let epoch = Date.now() / 1000 >> 0
/* Move it to the dispatcher queue for the chain */
obj.state.dispatcher_queue[trx.chain].push({
epoch: epoch,
started: false,
json: json
})
}
/* Return the completed trx with some metadata */
obj.trx = {
chain: trx.chain,
json: json
}
/* Remove the transaction from the map */
delete obj.state.trx_map[trx_id]
}
}
/* In each of these 'processor' functions, the decoder's decoded output
* stored in the 'out' array is captured and processed accordingly.
* The elements of the 'out' array correspond to output from each primitive decoder
* composing the top-level decoder. For these Antelope decoders, there are at least
* two: the header (uint8) is available as out[0], and subsequent data as out[1], out[2], etc.
*
*/
function antelope_message_tapos_processor(out, obj) {
var header = out[0]
var trx_id = header & 0x7
var chain_id = out[1] & 0x7
var trx = get_trx(obj, trx_id)
if (trx.tapos == null) {
trx.chain = chain_id
var data = Buffer.from(out[2])
trx.tapos = Buffer.alloc(13)
/* max net, max cpu, delay sec all 0 */
data.copy(trx.tapos)
}
log('have tapos: ')
log_obj(trx.tapos)
check_dispatch(obj, trx_id, trx)
}
function antelope_message_action_processor(out, obj) {
var header = out[0]
var trx_id = header & 0x7
var trx = get_trx(obj, trx_id)
if (trx.action === null) {
/* Re-encode the action by inserting the array length fields */
/* 1 action, dapp name, action name, 1 perm, perm name, actor name */
trx.action = Buffer.alloc(34)
trx.action.writeUInt8(1, 0) /* varuint32 -> encodes '1' as simply 0x1 */
Buffer.from(out[1]).copy(trx.action, 1)
trx.action.writeUInt8(1, 17) /* varuint32 -> encodes '1' as simply 0x1 */
Buffer.from(out[2]).copy(trx.action, 18)
}
log('have action: ')
log_obj(trx.action)
check_dispatch(obj, trx_id, trx)
}
function antelope_message_serialized_action_processor(out, obj) {
var header = out[0]
var trx_id = header & 0x7
var trx = get_trx(obj, trx_id)
if (trx.data === null) {
/* */
trx.data = Buffer.from(out[2])
}
log('have action data: ')
log_obj(trx.data)
check_dispatch(obj, trx_id, trx)
}
function antelope_message_signature_processor(out, obj) {
/* This message provides all signature data so we can generate
* the string representation now.
*/
/* The output is a fixed array of bytes containing i, r, s.
* We can process the array as-is.
*/
var header = out[0]
var trx_id = header & 0x7
var trx = get_trx(obj, trx_id)
if (trx.signature === null) {
var sig_k1 = check_encode_k1(Buffer.from(out[1]))
trx.signature = sig_k1
}
log('have signature: ')
log_obj(trx.signature)
check_dispatch(obj, trx_id, trx)
}
function antelope_message_tapos_req_processor(out, obj) {
log('need tapos')
/* return request to the server */
obj.tapos_req = {
chain_id: out[0],
req_id : out[1]
}
}
/*
* The signature bytes must be processed and base58 encoded into string prepended with "SIG_K1_".
* The functions below do that.
*/
function check_encode_k1(key_buf) {
var hash_ripemd = crypto.createHash('ripemd160')
var buf = Buffer.concat([key_buf, Buffer.from('K1')])
hash_ripemd.update(buf)
var checksum = hash_ripemd.digest().slice(0, 4)
var result = Buffer.concat([key_buf, checksum])
return 'SIG_K1_' + to_b58(result)
}
/* https://gist.github.com/diafygi/90a3e80ca1c2793220e5/ */
function to_b58(B) {
var A = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
var d = [], s = "", i, j, c, n; for (var i = 0; i < B.length; i++) { j = 0, c = B[i]; s += c || s.length ^ i ? "" : 1; while (j in d || c) { n = d[j]; n = n ? n * 256 + c : c; c = n / 58 | 0; d[j] = n % 58; j++ } } while (j--) s += A[d[j]]; return s
};
var channel_map = {
0: { decoder: elpp.antelope_message_tapos_decoder, processor: antelope_message_tapos_processor },
1: { decoder: elpp.antelope_message_action_decoder, processor: antelope_message_action_processor },
2: { decoder: elpp.antelope_message_serialized_action_decoder, processor: antelope_message_serialized_action_processor },
3: { decoder: elpp.antelope_message_signature_decoder, processor: antelope_message_signature_processor },
4: { decoder: elpp.antelope_message_tapos_req_decoder, processor: antelope_message_tapos_req_processor },
}
/* The antenlope decoder returns:
* trx : {} This contains the complete JSON transaction (in json property) structure decoded from uplinked data.
* It can be pushed to the v1 chain APIs.
* tapos_req : { chain } If present this means a tapos request was decoded. Contains a 'chain' property.
*/
function decoder(payload, state) {
/* Because the Antelope decoder state must persist across invocations of 'decoder()' we
* must setup the decoder's transient state object with our persistent decoder state.
*/
var platform = {
/* Setup the object in a platform specific way */
pre_process: function (obj) {
obj.state = state /* persistent state */
},
/* Post process and return the data specific to the platform */
post_process: function (obj) {
return obj
}
}
return elpp.decoder(payload, channel_map, platform)
}
function get_status_trx(trx) {
let str = ''
if (!trx.signature) {
str += ' needs'
} else {
str += ' has'
}
str += ' signature'
if (!trx.tapos) {
str += ' needs'
} else {
str += ' has'
}
str += ' tapos'
if (!trx.action) {
str += ' needs'
} else {
str += ' has'
}
str += ' action'
if (!trx.data) {
str += ' needs'
} else {
str += ' has'
}
str += ' data'
return str
}
function get_status(trx_map) {
/* print all active IDs and what they are waiting for */
let str = ''
for (i in trx_map) {
let trx = trx_map[i]
str += 'trx ' + i
str += get_status_trx(trx) + '\n'
}
return str
}
function new_state() {
return {
trx_map: {}
}
}
module.exports = {
decoder,
get_status,
new_state
}