nimbus-eth1/nimbus/rpc/hexstrings.nim

334 lines
11 KiB
Nim
Raw Normal View History

2018-07-27 17:02:02 +00:00
# Nimbus
# Copyright (c) 2018 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
## This module implements the Ethereum hexadecimal string formats for JSON
## See: https://github.com/ethereum/wiki/wiki/JSON-RPC#hex-value-encoding
#[
Note:
The following types are converted to hex strings when marshalled to JSON:
* EthAddress
* ref EthAddress
* Hash256
* UInt256
* seq[byte]
* openArray[seq]
* ref BloomFilter
2019-03-23 20:54:28 +00:00
* PublicKey
* PrivateKey
* Topic
* Bytes
]#
2019-03-23 20:54:28 +00:00
import
std/strutils,
stint, stew/byteutils, eth/[keys, rlp],
eth/common/eth_types,
json_serialization
2018-07-27 17:02:02 +00:00
type
HexQuantityStr* = distinct string
HexDataStr* = distinct string
EthAddressStr* = distinct string # Same as HexDataStr but must be less <= 20 bytes
EthHashStr* = distinct string # Same as HexDataStr but must be exactly 32 bytes
HexStrings = HexQuantityStr | HexDataStr | EthAddressStr | EthHashStr
2018-08-13 16:33:57 +00:00
2018-08-24 13:20:57 +00:00
template len*(value: HexStrings): int = value.string.len
2018-07-27 17:02:02 +00:00
# Hex validation
template stripLeadingZeros(value: string): string =
var cidx = 0
# ignore the last character so we retain '0' on zero value
while cidx < value.len - 1 and value[cidx] == '0':
cidx.inc
value[cidx .. ^1]
func encodeQuantity*(value: SomeUnsignedInt): HexQuantityStr {.inline.} =
2018-07-27 17:02:02 +00:00
var hValue = value.toHex.stripLeadingZeros
result = HexQuantityStr("0x" & hValue)
func encodeQuantity*(value: UInt256): HexQuantityStr {.inline.} =
var hValue = value.toHex
result = HexQuantityStr("0x" & hValue)
2018-07-27 17:02:02 +00:00
template hasHexHeader(value: string): bool =
if value.len >= 2 and value[0] == '0' and value[1] in {'x', 'X'}: true
2018-07-27 17:02:02 +00:00
else: false
template isHexChar(c: char): bool =
2018-07-27 17:02:02 +00:00
if c notin {'0'..'9'} and
c notin {'a'..'f'} and
c notin {'A'..'F'}: false
else: true
func `==`*(a, b: HexQuantityStr): bool {.inline.} =
a.string == b.string
func `==`*(a, b: EthAddressStr): bool {.inline.} =
a.string == b.string
func `==`*(a, b: HexDataStr): bool {.inline.} =
a.string == b.string
func isValidHexQuantity*(value: string): bool =
if not value.hasHexHeader:
2018-07-27 17:02:02 +00:00
return false
# No leading zeros (but allow 0x0)
if value.len < 3 or (value.len > 3 and value[2] == '0'): return false
2018-07-27 17:02:02 +00:00
for i in 2 ..< value.len:
let c = value[i]
if not c.isHexChar:
return false
return true
2019-03-23 20:54:28 +00:00
func isValidHexData*(value: string, header = true): bool =
if header and not value.hasHexHeader:
2018-07-27 17:02:02 +00:00
return false
# Must be even number of digits
if value.len mod 2 != 0: return false
# Leading zeros are allowed
for i in 2 ..< value.len:
let c = value[i]
if not c.isHexChar:
return false
return true
template isValidHexData(value: string, hexLen: int, header = true): bool =
value.len == hexLen and value.isValidHexData(header)
2018-08-13 16:33:57 +00:00
func isValidEthAddress*(value: string): bool =
# 20 bytes for EthAddress plus "0x"
2018-08-13 17:39:17 +00:00
# Addresses are allowed to be shorter than 20 bytes for convenience
2018-08-13 16:33:57 +00:00
result = value.len <= 42 and value.isValidHexData
2018-08-13 17:39:17 +00:00
func isValidEthHash*(value: string): bool =
# 32 bytes for EthAddress plus "0x"
# Currently hashes are required to be exact lengths
# TODO: Allow shorter hashes (pad with zeros) for convenience?
result = value.isValidHexData(66)
2018-08-13 17:39:17 +00:00
2019-03-23 20:54:28 +00:00
func isValidPublicKey*(value: string): bool =
# 65 bytes for Public Key plus 1 byte for 0x prefix
result = value.isValidHexData(132)
2019-03-23 20:54:28 +00:00
func isValidPrivateKey*(value: string): bool =
# 32 bytes for Private Key plus 1 byte for 0x prefix
result = value.isValidHexData(66)
2019-03-23 20:54:28 +00:00
func isValidHash256*(value: string): bool =
# 32 bytes for Hash256 plus 1 byte for 0x prefix
result = value.isValidHexData(66)
2019-03-23 20:54:28 +00:00
func isValidTopic*(value: string): bool =
# 4 bytes for Topic plus 1 byte for 0x prefix
result = value.isValidHexData(10)
const
SInvalidQuantity = "Invalid hex quantity format for Ethereum"
SInvalidData = "Invalid hex data format for Ethereum"
2018-08-13 16:33:57 +00:00
SInvalidAddress = "Invalid address format for Ethereum"
2018-08-13 17:39:17 +00:00
SInvalidHash = "Invalid hash format for Ethereum"
proc validateHexQuantity*(value: string) {.inline.} =
if unlikely(not value.isValidHexQuantity):
raise newException(ValueError, SInvalidQuantity & ": " & value)
proc validateHexData*(value: string) {.inline.} =
if unlikely(not value.isValidHexData):
raise newException(ValueError, SInvalidData & ": " & value)
2018-08-13 16:33:57 +00:00
proc validateHexAddressStr*(value: string) {.inline.} =
if unlikely(not value.isValidEthAddress):
raise newException(ValueError, SInvalidAddress & ": " & value)
2018-08-13 17:39:17 +00:00
proc validateHashStr*(value: string) {.inline.} =
if unlikely(not value.isValidEthHash):
raise newException(ValueError, SInvalidHash & ": " & value)
proc getEthAddress*(value: string): Option[EthAddress] =
if value.isValidEthAddress:
var ethAddress: EthAddress
hexToByteArray(value, ethAddress)
return some(ethAddress)
else:
return none[EthAddress]()
proc getHash256*(value: string): Option[Hash256] =
if value.isValidHash256:
var hash: Hash256
hexToByteArray(value, hash.data)
return some(hash)
else:
return none[Hash256]()
2018-07-27 17:02:02 +00:00
# Initialisation
proc hexQuantityStr*(value: string): HexQuantityStr {.inline.} =
value.validateHexQuantity
result = value.HexQuantityStr
2018-07-27 17:02:02 +00:00
proc hexDataStr*(value: string): HexDataStr {.inline.} =
value.validateHexData
result = value.HexDataStr
2018-07-27 17:02:02 +00:00
proc hexDataStr*(value: openArray[byte]): HexDataStr {.inline.} =
result = HexDataStr("0x" & value.toHex)
2022-04-08 04:54:11 +00:00
proc hexDataStr*(value: UInt256): HexDataStr {.inline.} =
result = HexDataStr("0x" & toBytesBE(value).toHex)
2018-08-13 16:33:57 +00:00
proc ethAddressStr*(value: string): EthAddressStr {.inline.} =
value.validateHexAddressStr
result = value.EthAddressStr
func ethAddressStr*(x: EthAddress): EthAddressStr {.inline.} =
result = EthAddressStr("0x" & toHex(x))
2018-08-13 17:39:17 +00:00
proc ethHashStr*(value: string): EthHashStr {.inline.} =
value.validateHashStr
result = value.EthHashStr
func ethHashStr*(value: Hash256): EthHashStr {.inline.} =
result = EthHashStr("0x" & value.data.toHex)
2018-07-27 17:02:02 +00:00
# Converters for use in RPC
import json
from json_rpc/rpcserver import expect
2018-08-21 21:21:30 +00:00
proc `%`*(value: HexStrings): JsonNode =
result = %(value.string)
# Overloads to support expected representation of hex data
proc `%`*(value: EthAddress): JsonNode =
result = %("0x" & value.toHex)
2018-08-16 16:41:40 +00:00
proc `%`*(value: ref EthAddress): JsonNode =
result = %("0x" & value[].toHex)
proc `%`*(value: Hash256): JsonNode =
2019-03-23 20:54:28 +00:00
#result = %("0x" & $value) # More clean but no lowercase :(
result = %("0x" & value.data.toHex)
proc `%`*(value: UInt256): JsonNode =
result = %("0x" & value.toString(16))
2018-08-15 13:07:06 +00:00
proc `%`*(value: ref BloomFilter): JsonNode =
result = %("0x" & toHex[256](value[]))
2019-03-23 20:54:28 +00:00
proc `%`*(value: PublicKey): JsonNode =
result = %("0x04" & $value)
proc `%`*(value: PrivateKey): JsonNode =
result = %("0x" & $value)
proc `%`*(value: seq[byte]): JsonNode =
2019-03-23 20:54:28 +00:00
result = %("0x" & value.toHex)
# Helpers for the fromJson procs
proc toPublicKey*(key: string): PublicKey {.inline.} =
result = PublicKey.fromHex(key[4 .. ^1]).tryGet()
proc toPrivateKey*(key: string): PrivateKey {.inline.} =
result = PrivateKey.fromHex(key[2 .. ^1]).tryGet()
2018-08-15 13:07:06 +00:00
# Marshalling from JSON to Nim types that includes format checking
func invalidMsg(name: string): string = "When marshalling from JSON, parameter \"" & name & "\" is not valid"
2018-07-27 17:02:02 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var HexQuantityStr) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHexQuantity:
raise newException(ValueError, invalidMsg(argName) & " as an Ethereum hex quantity \"" & hexStr & "\"")
2018-07-31 16:46:40 +00:00
result = hexStr.hexQuantityStr
2018-07-27 17:02:02 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var HexDataStr) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHexData:
raise newException(ValueError, invalidMsg(argName) & " as Ethereum data \"" & hexStr & "\"")
2018-07-31 16:46:40 +00:00
result = hexStr.hexDataStr
2018-07-27 17:02:02 +00:00
2018-08-13 16:33:57 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var EthAddressStr) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidEthAddress:
raise newException(ValueError, invalidMsg(argName) & "\" as an Ethereum address \"" & hexStr & "\"")
2018-08-13 16:33:57 +00:00
result = hexStr.EthAddressStr
2020-07-30 07:21:11 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var EthAddress) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidEthAddress:
raise newException(ValueError, invalidMsg(argName) & "\" as an Ethereum address \"" & hexStr & "\"")
hexToByteArray(hexStr, result)
2018-08-13 17:39:17 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var EthHashStr) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidEthHash:
raise newException(ValueError, invalidMsg(argName) & " as an Ethereum hash \"" & hexStr & "\"")
2018-08-13 17:39:17 +00:00
result = hexStr.EthHashStr
2018-11-23 17:21:03 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var UInt256) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
2019-06-19 19:10:01 +00:00
if not (hexStr.len <= 66 and hexStr.isValidHexQuantity):
raise newException(ValueError, invalidMsg(argName) & " as a UInt256 \"" & hexStr & "\"")
2018-11-23 17:21:03 +00:00
result = readUintBE[256](hexToPaddedByteArray[32](hexStr))
proc fromJson*(n: JsonNode, argName: string, result: var PublicKey) =
2019-03-23 20:54:28 +00:00
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidPublicKey:
raise newException(ValueError, invalidMsg(argName) & " as a public key \"" & hexStr & "\"")
result = hexStr.toPublicKey
2019-03-23 20:54:28 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var PrivateKey) =
2019-03-23 20:54:28 +00:00
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidPrivateKey:
raise newException(ValueError, invalidMsg(argName) & " as a private key \"" & hexStr & "\"")
result = hexStr.toPrivateKey
2019-03-23 20:54:28 +00:00
# Following procs currently required only for testing, the `createRpcSigs` macro
# requires it as it will convert the JSON results back to the original Nim
# types, but it needs the `fromJson` calls for those specific Nim types to do so
proc fromJson*(n: JsonNode, argName: string, result: var seq[byte]) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHexData:
raise newException(ValueError, invalidMsg(argName) & " as a hex data \"" & hexStr & "\"")
2019-11-13 14:49:39 +00:00
result = hexToSeqByte(hexStr)
proc fromJson*(n: JsonNode, argName: string, result: var Hash256) =
n.kind.expect(JString, argName)
let hexStr = n.getStr()
if not hexStr.isValidHash256:
raise newException(ValueError, invalidMsg(argName) & " as a Hash256 \"" & hexStr & "\"")
2019-11-13 14:49:39 +00:00
hexToByteArray(hexStr, result.data)
2020-07-30 07:21:11 +00:00
proc fromJson*(n: JsonNode, argName: string, result: var JsonNode) =
result = n
proc writeValue*(writer: var JsonWriter, value: HexQuantityStr) =
writeValue(writer, string value)
proc writeValue*(writer: var JsonWriter, value: HexDataStr) =
writeValue(writer, string value)
proc writeValue*(writer: var JsonWriter, value: EthAddressStr) =
writeValue(writer, string value)
proc writeValue*(writer: var JsonWriter, value: EthHashStr) =
writeValue(writer, string value)