Add Whisper implementation
This commit is contained in:
parent
dbf7d8b3ff
commit
70fc6874be
|
@ -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
|
||||
self.items.del(self.items.len() - 1)
|
||||
return false
|
||||
|
||||
self.items.insert(msg, self.items.lowerBound(msg, cmpPow))
|
||||
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
|
||||
|
|
|
@ -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()
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue