nimbus-eth1/wrappers/libnimbus.nim

485 lines
16 KiB
Nim
Raw Normal View History

#
2019-09-12 16:32:07 +00:00
# Nimbus
2019-11-07 11:09:30 +00:00
# (c) Copyright 2019
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import
chronos, chronicles, nimcrypto/[utils, hmac, pbkdf2, hash, sysrand], tables,
stew/ranges/ptr_arith, eth/[keys, rlp, p2p, async_utils],
eth/p2p/rlpx_protocols/whisper_protocol,
2019-11-07 11:09:30 +00:00
eth/p2p/[peer_pool, bootnodes, whispernodes]
2019-09-12 16:32:07 +00:00
const idLen = 32
2019-09-12 16:32:07 +00:00
# TODO: If we really want/need this type of API for the keys, put it somewhere
# seperate as it is the same code for Whisper RPC
type
WhisperKeys* = ref object
asymKeys*: Table[string, KeyPair]
symKeys*: Table[string, SymKey]
Identifier = array[idLen, byte]
proc newWhisperKeys(): WhisperKeys =
2019-09-12 16:32:07 +00:00
new(result)
result.asymKeys = initTable[string, KeyPair]()
result.symKeys = initTable[string, SymKey]()
proc generateRandomID(): Identifier =
while true: # TODO: error instead of looping?
if randomBytes(result) == idLen:
break
2019-11-07 11:09:30 +00:00
# TODO: again, lots of overlap with Nimbus Whisper RPC here, however not all
# the same due to type conversion (no use of Option and such). Perhaps some
# parts can be refactored in sharing some of the code.
type
CReceivedMessage* = object
decoded*: ptr byte
decodedLen*: csize
source*: ptr byte
recipientPublicKey*: ptr byte
timestamp*: uint32
ttl*: uint32
topic*: Topic
pow*: float64
hash*: Hash
2019-09-12 16:32:07 +00:00
CFilterOptions* = object
symKeyID*: ptr byte
privateKeyID*: ptr byte
2019-11-04 16:51:59 +00:00
source*: ptr byte
2019-09-12 16:32:07 +00:00
minPow*: float64
2019-11-07 11:09:30 +00:00
topic*: Topic # lets go with one topic for now unless more are required
2019-10-25 15:31:42 +00:00
allowP2P*: bool
2019-09-12 16:32:07 +00:00
CPostMessage* = object
symKeyID*: ptr byte
2019-11-04 16:51:59 +00:00
pubKey*: ptr byte
sourceID*: ptr byte
2019-09-12 16:32:07 +00:00
ttl*: uint32
topic*: Topic
payload*: ptr byte
payloadLen*: csize
padding*: ptr byte
paddingLen*: csize
2019-09-12 16:32:07 +00:00
powTime*: float64
powTarget*: float64
CTopic* = object
topic*: Topic
# Don't do this at home, you'll never get rid of ugly globals like this!
var
node: EthereumNode
2019-09-12 16:32:07 +00:00
# You will only add more instead!
let whisperKeys = newWhisperKeys()
proc setBootNodes(nodes: openArray[string]): seq[ENode] =
var bootnode: ENode
result = newSeqOfCap[ENode](nodes.len)
for nodeId in nodes:
# For now we can just do assert as we only pass our own const arrays.
doAssert(initENode(nodeId, bootnode) == ENodeStatus.Success)
result.add(bootnode)
proc connectToNodes(nodes: openArray[string]) =
for nodeId in nodes:
var whisperENode: ENode
# For now we can just do assert as we only pass our own const arrays.
doAssert(initENode(nodeId, whisperENode) == ENodeStatus.Success)
traceAsyncErrors node.peerPool.connectToNode(newNode(whisperENode))
2019-11-07 11:09:30 +00:00
# Setting up the node
2019-11-04 12:56:24 +00:00
proc nimbus_start(port: uint16, startListening: bool, enableDiscovery: bool,
minPow: float64, privateKey: ptr byte, staging: bool): bool {.exportc.} =
# TODO: any async calls can still create `Exception`, why?
let address = Address(
udpPort: port.Port, tcpPort: port.Port, ip: parseIpAddress("0.0.0.0"))
var keypair: KeyPair
if privateKey.isNil:
keypair = newKeyPair()
else:
try:
let privKey = initPrivateKey(makeOpenArray(privateKey, 32))
keypair = KeyPair(seckey: privKey, pubkey: privKey.getPublicKey())
except EthKeysException:
2019-11-07 11:09:30 +00:00
error "Passed an invalid private key."
return false
node = newEthereumNode(keypair, address, 1, nil, addAllCapabilities = false)
node.addCapability Whisper
2019-11-04 12:56:24 +00:00
node.protocolState(Whisper).config.powRequirement = minPow
# TODO: should we start the node with an empty bloomfilter?
# var bloom: Bloom
# node.protocolState(Whisper).config.bloom = bloom
let bootnodes = if staging: setBootNodes(StatusBootNodesStaging)
else: setBootNodes(StatusBootNodes)
traceAsyncErrors node.connectToNetwork(bootnodes, startListening,
enableDiscovery)
# Connect to known Status Whisper fleet directly
if staging: connectToNodes(WhisperNodesStaging)
else: connectToNodes(WhisperNodes)
result = true
proc nimbus_poll() {.exportc.} =
poll()
proc nimbus_add_peer(nodeId: cstring): bool {.exportc.} =
var
whisperENode: ENode
whisperNode: Node
discard initENode($nodeId, whisperENode)
try:
whisperNode = newNode(whisperENode)
except Secp256k1Exception:
return false
# TODO: call can create `Exception`, why?
2019-11-04 12:56:24 +00:00
traceAsyncErrors node.peerPool.connectToNode(whisperNode)
result = true
2019-09-12 16:32:07 +00:00
2019-11-07 11:09:30 +00:00
# Whisper API (Similar to Whisper JSON-RPC API)
2019-09-12 16:32:07 +00:00
proc nimbus_channel_to_topic(channel: cstring): CTopic {.exportc, raises: [].} =
2019-11-07 11:09:30 +00:00
# Only used for the example, to conveniently convert channel to topic.
doAssert(not channel.isNil, "Channel cannot be nil.")
2019-11-04 16:51:59 +00:00
let hash = digest(keccak256, $channel)
2019-09-12 16:32:07 +00:00
for i in 0..<4:
result.topic[i] = hash.data[i]
2019-11-07 11:09:30 +00:00
# Asymmetric Keys
2019-09-12 17:27:43 +00:00
proc nimbus_new_keypair(id: var Identifier): bool {.exportc, raises: [].} =
## Caller needs to provide as id a pointer to 32 bytes allocation.
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
id = generateRandomID()
try:
whisperKeys.asymKeys.add(id.toHex(), newKeyPair())
result = true
except Secp256k1Exception:
# Don't think this can actually happen, comes from the `getPublicKey` part
# in `newKeyPair`
discard
2019-09-12 16:32:07 +00:00
proc nimbus_add_keypair(id: var Identifier, privateKey: ptr byte):
bool {.exportc, raises: [OSError, IOError, ValueError].} =
## Caller needs to provide as id a pointer to 32 bytes allocation.
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-07 11:09:30 +00:00
doAssert(not privateKey.isNil, "Private key cannot be nil.")
2019-09-12 17:27:43 +00:00
var keypair: KeyPair
try:
keypair.seckey = initPrivateKey(makeOpenArray(privateKey, 32))
keypair.pubkey = keypair.seckey.getPublicKey()
except EthKeysException, Secp256k1Exception:
2019-11-07 11:09:30 +00:00
error "Passed an invalid private key."
return false
result = true
id = generateRandomID()
whisperKeys.asymKeys.add(id.toHex(), keypair)
2019-09-12 16:32:07 +00:00
proc nimbus_delete_keypair(id: Identifier): bool {.exportc, raises: [].} =
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-04 16:51:59 +00:00
var unneeded: KeyPair
result = whisperKeys.asymKeys.take(id.toHex(), unneeded)
2019-09-12 16:32:07 +00:00
2019-11-13 10:49:35 +00:00
proc nimbus_get_private_key(id: Identifier, privateKey: var PrivateKey):
bool {.exportc, raises: [OSError, IOError, ValueError].} =
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-13 10:49:35 +00:00
doAssert(not (unsafeAddr privateKey).isNil, "Private key cannot be nil.")
try:
2019-11-13 10:49:35 +00:00
privateKey = whisperKeys.asymkeys[id.toHex()].seckey
result = true
except KeyError:
2019-11-07 11:09:30 +00:00
error "Private key not found."
2019-11-07 11:09:30 +00:00
# Symmetric Keys
proc nimbus_add_symkey(id: var Identifier, symKey: ptr SymKey): bool
{.exportc, raises: [].} =
## Caller needs to provide as id a pointer to 32 bytes allocation.
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-07 11:09:30 +00:00
doAssert(not symKey.isNil, "Symmetric key cannot be nil.")
id = generateRandomID()
result = true
# Copy of key happens at add
whisperKeys.symKeys.add(id.toHex, symKey[])
proc nimbus_add_symkey_from_password(id: var Identifier, password: cstring):
bool {.exportc, raises: [].} =
## Caller needs to provide as id a pointer to 32 bytes allocation.
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-07 11:09:30 +00:00
doAssert(not password.isNil, "Password cannot be nil.")
2019-11-04 16:51:59 +00:00
2019-09-12 16:32:07 +00:00
var ctx: HMAC[sha256]
var symKey: SymKey
if pbkdf2(ctx, $password, "", 65356, symKey) != sizeof(SymKey):
return false
2019-09-12 16:32:07 +00:00
id = generateRandomID()
result = true
2019-09-12 16:32:07 +00:00
whisperKeys.symKeys.add(id.toHex(), symKey)
2019-09-12 16:32:07 +00:00
proc nimbus_delete_symkey(id: Identifier): bool {.exportc, raises: [].} =
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-04 16:51:59 +00:00
var unneeded: SymKey
result = whisperKeys.symKeys.take(id.toHex(), unneeded)
2019-11-13 10:49:35 +00:00
proc nimbus_get_symkey(id: Identifier, symKey: var SymKey):
bool {.exportc, raises: [OSError, IOError, ValueError].} =
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-13 10:49:35 +00:00
doAssert(not (unsafeAddr symKey).isNil, "Symmetric key cannot be nil.")
try:
2019-11-13 10:49:35 +00:00
symKey = whisperKeys.symkeys[id.toHex()]
result = true
except KeyError:
error "Symmetric key not found."
2019-09-12 16:32:07 +00:00
2019-11-07 11:09:30 +00:00
# Whisper message posting and receiving
2019-09-12 16:32:07 +00:00
proc nimbus_post(message: ptr CPostMessage): bool {.exportc.} =
## Encryption is mandatory.
## A symmetric key or an asymmetric key must be provided. Both is not allowed.
## Providing a payload is mandatory, it cannot be nil, but can be of length 0.
2019-11-07 11:09:30 +00:00
doAssert(not message.isNil, "Message pointer cannot be nil.")
2019-11-04 16:51:59 +00:00
2019-09-12 16:32:07 +00:00
var
sigPrivKey: Option[PrivateKey]
asymKey: Option[PublicKey]
symKey: Option[SymKey]
padding: Option[Bytes]
payload: Bytes
if not message.pubKey.isNil() and not message.symKeyID.isNil():
warn "Both symmetric and asymmetric keys are provided, choose one."
return false
if message.pubKey.isNil() and message.symKeyID.isNil():
warn "Both symmetric and asymmetric keys are nil, provide one."
return false
2019-10-25 15:31:42 +00:00
if not message.pubKey.isNil():
2019-11-04 16:51:59 +00:00
try:
asymKey = some(initPublicKey(makeOpenArray(message.pubKey, 64)))
except EthKeysException:
2019-11-07 11:09:30 +00:00
error "Passed an invalid public key for encryption."
2019-11-04 16:51:59 +00:00
return false
2019-10-25 15:31:42 +00:00
try:
if not message.symKeyID.isNil():
let symKeyId = makeOpenArray(message.symKeyID, idLen).toHex()
symKey = some(whisperKeys.symKeys[symKeyId])
2019-10-25 15:31:42 +00:00
if not message.sourceID.isNil():
let sourceId = makeOpenArray(message.sourceID, idLen).toHex()
sigPrivKey = some(whisperKeys.asymKeys[sourceId].seckey)
2019-10-25 15:31:42 +00:00
except KeyError:
2019-11-07 11:09:30 +00:00
warn "No key found with provided key id."
2019-10-25 15:31:42 +00:00
return false
2019-09-12 16:32:07 +00:00
if not message.payload.isNil():
# This will make a copy
payload = @(makeOpenArray(message.payload, message.payloadLen))
2019-10-25 15:31:42 +00:00
else:
warn "Message payload was nil, post aborted."
2019-10-25 15:31:42 +00:00
return false
2019-09-12 16:32:07 +00:00
if not message.padding.isNil():
# This will make a copy
padding = some(@(makeOpenArray(message.padding, message.paddingLen)))
2019-09-12 16:32:07 +00:00
# TODO: call can create `Exception`, why?
2019-10-25 15:31:42 +00:00
result = node.postMessage(asymKey,
symKey,
sigPrivKey,
ttl = message.ttl,
topic = message.topic,
payload = payload,
padding = padding,
powTime = message.powTime,
powTarget = message.powTarget)
2019-09-12 16:32:07 +00:00
proc nimbus_subscribe_filter(id: var Identifier, options: ptr CFilterOptions,
handler: proc (msg: ptr CReceivedMessage, udata: pointer) {.gcsafe, cdecl.},
udata: pointer = nil): bool {.exportc.} =
## Encryption is mandatory.
## A symmetric key or an asymmetric key must be provided. Both is not allowed.
2019-10-25 15:31:42 +00:00
## In case of a passed handler, the received msg needs to be copied before the
## handler ends.
doAssert(not (unsafeAddr id).isNil, "Key id cannot be nil.")
2019-11-07 11:09:30 +00:00
doAssert(not options.isNil, "Filter options pointer cannot be nil.")
2019-11-04 16:51:59 +00:00
var
src: Option[PublicKey]
symKey: Option[SymKey]
privateKey: Option[PrivateKey]
if not options.privateKeyID.isNil() and not options.symKeyID.isNil():
warn "Both symmetric and asymmetric keys are provided, choose one."
return false
if options.privateKeyID.isNil() and options.symKeyID.isNil():
warn "Both symmetric and asymmetric keys are nil, provide one."
return false
2019-10-25 15:31:42 +00:00
if not options.source.isNil():
2019-11-04 16:51:59 +00:00
try:
src = some(initPublicKey(makeOpenArray(options.source, 64)))
except EthKeysException:
2019-11-07 11:09:30 +00:00
error "Passed an invalid public key as source."
return false
2019-09-12 16:32:07 +00:00
2019-10-25 15:31:42 +00:00
try:
if not options.symKeyID.isNil():
let symKeyId = makeOpenArray(options.symKeyID, idLen).toHex()
symKey = some(whisperKeys.symKeys[symKeyId])
2019-10-25 15:31:42 +00:00
if not options.privateKeyID.isNil():
let privKeyId = makeOpenArray(options.privateKeyID, idLen).toHex()
privateKey = some(whisperKeys.asymKeys[privKeyId].seckey)
2019-10-25 15:31:42 +00:00
except KeyError:
return false
2019-09-12 16:32:07 +00:00
let filter = newFilter(src, privateKey, symKey, @[options.topic],
options.minPow, options.allowP2P)
2019-09-12 16:32:07 +00:00
if handler.isNil:
# TODO: call can create `Exception`, why?
hexToBytes(node.subscribeFilter(filter, nil), id)
2019-10-25 15:31:42 +00:00
else:
proc c_handler(msg: ReceivedMessage) {.gcsafe.} =
var cmsg = CReceivedMessage(
decoded: unsafeAddr msg.decoded.payload[0],
decodedLen: csize msg.decoded.payload.len(),
timestamp: msg.timestamp,
ttl: msg.ttl,
topic: msg.topic,
pow: msg.pow,
hash: msg.hash
)
2019-09-12 16:32:07 +00:00
# Could also allocate here, but this should stay in scope until handler
# finishes so it should be fine.
var
source: array[RawPublicKeySize, byte]
recipientPublicKey: array[RawPublicKeySize, byte]
2019-10-25 15:31:42 +00:00
if msg.decoded.src.isSome():
# Need to pass the serialized form
source = msg.decoded.src.get().getRaw()
cmsg.source = addr source[0]
2019-10-25 15:31:42 +00:00
if msg.dst.isSome():
# Need to pass the serialized form
recipientPublicKey = msg.decoded.src.get().getRaw()
cmsg.recipientPublicKey = addr recipientPublicKey[0]
2019-09-12 16:32:07 +00:00
handler(addr cmsg, udata)
2019-09-12 16:32:07 +00:00
# TODO: call can create `Exception`, why?
# TODO: if we decide to internally also work with other IDs, we don't need
# to do this hex conversion back and forth.
hexToBytes(node.subscribeFilter(filter, c_handler), id)
2019-09-12 16:32:07 +00:00
# Bloom filter has to follow only the subscribed topics
# TODO: better to have an "adding" proc here
# TODO: call can create `Exception`, why?
traceAsyncErrors node.setBloomFilter(node.filtersToBloom())
result = true
proc nimbus_unsubscribe_filter(id: Identifier): bool {.exportc, raises: [].} =
doAssert(not(unsafeAddr id).isNil, "Filter id cannot be nil.")
2019-11-04 16:51:59 +00:00
result = node.unsubscribeFilter(id.toHex())
proc nimbus_get_min_pow(): float64 {.exportc, raises: [].} =
result = node.protocolState(Whisper).config.powRequirement
2019-11-13 10:49:35 +00:00
proc nimbus_get_bloom_filter(bloom: var Bloom) {.exportc, raises: [].} =
doAssert(not (unsafeAddr bloom).isNil, "Bloom pointer cannot be nil.")
2019-11-04 16:51:59 +00:00
2019-11-13 10:49:35 +00:00
bloom = node.protocolState(Whisper).config.bloom
2019-11-07 11:09:30 +00:00
# Nimbus limited Status chat API
# TODO: Return filter ID if we ever want to unsubscribe
proc subscribeChannel(
channel: string, handler: proc (msg: ReceivedMessage) {.gcsafe.}) =
var ctx: HMAC[sha256]
var symKey: SymKey
discard ctx.pbkdf2(channel, "", 65356, symKey)
let channelHash = digest(keccak256, channel)
var topic: array[4, byte]
for i in 0..<4:
topic[i] = channelHash.data[i]
info "Subscribing to channel", channel, topic, symKey
discard node.subscribeFilter(newFilter(symKey = some(symKey),
topics = @[topic]),
handler)
proc nimbus_join_public_chat(channel: cstring,
handler: proc (msg: ptr CReceivedMessage)
{.gcsafe, cdecl.}) {.exportc.} =
if handler.isNil:
subscribeChannel($channel, nil)
else:
proc c_handler(msg: ReceivedMessage) =
var cmsg = CReceivedMessage(
decoded: unsafeAddr msg.decoded.payload[0],
decodedLen: csize msg.decoded.payload.len(),
timestamp: msg.timestamp,
ttl: msg.ttl,
topic: msg.topic,
pow: msg.pow,
hash: msg.hash
)
handler(addr cmsg)
subscribeChannel($channel, c_handler)
# TODO: Add signing key as parameter
# TODO: How would we do key management? In nimbus (like in rpc) or in status go?
proc nimbus_post_public(channel: cstring, payload: cstring) {.exportc.} =
let encPrivateKey = initPrivateKey("5dc5381cae54ba3174dc0d46040fe11614d0cc94d41185922585198b4fcef9d3")
var ctx: HMAC[sha256]
var symKey: SymKey
var npayload = cast[Bytes]($payload)
discard ctx.pbkdf2($channel, "", 65356, symKey)
let channelHash = digest(keccak256, $channel)
var topic: array[4, byte]
for i in 0..<4:
topic[i] = channelHash.data[i]
# TODO: Handle error case
discard node.postMessage(symKey = some(symKey),
src = some(encPrivateKey),
ttl = 20,
topic = topic,
payload = npayload,
powTarget = 0.002)