Add Whisper implementation

This commit is contained in:
deme 2018-11-16 15:10:03 +01:00 committed by zah
parent dbf7d8b3ff
commit 70fc6874be
4 changed files with 893 additions and 16 deletions

View File

@ -9,7 +9,7 @@
import
algorithm, bitops, endians, math, options, sequtils, strutils, tables, times,
secp256k1, chronicles, asyncdispatch2, eth_common/eth_types, eth_keys, rlp,
nimcrypto/[bcmode, hash, keccak, rijndael],
hashes, byteutils, nimcrypto/[bcmode, hash, keccak, rijndael, sysrand],
../../eth_p2p, ../ecies
const
@ -20,12 +20,16 @@ const
payloadLenLenBits = 0b11'u8 ## payload flags length-of-length mask
signatureBits = 0b100'u8 ## payload flags signature mask
whisperVersion* = 6
defaultMinPow = 0.001'f64
defaultMaxMsgSize = 1024 * 1024 # * 10 # should be no higher than max RLPx size
bloomSize = 512 div 8
defaultQueueCapacity = 256
type
Hash* = MDigest[256]
SymKey* = array[256 div 8, byte] ## AES256 key
Topic* = array[4, byte]
Bloom* = array[64, byte] ## XXX: nim-eth-bloom has really quirky API and fixed
Bloom* = array[bloomSize, byte] ## XXX: nim-eth-bloom has really quirky API and fixed
## bloom size.
## stint is massive overkill / poor fit - a bloom filter is an array of bits,
## not a number
@ -35,6 +39,7 @@ type
src*: Option[PrivateKey] ## Optional key used for signing message
dst*: Option[PublicKey] ## Optional key used for asymmetric encryption
symKey*: Option[SymKey] ## Optional key used for symmetric encryption
payload*: Bytes ## Application data / message contents
padding*: Option[Bytes] ## Padding - if unset, will automatically pad up to
@ -59,9 +64,10 @@ type
env*: Envelope
hash*: Hash ## Hash, as calculated for proof-of-work
size*: uint64 ## RLP-encoded size of message
size*: uint32 ## RLP-encoded size of message
pow*: float64 ## Calculated proof-of-work
bloom*: Bloom ## Filter sent to direct peers for topic-based filtering
isP2P: bool
Queue* = object
## Bounded message repository
@ -71,11 +77,39 @@ type
## room for those with higher pow, even if they haven't expired yet.
## Larger messages and those with high time-to-live will require more pow.
items*: seq[Message] ## Sorted by proof-of-work
itemHashes*: HashSet[Message] ## For easy duplication checking
# XXX: itemHashes is added for easy message duplication checking and for
# easy pruning of the peer received message sets. It does have an impact on
# adding and pruning of items however.
# Need to give it some more thought and check where most time is lost in
# typical cases, perhaps we are better of with one hash table (lose PoW
# sorting however), or perhaps there is a simpler solution...
capacity*: int ## Max messages to keep. \
## XXX: really big messages can cause excessive mem usage when using msg \
## count
# XXX: We have to return more than just the payload
FilterMsgHandler* = proc(payload: Bytes) {.closure.}
Filter* = object
src: Option[PublicKey]
privateKey: Option[PrivateKey]
symKey: Option[SymKey]
topics: seq[Topic]
powReq: float64
allowP2P: bool
bloom: Bloom # cached bloom filter of all topics of filter
handler: FilterMsgHandler
# NOTE: could also have a queue here instead, or leave it to the actual client
WhisperConfig* = object
powRequirement*: float64
bloom*: Bloom
isLightNode*: bool
maxMsgSize*: uint32
# Utilities --------------------------------------------------------------------
proc toBE(v: uint64): array[8, byte] =
@ -124,6 +158,43 @@ proc topicBloom*(topic: Topic): Bloom =
assert idx <= 511
result[idx div 8] = result[idx div 8] or byte(1 shl (idx and 7'u16))
proc generateRandomID(): string =
var bytes: array[256 div 8, byte]
while true: # XXX: error instead of looping?
if randomBytes(bytes) == 256 div 8:
result = toHex(bytes)
break
proc `or`(a, b: Bloom): Bloom =
for i in 0..<a.len:
result[i] = a[i] or b[i]
proc bytesCopy(bloom: var Bloom, b: Bytes) =
assert b.len == bloomSize
# memcopy?
for i in 0..<bloom.len:
bloom[i] = b[i]
proc toBloom*(topics: openArray[Topic]): Bloom =
#if topics.len == 0:
# XXX: should we set the bloom here the all 1's ?
for topic in topics:
result = result or topicBloom(topic)
proc bloomFilterMatch(filter, sample: Bloom): bool =
for i in 0..<filter.len:
if (filter[i] or sample[i]) != filter[i]:
return false
return true
proc fullBloom*(): Bloom =
for i in 0..<result.len:
result[i] = 0xFF
proc emptyBloom*(): Bloom =
for i in 0..<result.len:
result[i] = 0x00
proc encryptAesGcm(plain: openarray[byte], key: SymKey,
iv: array[gcmIVLen, byte]): Bytes =
## Encrypt using AES-GCM, making sure to append tag and iv, in that order
@ -339,6 +410,10 @@ proc toRlp(self: Envelope): Bytes =
## What gets sent out over the wire includes the nonce
rlp.encode(self)
# NOTE: minePow and calcPowHash are different from go-ethereum implementation.
# Is correct however with EIP-627, but perhaps this is not up to date.
# Follow-up here: https://github.com/ethereum/go-ethereum/issues/18070
proc minePow*(self: Envelope, seconds: float): uint64 =
## For the given envelope, spend millis milliseconds to find the
## best proof-of-work and return the nonce
@ -386,21 +461,52 @@ proc cmpPow(a, b: Message): int =
proc initMessage*(env: Envelope): Message =
result.env = env
result.hash = env.calcPowHash()
result.size = env.toRlp().len().uint64 # XXX: calc len without creating RLP
debug "PoW hash", hash = result.hash
result.size = env.toRlp().len().uint32 # XXX: calc len without creating RLP
result.pow = calcPow(result.size, result.env.ttl, result.hash)
result.bloom = topicBloom(env.topic)
proc hash*(msg: Message): hashes.Hash = hash(msg.hash.data)
proc allowed(msg: Message, config: WhisperConfig): bool =
# Check max msg size, already happens in RLPx but there is a specific shh
# max msg size which should always be < RLPx max msg size
if msg.size > config.maxMsgSize:
warn "Message size too large", size = msg.size
return false
if msg.pow < config.powRequirement:
warn "too low PoW envelope", pow = msg.pow, minPow = config.powRequirement
return false
if not bloomFilterMatch(config.bloom, msg.bloom):
warn "received message does not match node bloomfilter"
return false
return true
# Queues -----------------------------------------------------------------------
proc initQueue*(capacity: int): Queue =
result.items = newSeqOfCap[Message](capacity)
result.capacity = capacity
result.itemHashes.init()
proc prune(self: var Queue) =
## Remove items that are past their expiry time
let now = epochTime().uint64
self.items.keepIf(proc(m: Message): bool = m.env.expiry > now)
let now = epochTime().uint32
proc add*(self: var Queue, msg: Message) =
# keepIf code + pruning of hashset
var pos = 0
for i in 0 ..< len(self.items):
if self.items[i].env.expiry > now:
if pos != i:
shallowCopy(self.items[pos], self.items[i])
inc(pos)
else: self.itemHashes.excl(self.items[i])
setLen(self.items, pos)
proc add*(self: var Queue, msg: Message): bool =
## Add a message to the queue.
## If we're at capacity, we will be removing, in order:
## * expired messages
@ -416,31 +522,352 @@ proc add*(self: var Queue, msg: Message) =
if last.pow > msg.pow or
(last.pow == msg.pow and last.env.expiry > msg.env.expiry):
# The new message has less pow or will expire earlier - drop it
return false
self.items.del(self.items.len() - 1)
self.itemHashes.excl(last)
# check for duplicate
# NOTE: Could also track if duplicates come from the same peer and disconnect
# from that peer. Is this tracking overhead worth it though?
if self.itemHashes.containsOrIncl(msg):
return false
else:
self.items.insert(msg, self.items.lowerBound(msg, cmpPow))
return true
# Filters ----------------------------------------------------------------------
proc newFilter*(src = none[PublicKey](), privateKey = none[PrivateKey](),
symKey = none[SymKey](), topics: seq[Topic] = @[],
powReq = 0.0, allowP2P = false): Filter =
Filter(src: src, privateKey: privateKey, symKey: symKey, topics: topics,
powReq: powReq, allowP2P: allowP2P, bloom: toBloom(topics))
proc notify(filters: Table[string, Filter], msg: Message) =
var decoded: Option[DecodedPayload]
var keyHash: Hash
for filter in filters.values:
if not filter.allowP2P and msg.isP2P:
continue
# NOTE: should we still check PoW if msg.isP2P? Not much sense to it?
if filter.powReq > 0 and msg.pow < filter.powReq:
continue
if filter.topics.len > 0:
if msg.env.topic notin filter.topics:
continue
# Decode, if already decoded previously check if hash of key matches
if decoded.isNone():
decoded = decode(msg.env.data, dst = filter.privateKey,
symKey = filter.symKey)
if filter.privateKey.isSome():
keyHash = keccak256.digest(filter.privateKey.get().data)
elif filter.symKey.isSome():
keyHash = keccak256.digest(filter.symKey.get())
# else:
# NOTE: should we error on messages without encryption?
if decoded.isNone():
continue
else:
if filter.privateKey.isSome():
if keyHash != keccak256.digest(filter.privateKey.get().data):
continue
elif filter.symKey.isSome():
if keyHash != keccak256.digest(filter.symKey.get()):
continue
# else:
# NOTE: should we error on messages without encryption?
# When decoding is done we can check the src (signature)
if filter.src.isSome():
var src: Option[PublicKey] = decoded.get().src
if not src.isSome():
continue
elif src.get() != filter.src.get():
continue
# Run callback
# NOTE: could also add the message to a filter queue
filter.handler(decoded.get().payload)
type
PeerState = ref object
initialized*: bool # when successfully completed the handshake
powRequirement*: float64
bloom*: Bloom
isLightNode*: bool
trusted*: bool
received: HashSet[Message]
running*: bool
WhisperState = ref object
queue*: Queue
filters*: Table[string, Filter]
config*: WhisperConfig
proc run(peer: Peer) {.async.}
proc run(node: EthereumNode, network: WhisperState) {.async.}
proc initProtocolState*(network: var WhisperState, node: EthereumNode) =
network.queue = initQueue(defaultQueueCapacity)
network.filters = initTable[string, Filter]()
network.config.bloom = fullBloom()
network.config.powRequirement = defaultMinPow
network.config.isLightNode = false
network.config.maxMsgSize = defaultMaxMsgSize
asyncCheck node.run(network)
rlpxProtocol shh(version = whisperVersion,
peerState = PeerState,
networkState = WhisperState):
onPeerConnected do (peer: Peer):
debug "onPeerConnected Whisper"
let
shhNetwork = peer.networkState
shhPeer = peer.state
asyncCheck peer.status(whisperVersion,
cast[uint](shhNetwork.config.powRequirement),
@(shhNetwork.config.bloom),
shhNetwork.config.isLightNode)
# XXX: we should allow this to timeout and disconnect if so
let m = await peer.nextMsg(shh.status)
if m.protocolVersion == whisperVersion:
debug "Suitable Whisper peer", peer, whisperVersion
else:
raise newException(UselessPeerError, "Incompatible Whisper version")
shhPeer.powRequirement = cast[float64](m.powConverted)
if m.bloom.len > 0:
if m.bloom.len != bloomSize:
raise newException(UselessPeerError, "Bloomfilter size mismatch")
else:
shhPeer.bloom.bytesCopy(m.bloom)
else:
# If no bloom filter is send we allow all
shhPeer.bloom = fullBloom()
shhPeer.isLightNode = m.isLightNode
if shhPeer.isLightNode and shhNetwork.config.isLightNode:
# No sense in connecting two light nodes so we disconnect
raise newException(UselessPeerError, "Two light nodes connected")
shhPeer.received.init()
shhPeer.trusted = false
shhPeer.initialized = true
asyncCheck peer.run()
debug "Whisper peer initialized"
onPeerDisconnected do (peer: Peer, reason: DisconnectionReason) {.gcsafe.}:
peer.state.running = false
rlpxProtocol shh(version = whisperVersion):
proc status(peer: Peer,
protocolVersion: uint,
powCoverted: uint,
powConverted: uint,
bloom: Bytes,
isLightNode: bool) =
discard
proc messages(peer: Peer, envelopes: openarray[Envelope]) =
discard
if not peer.state.initialized:
warn "Handshake not completed yet, discarding messages"
return
proc powRequirement(peer: Peer, value: float64) =
discard
for envelope in envelopes:
# check if expired or in future, or ttl not 0
if not envelope.valid():
warn "expired or future timed envelope"
# disconnect from peers sending bad envelopes
# await peer.disconnect(SubprotocolReason)
continue
var msg = initMessage(envelope)
if not msg.allowed(peer.networkState.config):
# disconnect from peers sending bad envelopes
# await peer.disconnect(SubprotocolReason)
continue
# This peer send it thus should not receive it again
peer.state(shh).received.incl(msg)
if peer.networkState.queue.add(msg):
# notify filters of this message
peer.networkState.filters.notify(msg)
proc powRequirement(peer: Peer, value: uint) =
if not peer.state.initialized:
warn "Handshake not completed yet, discarding powRequirement"
return
peer.state.powRequirement = cast[float64](value)
proc bloomFilterExchange(peer: Peer, bloom: Bytes) =
discard
if not peer.state.initialized:
warn "Handshake not completed yet, discarding bloomFilterExchange"
return
peer.state.bloom.bytesCopy(bloom)
nextID 126
proc p2pRequest(peer: Peer, envelope: Envelope) =
# TODO: here we would have to allow to insert some specific implementation
# such as e.g. Whisper Mail Server
discard
proc p2pMessage(peer: Peer, envelope: Envelope) =
discard
if peer.state.trusted:
# when trusted we can bypass any checks on envelope
var msg = Message(env: envelope, isP2P: true)
peer.networkState.filters.notify(msg)
# 'Runner' calls ---------------------------------------------------------------
proc processQueue(peer: Peer) =
var envelopes: seq[Envelope] = @[]
for message in peer.networkState(shh).queue.items:
if peer.state(shh).received.contains(message):
# debug "message was already send to peer"
continue
if message.pow < peer.state(shh).powRequirement:
debug "Message PoW too low for peer"
continue
if not bloomFilterMatch(peer.state(shh).bloom, message.bloom):
debug "Peer bloomfilter blocked message"
continue
debug "Adding envelope"
envelopes.add(message.env)
peer.state(shh).received.incl(message)
debug "Sending envelopes", amount=envelopes.len
# await peer.messages(envelopes)
asyncCheck peer.messages(envelopes)
proc run(peer: Peer) {.async.} =
peer.state(shh).running = true
while peer.state(shh).running:
if not peer.networkState(shh).config.isLightNode:
peer.processQueue()
await sleepAsync(300)
proc pruneReceived(node: EthereumNode) =
if node.peerPool != nil: # XXX: a bit dirty to need to check for this here ...
for peer in node.peers(shh):
if not peer.state(shh).initialized:
continue
# NOTE: Perhaps alter the queue prune call to keep track of a HashSet
# of pruned messages (as these should be smaller), and diff this with
# the received sets.
peer.state(shh).received = intersection(peer.state(shh).received,
node.protocolState(shh).queue.itemHashes)
proc run(node: EthereumNode, network: WhisperState) {.async.} =
while true:
# prune message queue every second
# TTL unit is in seconds, so this should be sufficient?
network.queue.prune()
# pruning the received sets is not necessary for correct workings
# but simply from keeping the sets growing indefinitely
node.pruneReceived()
await sleepAsync(1000)
# Public EthereumNode calls ----------------------------------------------------
# XXX: add targetPeer option
proc postMessage*(node: EthereumNode, pubKey = none[PublicKey](),
symKey = none[SymKey](), src = none[PrivateKey](),
ttl: uint32, topic: Topic, payload: Bytes, powTime = 1) =
# NOTE: Allow a post without a key? Encryption is mandatory in v6?
# NOTE: Do we allow a light node to add messages to queue?
# if node.protocolState(shh).isLightNode:
# error "Light node not allowed to post messages"
# return
var payload = encode(Payload(payload: payload, src: src, dst: pubKey,
symKey: symKey))
if payload.isSome():
var env = Envelope(expiry:epochTime().uint32 + ttl + powTime.uint32,
ttl: ttl, topic: topic, data: payload.get(), nonce: 0)
# XXX: make this non blocking or not?
# In its current blocking state, it could be noticed by a peer that no
# messages are send for a while, and thus that mining PoW is done, and that
# next messages contains a message originated from this peer
env.nonce = env.minePow(powTime.float)
if not env.valid(): # actually just ttl !=0 is sufficient
return
# We have to do the same checks here as in the messages proc not to leak
# any information that the message originates from this node.
var msg = initMessage(env)
if not msg.allowed(node.protocolState(shh).config):
return
debug "Adding message to queue"
if node.protocolState(shh).queue.add(msg):
# Also notify our own filters of the message we are sending,
# e.g. msg from local Dapp to Dapp
node.protocolState(shh).filters.notify(msg)
else:
error "encoding failed"
proc subscribeFilter*(node: EthereumNode, filter: Filter,
handler: FilterMsgHandler): string =
# NOTE: Should we allow a filter without a key? Encryption is mandatory in v6?
# Check if asymmetric _and_ symmetric key? Now asymmetric just has precedence.
var id = generateRandomID()
var filter = filter
filter.handler = handler
node.protocolState(shh).filters.add(id, filter)
debug "Filter added", filter = id
return id
proc unsubscribeFilter*(node: EthereumNode, filterId: string): bool =
var filter: Filter
return node.protocolState(shh).filters.take(filterId, filter)
proc setPowRequirement*(node: EthereumNode, powReq: float64) {.async.} =
# NOTE: do we need a tolerance of old PoW for some time?
node.protocolState(shh).config.powRequirement = powReq
for peer in node.peers(shh):
# asyncCheck peer.powRequirement(cast[uint](powReq))
await peer.powRequirement(cast[uint](powReq))
proc setBloomFilter*(node: EthereumNode, bloom: Bloom) {.async.} =
# NOTE: do we need a tolerance of old bloom filter for some time?
node.protocolState(shh).config.bloom = bloom
for peer in node.peers(shh):
# asyncCheck peer.bloomFilterExchange(@bloom)
await peer.bloomFilterExchange(@bloom)
proc filtersToBloom*(node: EthereumNode): Bloom =
for filter in node.protocolState(shh).filters.values:
if filter.topics.len > 0:
result = result or filter.bloom
proc setMaxMessageSize*(node: EthereumNode, size: uint32) =
node.protocolState(shh).config.maxMsgSize = size
proc setPeerTrusted*(node: EthereumNode, peerId: NodeId) =
for peer in node.peers(shh):
if peer.remote.id == peerId:
peer.state(shh).trusted = true
break
# XXX: should probably only be allowed before connection is made,
# as there exists no message to communicate to peers that it is a light node
# How to arrange that?
proc setLightNode*(node: EthereumNode, isLightNode: bool) =
node.protocolState(shh).config.isLightNode = isLightNode

192
tests/shh_basic_client.nim Normal file
View File

@ -0,0 +1,192 @@
#
# Ethereum P2P
# (c) Copyright 2018
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import
sequtils, options, strutils, parseopt, asyncdispatch2,
eth_keys, rlp, eth_p2p, eth_p2p/rlpx_protocols/[shh_protocol],
eth_p2p/[discovery, enode, peer_pool]
const
DefaultListeningPort = 30303
Usage = """Usage:
tssh_client [options]
Options:
-p --port Listening port
--post Post messages
--watch Install filters
--mainnet Connect to main network (default local private)
--local Only local loopback
--help Display this help and exit"""
DockerBootnode = "enode://f41f87f084ed7df4a9fd0833e395f49c89764462d3c4bc16d061a3ae5e3e34b79eb47d61c2f62db95ff32ae8e20965e25a3c9d9b8dbccaa8e8d77ac6fc8efc06@172.17.0.2:30301"
# bootnodes taken from go-ethereum
MainBootnodes* = [
"enode://a979fb575495b8d6db44f750317d0f4622bf4c2aa3365d6af7c284339968eef29b69ad0dce72a4d8db5ebb4968de0e3bec910127f134779fbcb0cb6d3331163c@52.16.188.185:30303",
"enode://3f1d12044546b76342d59d4a05532c14b85aa669704bfe1f864fe079415aa2c02d743e03218e57a33fb94523adb54032871a6c51b2cc5514cb7c7e35b3ed0a99@13.93.211.84:30303",
"enode://78de8a0916848093c73790ead81d1928bec737d565119932b98c6b100d944b7a95e94f847f689fc723399d2e31129d182f7ef3863f2b4c820abbf3ab2722344d@191.235.84.50:30303",
"enode://158f8aab45f6d19c6cbf4a089c2670541a8da11978a2f90dbf6a502a4a3bab80d288afdbeb7ec0ef6d92de563767f3b1ea9e8e334ca711e9f8e2df5a0385e8e6@13.75.154.138:30303",
"enode://1118980bf48b0a3640bdba04e0fe78b1add18e1cd99bf22d53daac1fd9972ad650df52176e7c7d89d1114cfef2bc23a2959aa54998a46afcf7d91809f0855082@52.74.57.123:30303",
"enode://979b7fa28feeb35a4741660a16076f1943202cb72b6af70d327f053e248bab9ba81760f39d0701ef1d8f89cc1fbd2cacba0710a12cd5314d5e0c9021aa3637f9@5.1.83.226:30303",
]
# shh nodes taken from:
# https://github.com/status-im/status-react/blob/80aa0e92864c638777a45c3f2aeb66c3ae7c0b2e/resources/config/fleets.json
# These are probably not on the main network?
ShhNodes = [
"enode://66ba15600cda86009689354c3a77bdf1a97f4f4fb3ab50ffe34dbc904fac561040496828397be18d9744c75881ffc6ac53729ddbd2cdbdadc5f45c400e2622f7@206.189.243.176:30305",
"enode://0440117a5bc67c2908fad94ba29c7b7f2c1536e96a9df950f3265a9566bf3a7306ea8ab5a1f9794a0a641dcb1e4951ce7c093c61c0d255f4ed5d2ed02c8fce23@35.224.15.65:30305",
"enode://a80eb084f6bf3f98bf6a492fd6ba3db636986b17643695f67f543115d93d69920fb72e349e0c617a01544764f09375bb85f452b9c750a892d01d0e627d9c251e@47.89.16.125:30305",
"enode://4ea35352702027984a13274f241a56a47854a7fd4b3ba674a596cff917d3c825506431cf149f9f2312a293bb7c2b1cca55db742027090916d01529fe0729643b@206.189.243.178:30305",
"enode://552942cc4858073102a6bcd0df9fe4de6d9fc52ddf7363e8e0746eba21b0f98fb37e8270bc629f72cfe29e0b3522afaf51e309a05998736e2c0dad5288991148@130.211.215.133:30305",
"enode://aa97756bc147d74be6d07adfc465266e17756339d3d18591f4be9d1b2e80b86baf314aed79adbe8142bcb42bc7bc40e83ee3bbd0b82548e595bf855d548906a1@47.52.188.241:30305",
"enode://ce559a37a9c344d7109bd4907802dd690008381d51f658c43056ec36ac043338bd92f1ac6043e645b64953b06f27202d679756a9c7cf62fdefa01b2e6ac5098e@206.189.243.179:30305",
"enode://b33dc678589931713a085d29f9dc0efee1783dacce1d13696eb5d3a546293198470d97822c40b187336062b39fd3464e9807858109752767d486ea699a6ab3de@35.193.151.184:30305",
"enode://f34451823b173dc5f2ac0eec1668fdb13dba9452b174249a7e0272d6dce16fb811a01e623300d1b7a67c240ae052a462bff3f60e4a05e4c4bd23cc27dea57051@47.52.173.66:30305",
"enode://4e0a8db9b73403c9339a2077e911851750fc955db1fc1e09f81a4a56725946884dd5e4d11258eac961f9078a393c45bcab78dd0e3bc74e37ce773b3471d2e29c@206.189.243.171:30305",
"enode://eb4cc33c1948b1f4b9cb8157757645d78acd731cc8f9468ad91cef8a7023e9c9c62b91ddab107043aabc483742ac15cb4372107b23962d3bfa617b05583f2260@146.148.66.209:30305",
"enode://7c80e37f324bbc767d890e6381854ef9985d33940285413311e8b5927bf47702afa40cd5d34be9aa6183ac467009b9545e24b0d0bc54ef2b773547bb8c274192@47.91.155.62:30305",
"enode://a8bddfa24e1e92a82609b390766faa56cf7a5eef85b22a2b51e79b333c8aaeec84f7b4267e432edd1cf45b63a3ad0fc7d6c3a16f046aa6bc07ebe50e80b63b8c@206.189.243.172:30305",
"enode://c7e00e5a333527c009a9b8f75659d9e40af8d8d896ebaa5dbdd46f2c58fc010e4583813bc7fc6da98fcf4f9ca7687d37ced8390330ef570d30b5793692875083@35.192.123.253:30305",
"enode://4b2530d045b1d9e0e45afa7c008292744fe77675462090b4001f85faf03b87aa79259c8a3d6d64f815520ac76944e795cbf32ff9e2ce9ba38f57af00d1cc0568@47.90.29.122:30305",
"enode://887cbd92d95afc2c5f1e227356314a53d3d18855880ac0509e0c0870362aee03939d4074e6ad31365915af41d34320b5094bfcc12a67c381788cd7298d06c875@206.189.243.177:30305",
"enode://2af8f4f7a0b5aabaf49eb72b9b59474b1b4a576f99a869e00f8455928fa242725864c86bdff95638a8b17657040b21771a7588d18b0f351377875f5b46426594@35.232.187.4:30305",
"enode://76ee16566fb45ca7644c8dec7ac74cadba3bfa0b92c566ad07bcb73298b0ffe1315fd787e1f829e90dba5cd3f4e0916e069f14e50e9cbec148bead397ac8122d@47.91.226.75:30305",
"enode://2b01955d7e11e29dce07343b456e4e96c081760022d1652b1c4b641eaf320e3747871870fa682e9e9cfb85b819ce94ed2fee1ac458904d54fd0b97d33ba2c4a4@206.189.240.70:30305",
"enode://19872f94b1e776da3a13e25afa71b47dfa99e658afd6427ea8d6e03c22a99f13590205a8826443e95a37eee1d815fc433af7a8ca9a8d0df7943d1f55684045b7@35.238.60.236:30305"
]
type
ShhConfig* = object
listeningPort*: int
post*: bool
watch*: bool
main*: bool
local*: bool
proc processArguments*(): ShhConfig =
var opt = initOptParser()
var length = 0
for kind, key, value in opt.getopt():
case kind
of cmdArgument:
echo key
of cmdLongOption, cmdShortOption:
inc(length)
case key.toLowerAscii()
of "help", "h": quit(Usage, QuitSuccess)
of "port", "p":
result.listeningPort = value.parseInt
of "post":
result.post = true
of "watch":
result.watch = true
of "mainnet":
result.main = true
of "local":
result.local = true
else: quit(Usage)
of cmdEnd:
quit(Usage)
let config = processArguments()
var port: Port
var address: Address
var netId: uint
# config
if config.listeningPort != 0:
port = Port(config.listeningPort)
else:
port = Port(DefaultListeningPort)
if config.local:
address = Address(udpPort: port, tcpPort: port, ip: parseIpAddress("127.0.0.1"))
else:
address = Address(udpPort: port, tcpPort: port, ip: parseIpAddress("0.0.0.0"))
if config.main:
netId = 1
else:
netId = 15
let keys = newKeyPair()
var node = newEthereumNode(keys, address, netId, nil, addAllCapabilities = false)
node.addCapability shh
# lets prepare some prearranged keypairs
let encPrivateKey = initPrivateKey("5dc5381cae54ba3174dc0d46040fe11614d0cc94d41185922585198b4fcef9d3")
let encPublicKey = encPrivateKey.getPublicKey()
let signPrivateKey = initPrivateKey("365bda0757d22212b04fada4b9222f8c3da59b49398fa04cf612481cd893b0a3")
let signPublicKey = signPrivateKey.getPublicKey()
# var symKey: SymKey = [byte 234, 86, 75, 97, 0, 214, 53, 41, 62, 204, 78, 253, 220, 134, 78, 203, 58, 35, 51, 61, 95, 218, 42, 78, 146, 142, 229, 232, 151, 219, 224, 32]
var symKey: SymKey
let topic = [byte 0x12, 0, 0, 0]
if config.main:
var bootnodes: seq[ENode] = @[]
for nodeId in MainBootnodes:
var bootnode: ENode
discard initENode(nodeId, bootnode)
bootnodes.add(bootnode)
asyncCheck node.connectToNetwork(bootnodes, true, true)
# main network has mostly non SHH nodes, so we connect directly to SHH nodes
for nodeId in ShhNodes:
var shhENode: ENode
discard initENode(nodeId, shhENode)
var shhNode = newNode(shhENode)
asyncCheck node.peerPool.connectToNode(shhNode)
else:
var bootENode: ENode
discard initENode(DockerBootNode, bootENode)
waitFor node.connectToNetwork(@[bootENode], true, true)
if config.watch:
var data: seq[Bytes] = @[]
proc handler(payload: Bytes) =
echo payload.repr
data.add(payload)
# filter encrypted asym
discard node.subscribeFilter(newFilter(privateKey = some(encPrivateKey),
topics = @[topic]), handler)
# filter encrypted asym + signed
discard node.subscribeFilter(newFilter(some(signPublicKey),
privateKey = some(encPrivateKey),
topics = @[topic]), handler)
# filter encrypted sym
discard node.subscribeFilter(newFilter(symKey = some(symKey),
topics = @[topic]), handler)
# filter encrypted sym + signed
discard node.subscribeFilter(newFilter(some(signPublicKey),
symKey = some(symKey),
topics = @[topic]), handler)
# discard node.setBloomFilter(node.filtersToBloom())
# discard node.setBloomFilter(emptyBloom())
# waitFor sleepAsync(10000)
# echo data.repr
if config.post:
# encrypted asym
node.postMessage(some(encPublicKey), ttl = 5, topic = topic,
payload = repeat(byte 65, 10))
poll()
# # encrypted asym + signed
node.postMessage(some(encPublicKey), src = some(signPrivateKey), ttl = 5,
topic = topic, payload = repeat(byte 66, 10))
poll()
# # encrypted sym
node.postMessage(symKey = some(symKey), ttl = 5, topic = topic,
payload = repeat(byte 67, 10))
poll()
# # encrypted sym + signed
node.postMessage(symKey = some(symKey), src = some(signPrivateKey), ttl = 5,
topic = topic, payload = repeat(byte 68, 10))
while true:
poll()

207
tests/tshh_connect.nim Normal file
View File

@ -0,0 +1,207 @@
#
# Ethereum P2P
# (c) Copyright 2018
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import
sequtils, options, unittest, tables, asyncdispatch2, rlp, eth_keys,
eth_p2p, eth_p2p/rlpx_protocols/[shh_protocol], eth_p2p/[discovery, enode]
proc localAddress(port: int): Address =
let port = Port(port)
result = Address(udpPort: port, tcpPort: port, ip: parseIpAddress("127.0.0.1"))
proc startDiscoveryNode(privKey: PrivateKey, address: Address,
bootnodes: seq[ENode]): Future[DiscoveryProtocol] {.async.} =
result = newDiscoveryProtocol(privKey, address, bootnodes)
result.open()
await result.bootstrap()
proc setupBootNode(): Future[ENode] {.async.} =
let
bootNodeKey = newPrivateKey()
bootNodeAddr = localAddress(30301)
bootNode = await startDiscoveryNode(bootNodeKey, bootNodeAddr, @[])
result = initENode(bootNodeKey.getPublicKey, bootNodeAddr)
template asyncTest(name, body: untyped) =
test name:
proc scenario {.async.} = body
waitFor scenario()
const useCompression = defined(useSnappy)
let
keys1 = newKeyPair()
keys2 = newKeyPair()
var node1 = newEthereumNode(keys1, localAddress(30303), 1, nil,
addAllCapabilities = false,
useCompression = useCompression)
node1.addCapability shh
var node2 = newEthereumNode(keys2, localAddress(30304), 1, nil,
addAllCapabilities = false,
useCompression = useCompression)
node2.addCapability shh
template waitForEmptyQueues() =
while node1.protocolState(shh).queue.items.len != 0 or
node2.protocolState(shh).queue.items.len != 0: poll()
when not defined(directConnect):
let bootENode = waitFor setupBootNode()
# node2 listening and node1 not, to avoid many incoming vs outgoing
var node1Connected = node1.connectToNetwork(@[bootENode], false, true)
var node2Connected = node2.connectToNetwork(@[bootENode], true, true)
waitFor node1Connected
waitFor node2Connected
asyncTest "Two peers connected":
check:
node1.peerPool.connectedNodes.len() == 1
node2.peerPool.connectedNodes.len() == 1
else: # XXX: tricky without peerPool
node1.initProtocolStates()
node2.initProtocolStates()
node2.startListening()
discard waitFor node1.rlpxConnect(newNode(initENode(node2.keys.pubKey,
node2.address)))
asyncTest "Filters with encryption and signing":
let encryptKeyPair = newKeyPair()
let signKeyPair = newKeyPair()
var symKey: SymKey
let topic = [byte 0x12, 0, 0, 0]
var filters: seq[string] = @[]
proc handler1(payload: Bytes) =
check payload == repeat(byte 1, 10) or payload == repeat(byte 2, 10)
proc handler2(payload: Bytes) =
check payload == repeat(byte 2, 10)
proc handler3(payload: Bytes) =
check payload == repeat(byte 3, 10) or payload == repeat(byte 4, 10)
proc handler4(payload: Bytes) =
check payload == repeat(byte 4, 10)
# Filters
# filter for encrypted asym
filters.add(node1.subscribeFilter(newFilter(privateKey = some(encryptKeyPair.seckey),
topics = @[topic]), handler1))
# filter for encrypted asym + signed
filters.add(node1.subscribeFilter(newFilter(some(signKeyPair.pubkey),
privateKey = some(encryptKeyPair.seckey),
topics = @[topic]), handler2))
# filter for encrypted sym
filters.add(node1.subscribeFilter(newFilter(symKey = some(symKey),
topics = @[topic]), handler3))
# filter for encrypted sym + signed
filters.add(node1.subscribeFilter(newFilter(some(signKeyPair.pubkey),
symKey = some(symKey),
topics = @[topic]), handler4))
# Messages
# encrypted asym
node2.postMessage(some(encryptKeyPair.pubkey), ttl = 5, topic = topic,
payload = repeat(byte 1, 10))
# encrypted asym + signed
node2.postMessage(some(encryptKeyPair.pubkey), src = some(signKeyPair.seckey),
ttl = 4, topic = topic, payload = repeat(byte 2, 10))
# encrypted sym
node2.postMessage(symKey = some(symKey), ttl = 3, topic = topic,
payload = repeat(byte 3, 10))
# encrypted sym + signed
node2.postMessage(symKey = some(symKey), src = some(signKeyPair.seckey),
ttl = 2, topic = topic, payload = repeat(byte 4, 10))
check node2.protocolState(shh).queue.items.len == 4
# XXX: improve the dumb sleep
await sleepAsync(300)
check node1.protocolState(shh).queue.items.len == 4
for filter in filters:
check node1.unsubscribeFilter(filter) == true
waitForEmptyQueues()
asyncTest "Filters with topics":
check:
1 == 1
asyncTest "Filters with PoW":
check:
1 == 1
asyncTest "Bloomfilter blocking":
let sendTopic1 = [byte 0x12, 0, 0, 0]
let sendTopic2 = [byte 0x34, 0, 0, 0]
let filterTopics = @[[byte 0x34, 0, 0, 0],[byte 0x56, 0, 0, 0]]
proc handler(payload: Bytes) = discard
var filter = node1.subscribeFilter(newFilter(topics = filterTopics), handler)
await node1.setBloomFilter(node1.filtersToBloom())
node2.postMessage(ttl = 1, topic = sendTopic1, payload = repeat(byte 0, 10))
# XXX: improve the dumb sleep
await sleepAsync(300)
check:
node1.protocolState(shh).queue.items.len == 0
node2.protocolState(shh).queue.items.len == 1
waitForEmptyQueues()
node2.postMessage(ttl = 1, topic = sendTopic2, payload = repeat(byte 0, 10))
# XXX: improve the dumb sleep
await sleepAsync(300)
check:
node1.protocolState(shh).queue.items.len == 1
node2.protocolState(shh).queue.items.len == 1
await node1.setBloomFilter(fullBloom())
waitForEmptyQueues()
asyncTest "PoW blocking":
let topic = [byte 0, 0, 0, 0]
await node1.setPowRequirement(1.0)
node2.postMessage(ttl = 1, topic = topic, payload = repeat(byte 0, 10))
await sleepAsync(300)
check:
node1.protocolState(shh).queue.items.len == 0
node2.protocolState(shh).queue.items.len == 1
waitForEmptyQueues()
await node1.setPowRequirement(0.0)
node2.postMessage(ttl = 1, topic = topic, payload = repeat(byte 0, 10))
await sleepAsync(300)
check:
node1.protocolState(shh).queue.items.len == 1
node2.protocolState(shh).queue.items.len == 1
waitForEmptyQueues()
asyncTest "Queue pruning":
let topic = [byte 0, 0, 0, 0]
for i in countdown(10, 1):
node2.postMessage(ttl = i.uint32, topic = topic, payload = repeat(byte 0, 10))
await sleepAsync(300)
check:
node1.protocolState(shh).queue.items.len == 10
node2.protocolState(shh).queue.items.len == 10
await sleepAsync(1000)
check:
node1.protocolState(shh).queue.items.len == 0
node2.protocolState(shh).queue.items.len == 0
asyncTest "Lightnode":
check:
1 == 1
asyncTest "P2P":
check:
1 == 1

View File

@ -0,0 +1,51 @@
#
# Ethereum P2P
# (c) Copyright 2018
# Status Research & Development GmbH
#
# Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import
options, unittest, asyncdispatch2, rlp, eth_keys,
eth_p2p, eth_p2p/mock_peers, eth_p2p/rlpx_protocols/[shh_protocol]
proc localAddress(port: int): Address =
let port = Port(port)
result = Address(udpPort: port, tcpPort: port, ip: parseIpAddress("127.0.0.1"))
template asyncTest(name, body: untyped) =
test name:
proc scenario {.async.} = body
waitFor scenario()
asyncTest "network with 3 peers using shh protocol":
const useCompression = defined(useSnappy)
let localKeys = newKeyPair()
let localAddress = localAddress(30303)
var localNode = newEthereumNode(localKeys, localAddress, 1, nil,
addAllCapabilities = false,
useCompression = useCompression)
localNode.addCapability shh
localNode.initProtocolStates()
localNode.startListening()
var mock1 = newMockPeer do (m: MockConf):
m.addHandshake shh.status(protocolVersion: whisperVersion, powConverted: 0,
bloom: @[], isLightNode: false)
m.expect(shh.messages)
var mock2 = newMockPeer do (m: MockConf):
m.addHandshake shh.status(protocolVersion: whisperVersion,
powConverted: cast[uint](0.1),
bloom: @[], isLightNode: false)
m.expect(shh.messages)
var mock1Peer = await localNode.rlpxConnect(mock1)
var mock2Peer = await localNode.rlpxConnect(mock2)
check:
mock1Peer.state(shh).powRequirement == 0
mock2Peer.state(shh).powRequirement == 0.1