forked from status-im/nim-eth
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathrlp.nim
481 lines (370 loc) · 13.5 KB
/
rlp.nim
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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
## This module implements RLP encoding and decoding as
## defined in Appendix B of the Ethereum Yellow Paper:
## https://ethereum.github.io/yellowpaper/paper.pdf
import
std/[macros, strutils],
stew/byteutils,
./rlp/[writer, object_serialization],
./rlp/priv/defs
from stew/objects import checkedEnumAssign
export
writer, object_serialization
type
Rlp* = object
bytes: seq[byte]
position*: int
RlpNodeType* = enum
rlpBlob
rlpList
RlpNode* = object
case kind*: RlpNodeType
of rlpBlob:
bytes*: seq[byte]
of rlpList:
elems*: seq[RlpNode]
RlpError* = object of CatchableError
MalformedRlpError* = object of RlpError
UnsupportedRlpError* = object of RlpError
RlpTypeMismatch* = object of RlpError
proc rlpFromBytes*(data: seq[byte]): Rlp =
Rlp(bytes: data, position: 0)
proc rlpFromBytes*(data: openArray[byte]): Rlp =
rlpFromBytes(@data)
const zeroBytesRlp* = Rlp()
proc rlpFromHex*(input: string): Rlp =
rlpFromBytes(hexToSeqByte(input))
proc hasData*(self: Rlp): bool =
self.position < self.bytes.len
proc currentElemEnd*(self: Rlp): int {.gcsafe.}
template rawData*(self: Rlp): openArray[byte] =
self.bytes.toOpenArray(self.position, self.currentElemEnd - 1)
proc isBlob*(self: Rlp): bool =
self.hasData() and self.bytes[self.position] < LIST_START_MARKER
proc isEmpty*(self: Rlp): bool =
### Contains a blob or a list of zero length
self.hasData() and (self.bytes[self.position] == BLOB_START_MARKER or
self.bytes[self.position] == LIST_START_MARKER)
proc isList*(self: Rlp): bool =
self.hasData() and self.bytes[self.position] >= LIST_START_MARKER
template eosError =
raise newException(MalformedRlpError, "Read past the end of the RLP stream")
template requireData {.dirty.} =
if not self.hasData():
raise newException(MalformedRlpError, "Illegal operation over an empty RLP stream")
proc getType*(self: Rlp): RlpNodeType =
requireData()
return if self.isBlob(): rlpBlob else: rlpList
proc lengthBytesCount(self: Rlp): int =
var marker = self.bytes[self.position]
if self.isBlob() and marker > LEN_PREFIXED_BLOB_MARKER:
return int(marker - LEN_PREFIXED_BLOB_MARKER)
if self.isList() and marker > LEN_PREFIXED_LIST_MARKER:
return int(marker - LEN_PREFIXED_LIST_MARKER)
return 0
proc isSingleByte*(self: Rlp): bool =
self.hasData() and self.bytes[self.position] < BLOB_START_MARKER
proc getByteValue*(self: Rlp): byte =
doAssert self.isSingleByte()
return self.bytes[self.position]
proc payloadOffset(self: Rlp): int =
if self.isSingleByte(): 0 else: 1 + self.lengthBytesCount()
template readAheadCheck(numberOfBytes: int) =
# important to add nothing to the left side of the equation as `numberOfBytes`
# can in theory be at max size of its type already
if numberOfBytes > self.bytes.len - self.position - self.payloadOffset():
eosError()
template nonCanonicalNumberError =
raise newException(MalformedRlpError, "Small number encoded in a non-canonical way")
proc payloadBytesCount(self: Rlp): int =
if not self.hasData():
return 0
var marker = self.bytes[self.position]
if marker < BLOB_START_MARKER:
return 1
if marker <= LEN_PREFIXED_BLOB_MARKER:
result = int(marker - BLOB_START_MARKER)
readAheadCheck(result)
if result == 1:
if self.bytes[self.position + 1] < BLOB_START_MARKER:
nonCanonicalNumberError()
return
template readInt(startMarker, lenPrefixMarker) =
var
lengthBytes = int(marker - lenPrefixMarker)
remainingBytes = self.bytes.len - self.position
if remainingBytes <= lengthBytes:
eosError()
if remainingBytes > 1 and self.bytes[self.position + 1] == 0:
raise newException(MalformedRlpError, "Number encoded with a leading zero")
# check if the size is not bigger than the max that result can hold
if lengthBytes > sizeof(result) or
(lengthBytes == sizeof(result) and self.bytes[self.position + 1].int > 127):
raise newException(UnsupportedRlpError, "Message too large to fit in memory")
for i in 1 .. lengthBytes:
result = (result shl 8) or int(self.bytes[self.position + i])
# must be greater than the short-list size list
if result < THRESHOLD_LIST_LEN:
nonCanonicalNumberError()
if marker < LIST_START_MARKER:
readInt(BLOB_START_MARKER, LEN_PREFIXED_BLOB_MARKER)
elif marker <= LEN_PREFIXED_LIST_MARKER:
result = int(marker - LIST_START_MARKER)
else:
readInt(LIST_START_MARKER, LEN_PREFIXED_LIST_MARKER)
readAheadCheck(result)
proc blobLen*(self: Rlp): int =
if self.isBlob(): self.payloadBytesCount() else: 0
proc isInt*(self: Rlp): bool =
if not self.hasData():
return false
var marker = self.bytes[self.position]
if marker < BLOB_START_MARKER:
return marker != 0
if marker == BLOB_START_MARKER:
return true
if marker <= LEN_PREFIXED_BLOB_MARKER:
return self.bytes[self.position + 1] != 0
if marker < LIST_START_MARKER:
let offset = self.position + int(marker + 1 - LEN_PREFIXED_BLOB_MARKER)
if offset >= self.bytes.len: eosError()
return self.bytes[offset] != 0
return false
template maxBytes*(o: type[Ordinal | uint64 | uint]): int = sizeof(o)
proc toInt*(self: Rlp, IntType: type): IntType =
# XXX: work-around a Nim issue with type parameters
type OutputType = IntType
mixin maxBytes, to
# XXX: self insertions are not working in generic procs
# https://github.com/nim-lang/Nim/issues/5053
if not self.hasData():
raise newException(RlpTypeMismatch, "Attempt to read an Int value past the RLP end")
if self.isList():
raise newException(RlpTypeMismatch, "Int expected, but found a List")
let
payloadStart = self.payloadOffset()
payloadSize = self.payloadBytesCount()
if payloadSize > maxBytes(IntType):
raise newException(RlpTypeMismatch, "The RLP contains a larger than expected Int value")
for i in payloadStart ..< (payloadStart + payloadSize):
result = (result shl 8) or OutputType(self.bytes[self.position + i])
proc toString*(self: Rlp): string =
if not self.isBlob():
raise newException(RlpTypeMismatch, "String expected, but the source RLP is not a blob")
let
payloadOffset = self.payloadOffset()
payloadLen = self.payloadBytesCount()
result = newString(payloadLen)
for i in 0 ..< payloadLen:
# XXX: switch to copyMem here
result[i] = char(self.bytes[self.position + payloadOffset + i])
proc toBytes*(self: Rlp): seq[byte] =
if not self.isBlob():
raise newException(RlpTypeMismatch,
"Bytes expected, but the source RLP in not a blob")
let payloadLen = self.payloadBytesCount()
if payloadLen > 0:
let
payloadOffset = self.payloadOffset()
ibegin = self.position + payloadOffset
iend = ibegin + payloadLen - 1
result = self.bytes[ibegin..iend]
proc currentElemEnd*(self: Rlp): int =
doAssert self.hasData()
result = self.position
if self.isSingleByte():
result += 1
elif self.isBlob() or self.isList():
result += self.payloadOffset() + self.payloadBytesCount()
proc enterList*(self: var Rlp): bool =
if not self.isList():
return false
self.position += self.payloadOffset()
return true
proc tryEnterList*(self: var Rlp) =
if not self.enterList():
raise newException(RlpTypeMismatch, "List expected, but source RLP is not a list")
proc skipElem*(rlp: var Rlp) =
rlp.position = rlp.currentElemEnd
iterator items*(self: var Rlp): var Rlp =
doAssert self.isList()
var
payloadOffset = self.payloadOffset()
payloadEnd = self.position + payloadOffset + self.payloadBytesCount()
if payloadEnd > self.bytes.len:
raise newException(MalformedRlpError, "List length extends past the end of the stream")
self.position += payloadOffset
while self.position < payloadEnd:
let elemEnd = self.currentElemEnd()
yield self
self.position = elemEnd
proc listElem*(self: Rlp, i: int): Rlp =
doAssert self.isList()
let
payloadOffset = self.payloadOffset()
# This will only check if there is some data, not if it is correct according
# to list length. Could also run here payloadBytesCount() instead.
if self.position + payloadOffset + 1 > self.bytes.len: eosError()
let payload = self.bytes[self.position + payloadOffset..^1]
result = rlpFromBytes payload
var pos = 0
while pos < i and result.hasData:
result.position = result.currentElemEnd()
inc pos
proc listLen*(self: Rlp): int =
if not self.isList():
return 0
var rlp = self
for elem in rlp:
inc result
proc readImpl(rlp: var Rlp, T: type string): string =
result = rlp.toString
rlp.skipElem
proc readImpl(rlp: var Rlp, T: type Integer): Integer =
result = rlp.toInt(T)
rlp.skipElem
proc readImpl(rlp: var Rlp, T: type[enum]): T =
let value = rlp.toInt(int)
var res: T
if not checkedEnumAssign(res, value):
raise newException(RlpTypeMismatch,
"Enum value expected, but the source RLP is not in valid range.")
rlp.skipElem
res
proc readImpl(rlp: var Rlp, T: type bool): T =
result = rlp.toInt(int) != 0
rlp.skipElem
proc readImpl(rlp: var Rlp, T: type float64): T =
# This is not covered in the RLP spec, but Geth uses Go's
# `math.Float64bits`, which is defined here:
# https://github.com/gopherjs/gopherjs/blob/master/compiler/natives/src/math/math.go
let uint64bits = rlp.toInt(uint64)
var uint32parts = [uint32(uint64bits), uint32(uint64bits shr 32)]
return cast[ptr float64](unsafeAddr uint32parts)[]
proc readImpl[R, E](rlp: var Rlp, T: type array[R, E]): T =
mixin read
when E is (byte or char):
if not rlp.isBlob:
raise newException(RlpTypeMismatch, "Bytes array expected, but the source RLP is not a blob.")
var bytes = rlp.toBytes
if result.len != bytes.len:
raise newException(RlpTypeMismatch, "Fixed-size array expected, but the source RLP contains a blob of different length")
copyMem(addr result[0], unsafeAddr bytes[0], bytes.len)
rlp.skipElem
else:
if not rlp.isList:
raise newException(RlpTypeMismatch, "List expected, but the source RLP is not a list.")
if result.len != rlp.listLen:
raise newException(RlpTypeMismatch, "Fixed-size array expected, but the source RLP contains a list of different length")
var i = 0
for elem in rlp:
result[i] = rlp.read(E)
inc i
proc readImpl[E](rlp: var Rlp, T: type seq[E]): T =
mixin read
when E is (byte or char):
result = rlp.toBytes
rlp.skipElem
else:
if not rlp.isList:
raise newException(RlpTypeMismatch, "Sequence expected, but the source RLP is not a list.")
result = newSeqOfCap[E](rlp.listLen)
for elem in rlp:
result.add rlp.read(E)
proc readImpl[E](rlp: var Rlp, T: type openArray[E]): seq[E] =
result = readImpl(rlp, seq[E])
proc readImpl(rlp: var Rlp, T: type[object|tuple],
wrappedInList = wrapObjsInList): T =
mixin enumerateRlpFields, read
if wrappedInList:
if not rlp.isList:
raise newException(RlpTypeMismatch,
"List expected, but the source RLP is not a list.")
var
payloadOffset = rlp.payloadOffset()
# there's an exception-raising side effect in there *sigh*
discard rlp.payloadBytesCount()
rlp.position += payloadOffset
template op(field) =
when hasCustomPragma(field, rlpCustomSerialization):
field = rlp.read(result, type(field))
else:
field = rlp.read(type(field))
enumerateRlpFields(result, op)
proc toNodes*(self: var Rlp): RlpNode =
requireData()
if self.isList():
result.kind = rlpList
newSeq result.elems, 0
for e in self:
result.elems.add e.toNodes
else:
doAssert self.isBlob()
result.kind = rlpBlob
result.bytes = self.toBytes()
self.position = self.currentElemEnd()
# We define a single `read` template with a pretty low specificity
# score in order to facilitate easier overloading with user types:
template read*(rlp: var Rlp, T: type): auto =
readImpl(rlp, T)
proc `>>`*[T](rlp: var Rlp, location: var T) =
mixin read
location = rlp.read(T)
template readRecordType*(rlp: var Rlp, T: type, wrappedInList: bool): auto =
readImpl(rlp, T, wrappedInList)
proc decode*(bytes: openArray[byte]): RlpNode =
var rlp = rlpFromBytes(bytes)
rlp.toNodes
template decode*(bytes: openArray[byte], T: type): untyped =
mixin read
var rlp = rlpFromBytes(bytes)
rlp.read(T)
template decode*(bytes: seq[byte], T: type): untyped =
mixin read
var rlp = rlpFromBytes(bytes)
rlp.read(T)
proc append*(writer: var RlpWriter; rlp: Rlp) =
appendRawBytes(writer, rlp.rawData)
proc isPrintable(s: string): bool =
for c in s:
if ord(c) < 32 or ord(c) >= 128:
return false
return true
proc inspectAux(self: var Rlp, depth: int, hexOutput: bool, output: var string) =
if not self.hasData():
return
template indent =
for i in 0..<depth:
output.add " "
indent()
if self.isSingleByte:
output.add "byte "
output.add $self.bytes[self.position]
elif self.isBlob:
let str = self.toString
if str.isPrintable:
output.add '"'
output.add str
output.add '"'
else:
output.add "blob(" & $str.len & ") ["
for c in str:
if hexOutput:
output.add toHex(int(c), 2)
else:
output.add $ord(c)
output.add ","
if hexOutput:
output.add ']'
else:
output[^1] = ']'
else:
output.add "{\n"
for subitem in self:
inspectAux(subitem, depth + 1, hexOutput, output)
output.add "\n"
indent()
output.add "}"
proc inspect*(self: Rlp, indent = 0, hexOutput = true): string =
var rlpCopy = self
result = newStringOfCap(self.bytes.len)
inspectAux(rlpCopy, indent, hexOutput, result)