mirror of https://github.com/status-im/nim-eth.git
remove Whisper (#537)
replaced by https://rfc.vac.dev/ / https://github.com/status-im/nwaku
This commit is contained in:
parent
2734ed6bda
commit
591360284b
|
@ -1,668 +0,0 @@
|
||||||
# nim-eth - Whisper
|
|
||||||
# Copyright (c) 2018-2021 Status Research & Development GmbH
|
|
||||||
# Licensed and distributed under either of
|
|
||||||
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
||||||
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
||||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
import
|
|
||||||
std/[algorithm, bitops, math, options, tables, times, hashes],
|
|
||||||
chronicles, stew/[byteutils, endians2], metrics,
|
|
||||||
nimcrypto/[bcmode, hash, keccak, rijndael],
|
|
||||||
".."/../../[keys, rlp, p2p], ../../ecies
|
|
||||||
|
|
||||||
when chronicles.enabledLogLevel == LogLevel.TRACE:
|
|
||||||
import std/strutils
|
|
||||||
|
|
||||||
logScope:
|
|
||||||
topics = "whisper_types"
|
|
||||||
|
|
||||||
declarePublicCounter envelopes_valid,
|
|
||||||
"Received & posted valid envelopes"
|
|
||||||
declarePublicCounter envelopes_dropped,
|
|
||||||
"Dropped envelopes", labels = ["reason"]
|
|
||||||
|
|
||||||
const
|
|
||||||
flagsLen = 1 ## payload flags field length, bytes
|
|
||||||
gcmIVLen = 12 ## Length of IV (seed) used for AES
|
|
||||||
gcmTagLen = 16 ## Length of tag used to authenticate AES-GCM-encrypted message
|
|
||||||
padMaxLen = 256 ## payload will be padded to multiples of this by default
|
|
||||||
signatureBits = 0b100'u8 ## payload flags signature mask
|
|
||||||
bloomSize* = 512 div 8
|
|
||||||
defaultFilterQueueCapacity = 64
|
|
||||||
|
|
||||||
type
|
|
||||||
Hash* = MDigest[256]
|
|
||||||
SymKey* = array[256 div 8, byte] ## AES256 key.
|
|
||||||
Topic* = array[4, byte] ## 4 bytes that can be used to filter messages on.
|
|
||||||
Bloom* = array[bloomSize, byte] ## A bloom filter that can be used to identify
|
|
||||||
## a number of topics that a peer is interested in.
|
|
||||||
# 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
|
|
||||||
|
|
||||||
Payload* = object
|
|
||||||
## Payload is what goes in the data field of the Envelope.
|
|
||||||
|
|
||||||
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*: seq[byte] ## Application data / message contents
|
|
||||||
padding*: Option[seq[byte]] ## Padding - if unset, will automatically pad up to
|
|
||||||
## nearest maxPadLen-byte boundary
|
|
||||||
DecodedPayload* = object
|
|
||||||
## The decoded payload of a received message.
|
|
||||||
|
|
||||||
src*: Option[PublicKey] ## If the message was signed, this is the public key
|
|
||||||
## of the source
|
|
||||||
payload*: seq[byte] ## Application data / message contents
|
|
||||||
padding*: Option[seq[byte]] ## Message padding
|
|
||||||
|
|
||||||
Envelope* = object
|
|
||||||
## What goes on the wire in the whisper protocol - a payload and some
|
|
||||||
## book-keeping
|
|
||||||
# Don't touch field order, there's lots of macro magic that depends on it
|
|
||||||
expiry*: uint32 ## Unix timestamp when message expires
|
|
||||||
ttl*: uint32 ## Time-to-live, seconds - message was created at (expiry - ttl)
|
|
||||||
topic*: Topic
|
|
||||||
data*: seq[byte] ## Payload, as given by user
|
|
||||||
nonce*: uint64 ## Nonce used for proof-of-work calculation
|
|
||||||
|
|
||||||
Message* = object
|
|
||||||
## An Envelope with a few cached properties
|
|
||||||
|
|
||||||
env*: Envelope
|
|
||||||
hash*: Hash ## Hash, as calculated for proof-of-work
|
|
||||||
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
|
|
||||||
|
|
||||||
ReceivedMessage* = object
|
|
||||||
## A received message that matched a filter and was possible to decrypt.
|
|
||||||
## Contains the decoded payload and additional information.
|
|
||||||
decoded*: DecodedPayload
|
|
||||||
timestamp*: uint32
|
|
||||||
ttl*: uint32
|
|
||||||
topic*: Topic
|
|
||||||
pow*: float64
|
|
||||||
hash*: Hash
|
|
||||||
dst*: Option[PublicKey]
|
|
||||||
|
|
||||||
Queue* = object
|
|
||||||
## Bounded message repository
|
|
||||||
##
|
|
||||||
## Whisper uses proof-of-work to judge the usefulness of a message staying
|
|
||||||
## in the "cloud" - messages with low proof-of-work will be removed to make
|
|
||||||
## 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[Hash] ## 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
|
|
||||||
|
|
||||||
FilterMsgHandler* = proc(msg: ReceivedMessage) {.gcsafe, raises: [Defect].}
|
|
||||||
|
|
||||||
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
|
|
||||||
queue: seq[ReceivedMessage]
|
|
||||||
|
|
||||||
Filters* = Table[string, Filter]
|
|
||||||
|
|
||||||
# Utilities --------------------------------------------------------------------
|
|
||||||
|
|
||||||
proc leadingZeroBits(hash: MDigest): int =
|
|
||||||
## Number of most significant zero bits before the first one
|
|
||||||
for h in hash.data:
|
|
||||||
static: doAssert sizeof(h) == 1
|
|
||||||
if h == 0:
|
|
||||||
result += 8
|
|
||||||
else:
|
|
||||||
result += countLeadingZeroBits(h)
|
|
||||||
break
|
|
||||||
|
|
||||||
proc calcPow*(size, ttl: uint64, hash: Hash): float64 =
|
|
||||||
## Whisper proof-of-work is defined as the best bit of a hash divided by
|
|
||||||
## encoded size and time-to-live, such that large and long-lived messages get
|
|
||||||
## penalized
|
|
||||||
|
|
||||||
let bits = leadingZeroBits(hash)
|
|
||||||
return pow(2.0, bits.float64) / (size.float64 * ttl.float64)
|
|
||||||
|
|
||||||
proc topicBloom*(topic: Topic): Bloom =
|
|
||||||
## Whisper uses 512-bit bloom filters meaning 9 bits of indexing - 3 9-bit
|
|
||||||
## indexes into the bloom are created using the first 3 bytes of the topic and
|
|
||||||
## complementing each byte with an extra bit from the last topic byte
|
|
||||||
for i in 0..<3:
|
|
||||||
var idx = uint16(topic[i])
|
|
||||||
if (topic[3] and byte(1 shl i)) != 0: # fetch the 9'th bit from the last byte
|
|
||||||
idx = idx + 256
|
|
||||||
|
|
||||||
doAssert idx <= 511
|
|
||||||
result[idx div 8] = result[idx div 8] or byte(1 shl (idx and 7'u16))
|
|
||||||
|
|
||||||
proc generateRandomID*(rng: var HmacDrbgContext): string =
|
|
||||||
toHex(rng.generate(array[256 div 8, byte]))
|
|
||||||
|
|
||||||
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: openArray[byte]) =
|
|
||||||
doAssert b.len == bloomSize
|
|
||||||
copyMem(addr bloom[0], unsafeAddr b[0], bloomSize)
|
|
||||||
|
|
||||||
proc toBloom*(topics: openArray[Topic]): Bloom =
|
|
||||||
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 =
|
|
||||||
## Returns a fully set bloom filter. To be used when allowing all topics.
|
|
||||||
# There is no setMem exported in system, assume compiler is smart enough?
|
|
||||||
for i in 0..<result.len:
|
|
||||||
result[i] = 0xFF
|
|
||||||
|
|
||||||
proc encryptAesGcm(plain: openArray[byte], key: SymKey,
|
|
||||||
iv: array[gcmIVLen, byte]): seq[byte] =
|
|
||||||
## Encrypt using AES-GCM, making sure to append tag and iv, in that order
|
|
||||||
var gcm: GCM[aes256]
|
|
||||||
result = newSeqOfCap[byte](plain.len + gcmTagLen + iv.len)
|
|
||||||
result.setLen plain.len
|
|
||||||
gcm.init(key, iv, [])
|
|
||||||
gcm.encrypt(plain, result)
|
|
||||||
var tag: array[gcmTagLen, byte]
|
|
||||||
gcm.getTag(tag)
|
|
||||||
gcm.clear()
|
|
||||||
result.add tag
|
|
||||||
result.add iv
|
|
||||||
|
|
||||||
proc decryptAesGcm(cipher: openArray[byte], key: SymKey): Option[seq[byte]] =
|
|
||||||
## Decrypt AES-GCM ciphertext and validate authenticity - assumes
|
|
||||||
## cipher-tag-iv format of the buffer
|
|
||||||
if cipher.len < gcmTagLen + gcmIVLen:
|
|
||||||
debug "cipher missing tag/iv", len = cipher.len
|
|
||||||
return
|
|
||||||
let plainLen = cipher.len - gcmTagLen - gcmIVLen
|
|
||||||
var gcm: GCM[aes256]
|
|
||||||
var res = newSeq[byte](plainLen)
|
|
||||||
let iv = cipher[^gcmIVLen .. ^1]
|
|
||||||
let tag = cipher[^(gcmIVLen + gcmTagLen) .. ^(gcmIVLen + 1)]
|
|
||||||
gcm.init(key, iv, [])
|
|
||||||
gcm.decrypt(cipher[0 ..< ^(gcmIVLen + gcmTagLen)], res)
|
|
||||||
var tag2: array[gcmTagLen, byte]
|
|
||||||
gcm.getTag(tag2)
|
|
||||||
gcm.clear()
|
|
||||||
|
|
||||||
if tag != tag2:
|
|
||||||
debug "cipher tag mismatch", len = cipher.len, tag, tag2
|
|
||||||
return
|
|
||||||
return some(res)
|
|
||||||
|
|
||||||
# Payloads ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
# Several differences between geth and parity - this code is closer to geth
|
|
||||||
# simply because that makes it closer to EIP 627 - see also:
|
|
||||||
# https://github.com/paritytech/parity-ethereum/issues/9652
|
|
||||||
|
|
||||||
proc encode*(rng: var HmacDrbgContext, self: Payload): Option[seq[byte]] =
|
|
||||||
## Encode a payload according so as to make it suitable to put in an Envelope
|
|
||||||
## The format follows EIP 627 - https://eips.ethereum.org/EIPS/eip-627
|
|
||||||
|
|
||||||
# XXX is this limit too high? We could limit it here but the protocol
|
|
||||||
# technically supports it..
|
|
||||||
if self.payload.len >= 256*256*256:
|
|
||||||
notice "Payload exceeds max length", len = self.payload.len
|
|
||||||
return
|
|
||||||
|
|
||||||
# length of the payload length field :)
|
|
||||||
let payloadLenLen =
|
|
||||||
if self.payload.len >= 256*256: 3'u8
|
|
||||||
elif self.payload.len >= 256: 2'u8
|
|
||||||
else: 1'u8
|
|
||||||
|
|
||||||
let signatureLen =
|
|
||||||
if self.src.isSome(): keys.RawSignatureSize
|
|
||||||
else: 0
|
|
||||||
|
|
||||||
# useful data length
|
|
||||||
let dataLen = flagsLen + payloadLenLen.int + self.payload.len + signatureLen
|
|
||||||
|
|
||||||
let padLen =
|
|
||||||
if self.padding.isSome(): self.padding.get().len
|
|
||||||
# is there a reason why 256 bytes are padded when the dataLen is 256?
|
|
||||||
else: padMaxLen - (dataLen mod padMaxLen)
|
|
||||||
|
|
||||||
# buffer space that we need to allocate
|
|
||||||
let totalLen = dataLen + padLen
|
|
||||||
|
|
||||||
var plain = newSeqOfCap[byte](totalLen)
|
|
||||||
|
|
||||||
let signatureFlag =
|
|
||||||
if self.src.isSome(): signatureBits
|
|
||||||
else: 0'u8
|
|
||||||
|
|
||||||
# byte 0: flags with payload length length and presence of signature
|
|
||||||
plain.add payloadLenLen or signatureFlag
|
|
||||||
|
|
||||||
# next, length of payload - little endian (who comes up with this stuff? why
|
|
||||||
# can't the world just settle on one endian?)
|
|
||||||
let payloadLenLE = self.payload.len.uint32.toBytesLE
|
|
||||||
|
|
||||||
# No, I have no love for nim closed ranges - such a mess to remember the extra
|
|
||||||
# < or risk off-by-ones when working with lengths..
|
|
||||||
plain.add payloadLenLE[0..<payloadLenLen]
|
|
||||||
plain.add self.payload
|
|
||||||
|
|
||||||
if self.padding.isSome():
|
|
||||||
plain.add self.padding.get()
|
|
||||||
else:
|
|
||||||
var padding = newSeq[byte](padLen)
|
|
||||||
rng.generate(padding)
|
|
||||||
|
|
||||||
plain.add padding
|
|
||||||
|
|
||||||
if self.src.isSome(): # Private key present - signature requested
|
|
||||||
let sig = sign(self.src.get(), plain)
|
|
||||||
|
|
||||||
plain.add sig.toRaw()
|
|
||||||
|
|
||||||
if self.dst.isSome(): # Asymmetric key present - encryption requested
|
|
||||||
var res = newSeq[byte](eciesEncryptedLength(plain.len))
|
|
||||||
let err = eciesEncrypt(rng, plain, res, self.dst.get())
|
|
||||||
if err.isErr:
|
|
||||||
notice "Encryption failed", err = err.error
|
|
||||||
return
|
|
||||||
return some(res)
|
|
||||||
|
|
||||||
if self.symKey.isSome(): # Symmetric key present - encryption requested
|
|
||||||
let iv = rng.generate(array[gcmIVLen, byte])
|
|
||||||
|
|
||||||
return some(encryptAesGcm(plain, self.symKey.get(), iv))
|
|
||||||
|
|
||||||
# No encryption!
|
|
||||||
return some(plain)
|
|
||||||
|
|
||||||
proc decode*(data: openArray[byte], dst = none[PrivateKey](),
|
|
||||||
symKey = none[SymKey]()): Option[DecodedPayload] =
|
|
||||||
## Decode data into payload, potentially trying to decrypt if keys are
|
|
||||||
## provided
|
|
||||||
|
|
||||||
# Careful throughout - data coming from unknown source - malformatted data
|
|
||||||
# expected
|
|
||||||
|
|
||||||
var res: DecodedPayload
|
|
||||||
|
|
||||||
var plain: seq[byte]
|
|
||||||
if dst.isSome():
|
|
||||||
# XXX: eciesDecryptedLength is pretty fragile, API-wise.. is this really the
|
|
||||||
# way to check for errors / sufficient length?
|
|
||||||
let plainLen = eciesDecryptedLength(data.len)
|
|
||||||
if plainLen < 0:
|
|
||||||
debug "Not enough data to decrypt", len = data.len
|
|
||||||
return
|
|
||||||
|
|
||||||
plain.setLen(eciesDecryptedLength(data.len))
|
|
||||||
if eciesDecrypt(data, plain, dst.get()).isErr:
|
|
||||||
debug "Couldn't decrypt using asymmetric key", len = data.len
|
|
||||||
return
|
|
||||||
elif symKey.isSome():
|
|
||||||
let tmp = decryptAesGcm(data, symKey.get())
|
|
||||||
if tmp.isNone():
|
|
||||||
debug "Couldn't decrypt using symmetric key", len = data.len
|
|
||||||
return
|
|
||||||
|
|
||||||
plain = tmp.get()
|
|
||||||
else: # No encryption!
|
|
||||||
plain = @data
|
|
||||||
|
|
||||||
if plain.len < 2: # Minimum 1 byte flags, 1 byte payload len
|
|
||||||
debug "Missing flags or payload length", len = plain.len
|
|
||||||
return
|
|
||||||
|
|
||||||
var pos = 0
|
|
||||||
|
|
||||||
let payloadLenLen = int(plain[pos] and 0b11'u8)
|
|
||||||
let hasSignature = (plain[pos] and 0b100'u8) != 0
|
|
||||||
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
if plain.len < pos + payloadLenLen:
|
|
||||||
debug "Missing payload length", len = plain.len, pos, payloadLenLen
|
|
||||||
return
|
|
||||||
|
|
||||||
var payloadLenLE: array[4, byte]
|
|
||||||
|
|
||||||
for i in 0..<payloadLenLen: payloadLenLE[i] = plain[pos + i]
|
|
||||||
pos += payloadLenLen
|
|
||||||
|
|
||||||
let payloadLen = int(fromBytesLE(uint32, payloadLenLE))
|
|
||||||
if plain.len < pos + payloadLen:
|
|
||||||
debug "Missing payload", len = plain.len, pos, payloadLen
|
|
||||||
return
|
|
||||||
|
|
||||||
res.payload = plain[pos ..< pos + payloadLen]
|
|
||||||
|
|
||||||
pos += payloadLen
|
|
||||||
|
|
||||||
if hasSignature:
|
|
||||||
if plain.len < (keys.RawSignatureSize + pos):
|
|
||||||
debug "Missing expected signature", len = plain.len
|
|
||||||
return
|
|
||||||
|
|
||||||
let sig = Signature.fromRaw(plain[^keys.RawSignatureSize .. ^1])
|
|
||||||
let key = sig and recover(
|
|
||||||
sig[], plain.toOpenArray(0, plain.len - keys.RawSignatureSize - 1))
|
|
||||||
if key.isErr:
|
|
||||||
debug "Failed to recover signature key", err = key.error
|
|
||||||
return
|
|
||||||
res.src = some(key[])
|
|
||||||
|
|
||||||
if hasSignature:
|
|
||||||
if plain.len > pos + keys.RawSignatureSize:
|
|
||||||
res.padding = some(plain[pos .. ^(keys.RawSignatureSize+1)])
|
|
||||||
else:
|
|
||||||
if plain.len > pos:
|
|
||||||
res.padding = some(plain[pos .. ^1])
|
|
||||||
|
|
||||||
return some(res)
|
|
||||||
|
|
||||||
# Envelopes --------------------------------------------------------------------
|
|
||||||
|
|
||||||
proc valid*(self: Envelope, now = epochTime()): bool =
|
|
||||||
if self.expiry.float64 < now: # expired
|
|
||||||
envelopes_dropped.inc(labelValues = ["expired"])
|
|
||||||
return false
|
|
||||||
if self.ttl <= 0: # this would invalidate pow calculation
|
|
||||||
envelopes_dropped.inc(labelValues = ["expired"])
|
|
||||||
return false
|
|
||||||
|
|
||||||
let created = self.expiry - self.ttl
|
|
||||||
if created.float64 > (now + 2.0): # created in the future
|
|
||||||
envelopes_dropped.inc(labelValues = ["future_timestamp"])
|
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc len(self: Envelope): int = 20 + self.data.len
|
|
||||||
|
|
||||||
proc toShortRlp*(self: Envelope): seq[byte] =
|
|
||||||
## RLP-encoded message without nonce is used during proof-of-work calculations
|
|
||||||
rlp.encodeList(self.expiry, self.ttl, self.topic, self.data)
|
|
||||||
|
|
||||||
proc toRlp(self: Envelope): seq[byte] =
|
|
||||||
## What gets sent out over the wire includes the nonce
|
|
||||||
rlp.encode(self)
|
|
||||||
|
|
||||||
proc minePow*(self: Envelope, seconds: float, bestBitTarget: int = 0): (uint64, Hash) =
|
|
||||||
## For the given envelope, spend millis milliseconds to find the
|
|
||||||
## best proof-of-work and return the nonce
|
|
||||||
let bytes = self.toShortRlp()
|
|
||||||
|
|
||||||
var ctx: keccak256
|
|
||||||
ctx.init()
|
|
||||||
ctx.update(bytes)
|
|
||||||
|
|
||||||
var bestBit: int = 0
|
|
||||||
|
|
||||||
let mineEnd = epochTime() + seconds
|
|
||||||
|
|
||||||
var i: uint64
|
|
||||||
while epochTime() < mineEnd or bestBit == 0: # At least one round
|
|
||||||
var tmp = ctx # copy hash calculated so far - we'll reuse that for each iter
|
|
||||||
tmp.update(i.toBytesBE())
|
|
||||||
# XXX:a random nonce here would not leak number of iters
|
|
||||||
let hash = tmp.finish()
|
|
||||||
let zeroBits = leadingZeroBits(hash)
|
|
||||||
if zeroBits > bestBit: # XXX: could also compare hashes as numbers instead
|
|
||||||
bestBit = zeroBits
|
|
||||||
result = (i, hash)
|
|
||||||
if bestBitTarget > 0 and bestBit >= bestBitTarget:
|
|
||||||
break
|
|
||||||
|
|
||||||
i.inc
|
|
||||||
|
|
||||||
proc calcPowHash*(self: Envelope): Hash =
|
|
||||||
## Calculate the message hash, as done during mining - this can be used to
|
|
||||||
## verify proof-of-work
|
|
||||||
|
|
||||||
let bytes = self.toShortRlp()
|
|
||||||
|
|
||||||
var ctx: keccak256
|
|
||||||
ctx.init()
|
|
||||||
ctx.update(bytes)
|
|
||||||
ctx.update(self.nonce.toBytesBE())
|
|
||||||
return ctx.finish()
|
|
||||||
|
|
||||||
# Messages ---------------------------------------------------------------------
|
|
||||||
|
|
||||||
proc cmpPow(a, b: Message): int =
|
|
||||||
## Biggest pow first, lowest at the end (for easy popping)
|
|
||||||
if a.pow < b.pow: 1
|
|
||||||
elif a.pow == b.pow: 0
|
|
||||||
else: -1
|
|
||||||
|
|
||||||
proc initMessage*(env: Envelope, powCalc = true): Message =
|
|
||||||
result.env = env
|
|
||||||
result.size = env.toRlp().len().uint32 # XXX: calc len without creating RLP
|
|
||||||
result.bloom = topicBloom(env.topic)
|
|
||||||
if powCalc:
|
|
||||||
result.hash = env.calcPowHash()
|
|
||||||
result.pow = calcPow(result.env.len.uint32, result.env.ttl, result.hash)
|
|
||||||
trace "Message PoW", pow = result.pow.formatFloat(ffScientific)
|
|
||||||
|
|
||||||
proc hash*(hash: Hash): hashes.Hash = hashes.hash(hash.data)
|
|
||||||
|
|
||||||
# NOTE: Hashing and leading zeroes calculation is now the same between geth,
|
|
||||||
# parity and this implementation.
|
|
||||||
# However, there is still a difference in the size calculation.
|
|
||||||
# See also here: https://github.com/ethereum/go-ethereum/pull/19753
|
|
||||||
# This implementation is not conform EIP-627 as we do not use the size of the
|
|
||||||
# RLP-encoded envelope, but the size of the envelope object itself.
|
|
||||||
# This is done to be able to correctly calculate the bestBitTarget.
|
|
||||||
# Other options would be:
|
|
||||||
# - work directly with powTarget in minePow, but this requires recalculation of
|
|
||||||
# rlp size + calcPow
|
|
||||||
# - Use worst case size of envelope nonce
|
|
||||||
# - Mine PoW for x interval, calcPow of best result, if target not met .. repeat
|
|
||||||
proc sealEnvelope*(msg: var Message, powTime: float, powTarget: float): bool =
|
|
||||||
let size = msg.env.len
|
|
||||||
if powTarget > 0:
|
|
||||||
let x = powTarget * size.float * msg.env.ttl.float
|
|
||||||
var bestBitTarget: int
|
|
||||||
if x <= 1: # log() would return negative numbers or 0
|
|
||||||
bestBitTarget = 1
|
|
||||||
else:
|
|
||||||
bestBitTarget = ceil(log(x, 2)).int
|
|
||||||
(msg.env.nonce, msg.hash) = msg.env.minePow(powTime, bestBitTarget)
|
|
||||||
else:
|
|
||||||
# If no target is set, we are certain of executed powTime
|
|
||||||
msg.env.expiry += powTime.uint32
|
|
||||||
(msg.env.nonce, msg.hash) = msg.env.minePow(powTime)
|
|
||||||
|
|
||||||
msg.pow = calcPow(size.uint32, msg.env.ttl, msg.hash)
|
|
||||||
trace "Message PoW", pow = msg.pow
|
|
||||||
if msg.pow < powTarget:
|
|
||||||
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().uint32
|
|
||||||
|
|
||||||
# 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].hash)
|
|
||||||
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
|
|
||||||
## * lowest proof-of-work message - this may be `msg` itself!
|
|
||||||
|
|
||||||
# check for duplicate before pruning
|
|
||||||
if self.itemHashes.contains(msg.hash):
|
|
||||||
envelopes_dropped.inc(labelValues = ["benign_duplicate"])
|
|
||||||
return false
|
|
||||||
else:
|
|
||||||
envelopes_valid.inc()
|
|
||||||
if self.items.len >= self.capacity:
|
|
||||||
self.prune() # Only prune if needed
|
|
||||||
|
|
||||||
if self.items.len >= self.capacity:
|
|
||||||
# Still no room - go by proof-of-work quantity
|
|
||||||
let last = self.items[^1]
|
|
||||||
|
|
||||||
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
|
|
||||||
envelopes_dropped.inc(labelValues = ["full_queue_new"])
|
|
||||||
return false
|
|
||||||
|
|
||||||
self.items.del(self.items.len() - 1)
|
|
||||||
self.itemHashes.excl(last.hash)
|
|
||||||
envelopes_dropped.inc(labelValues = ["full_queue_old"])
|
|
||||||
|
|
||||||
self.itemHashes.incl(msg.hash)
|
|
||||||
self.items.insert(msg, self.items.lowerBound(msg, cmpPow))
|
|
||||||
return true
|
|
||||||
|
|
||||||
# Filters ----------------------------------------------------------------------
|
|
||||||
proc initFilter*(src = none[PublicKey](), privateKey = none[PrivateKey](),
|
|
||||||
symKey = none[SymKey](), topics: seq[Topic] = @[],
|
|
||||||
powReq = 0.0, allowP2P = false): Filter =
|
|
||||||
# Zero topics will give an empty bloom filter which is fine as this bloom
|
|
||||||
# filter is only used to `or` with existing/other bloom filters. Not to do
|
|
||||||
# matching.
|
|
||||||
Filter(src: src, privateKey: privateKey, symKey: symKey, topics: topics,
|
|
||||||
powReq: powReq, allowP2P: allowP2P, bloom: toBloom(topics))
|
|
||||||
|
|
||||||
proc subscribeFilter*(
|
|
||||||
rng: var HmacDrbgContext, filters: var Filters, filter: Filter,
|
|
||||||
handler: FilterMsgHandler = nil): 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.
|
|
||||||
let id = generateRandomID(rng)
|
|
||||||
var filter = filter
|
|
||||||
if handler.isNil():
|
|
||||||
filter.queue = newSeqOfCap[ReceivedMessage](defaultFilterQueueCapacity)
|
|
||||||
else:
|
|
||||||
filter.handler = handler
|
|
||||||
|
|
||||||
filters.add(id, filter)
|
|
||||||
debug "Filter added", filter = id
|
|
||||||
return id
|
|
||||||
|
|
||||||
proc notify*(filters: var Filters, msg: Message) {.gcsafe.} =
|
|
||||||
var decoded: Option[DecodedPayload]
|
|
||||||
var keyHash: Hash
|
|
||||||
var dst: Option[PublicKey]
|
|
||||||
|
|
||||||
for filter in filters.mvalues:
|
|
||||||
if not filter.allowP2P and msg.isP2P:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# if message is direct p2p PoW doesn't matter
|
|
||||||
if msg.pow < filter.powReq and not msg.isP2P:
|
|
||||||
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 decoded.isNone():
|
|
||||||
continue
|
|
||||||
if filter.privateKey.isSome():
|
|
||||||
keyHash = keccak256.digest(filter.privateKey.get().toRaw())
|
|
||||||
# TODO: Get rid of the hash and just use pubkey to compare?
|
|
||||||
dst = some(toPublicKey(filter.privateKey.get()))
|
|
||||||
elif filter.symKey.isSome():
|
|
||||||
keyHash = keccak256.digest(filter.symKey.get())
|
|
||||||
# else:
|
|
||||||
# NOTE: In this case the message was not encrypted
|
|
||||||
else:
|
|
||||||
if filter.privateKey.isSome():
|
|
||||||
if keyHash != keccak256.digest(filter.privateKey.get().toRaw()):
|
|
||||||
continue
|
|
||||||
elif filter.symKey.isSome():
|
|
||||||
if keyHash != keccak256.digest(filter.symKey.get()):
|
|
||||||
continue
|
|
||||||
# else:
|
|
||||||
# NOTE: In this case the message was not encrypted
|
|
||||||
|
|
||||||
# When decoding is done we can check the src (signature)
|
|
||||||
if filter.src.isSome():
|
|
||||||
let src: Option[PublicKey] = decoded.get().src
|
|
||||||
if not src.isSome():
|
|
||||||
continue
|
|
||||||
elif src.get() != filter.src.get():
|
|
||||||
continue
|
|
||||||
|
|
||||||
let receivedMsg = ReceivedMessage(decoded: decoded.get(),
|
|
||||||
timestamp: msg.env.expiry - msg.env.ttl,
|
|
||||||
ttl: msg.env.ttl,
|
|
||||||
topic: msg.env.topic,
|
|
||||||
pow: msg.pow,
|
|
||||||
hash: msg.hash,
|
|
||||||
dst: dst)
|
|
||||||
# Either run callback or add to queue
|
|
||||||
if filter.handler.isNil():
|
|
||||||
filter.queue.insert(receivedMsg)
|
|
||||||
else:
|
|
||||||
filter.handler(receivedMsg)
|
|
||||||
|
|
||||||
proc getFilterMessages*(filters: var Filters, filterId: string):
|
|
||||||
seq[ReceivedMessage] {.raises: [KeyError, Defect].} =
|
|
||||||
result = @[]
|
|
||||||
if filters.contains(filterId):
|
|
||||||
if filters[filterId].handler.isNil():
|
|
||||||
shallowCopy(result, filters[filterId].queue)
|
|
||||||
filters[filterId].queue =
|
|
||||||
newSeqOfCap[ReceivedMessage](defaultFilterQueueCapacity)
|
|
||||||
|
|
||||||
proc toBloom*(filters: Filters): Bloom =
|
|
||||||
for filter in filters.values:
|
|
||||||
if filter.topics.len > 0:
|
|
||||||
result = result or filter.bloom
|
|
|
@ -1,478 +0,0 @@
|
||||||
# nim-eth - Whisper
|
|
||||||
# Copyright (c) 2018-2021 Status Research & Development GmbH
|
|
||||||
# Licensed and distributed under either of
|
|
||||||
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
||||||
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
||||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
||||||
|
|
||||||
## Whisper
|
|
||||||
## *******
|
|
||||||
##
|
|
||||||
## Whisper is a gossip protocol that synchronizes a set of messages across nodes
|
|
||||||
## with attention given to sender and recipient anonymitiy. Messages are
|
|
||||||
## categorized by a topic and stay alive in the network based on a time-to-live
|
|
||||||
## measured in seconds. Spam prevention is based on proof-of-work, where large
|
|
||||||
## or long-lived messages must spend more work.
|
|
||||||
##
|
|
||||||
## Example usage
|
|
||||||
## ----------
|
|
||||||
## First an `EthereumNode` needs to be created, either with all capabilities set
|
|
||||||
## or with specifically the Whisper capability set.
|
|
||||||
## The latter can be done like this:
|
|
||||||
##
|
|
||||||
## .. code-block::nim
|
|
||||||
## var node = newEthereumNode(keypair, address, netId, nil,
|
|
||||||
## addAllCapabilities = false)
|
|
||||||
## node.addCapability Whisper
|
|
||||||
##
|
|
||||||
## Now calls such as ``postMessage`` and ``subscribeFilter`` can be done.
|
|
||||||
## However, they only make real sense after ``connectToNetwork`` was started. As
|
|
||||||
## else there will be no peers to send and receive messages from.
|
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
import
|
|
||||||
std/[options, tables, times],
|
|
||||||
chronos, chronicles, metrics,
|
|
||||||
".."/../[keys, async_utils, p2p],
|
|
||||||
./whisper/whisper_types
|
|
||||||
|
|
||||||
export
|
|
||||||
whisper_types
|
|
||||||
|
|
||||||
logScope:
|
|
||||||
topics = "whisper"
|
|
||||||
|
|
||||||
const
|
|
||||||
defaultQueueCapacity = 2048
|
|
||||||
whisperVersion* = 6 ## Whisper version.
|
|
||||||
whisperVersionStr* = $whisperVersion ## Whisper version.
|
|
||||||
defaultMinPow* = 0.2'f64 ## The default minimum PoW requirement for this node.
|
|
||||||
defaultMaxMsgSize* = 1024'u32 * 1024'u32 ## The current default and max
|
|
||||||
## message size. This can never be larger than the maximum RLPx message size.
|
|
||||||
messageInterval* = chronos.milliseconds(300) ## Interval at which messages are
|
|
||||||
## send to peers, in ms.
|
|
||||||
pruneInterval* = chronos.milliseconds(1000) ## Interval at which message
|
|
||||||
## queue is pruned, in ms.
|
|
||||||
|
|
||||||
type
|
|
||||||
WhisperConfig* = object
|
|
||||||
powRequirement*: float64
|
|
||||||
bloom*: Bloom
|
|
||||||
isLightNode*: bool
|
|
||||||
maxMsgSize*: uint32
|
|
||||||
|
|
||||||
WhisperPeer = ref object
|
|
||||||
initialized: bool # when successfully completed the handshake
|
|
||||||
powRequirement*: float64
|
|
||||||
bloom*: Bloom
|
|
||||||
isLightNode*: bool
|
|
||||||
trusted*: bool
|
|
||||||
received: HashSet[Hash]
|
|
||||||
|
|
||||||
WhisperNetwork = ref object
|
|
||||||
queue*: ref Queue
|
|
||||||
filters*: Filters
|
|
||||||
config*: WhisperConfig
|
|
||||||
|
|
||||||
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:
|
|
||||||
envelopes_dropped.inc(labelValues = ["too_large"])
|
|
||||||
warn "Message size too large", size = msg.size
|
|
||||||
return false
|
|
||||||
|
|
||||||
if msg.pow < config.powRequirement:
|
|
||||||
envelopes_dropped.inc(labelValues = ["low_pow"])
|
|
||||||
warn "Message PoW too low", pow = msg.pow, minPow = config.powRequirement
|
|
||||||
return false
|
|
||||||
|
|
||||||
if not bloomFilterMatch(config.bloom, msg.bloom):
|
|
||||||
envelopes_dropped.inc(labelValues = ["bloom_filter_mismatch"])
|
|
||||||
warn "Message does not match node bloom filter"
|
|
||||||
return false
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc run(peer: Peer) {.gcsafe, async, raises: [Defect].}
|
|
||||||
proc run(node: EthereumNode, network: WhisperNetwork)
|
|
||||||
{.gcsafe, async, raises: [Defect].}
|
|
||||||
|
|
||||||
proc initProtocolState*(network: WhisperNetwork, node: EthereumNode) {.gcsafe.} =
|
|
||||||
new(network.queue)
|
|
||||||
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
|
|
||||||
asyncSpawn node.run(network)
|
|
||||||
|
|
||||||
p2pProtocol Whisper(version = whisperVersion,
|
|
||||||
rlpxName = "shh",
|
|
||||||
peerState = WhisperPeer,
|
|
||||||
networkState = WhisperNetwork):
|
|
||||||
|
|
||||||
onPeerConnected do (peer: Peer):
|
|
||||||
trace "onPeerConnected Whisper"
|
|
||||||
let
|
|
||||||
whisperNet = peer.networkState
|
|
||||||
whisperPeer = peer.state
|
|
||||||
|
|
||||||
let m = await peer.status(whisperVersion,
|
|
||||||
cast[uint64](whisperNet.config.powRequirement),
|
|
||||||
@(whisperNet.config.bloom),
|
|
||||||
whisperNet.config.isLightNode,
|
|
||||||
timeout = chronos.milliseconds(5000))
|
|
||||||
|
|
||||||
if m.protocolVersion == whisperVersion:
|
|
||||||
debug "Whisper peer", peer, whisperVersion
|
|
||||||
else:
|
|
||||||
raise newException(UselessPeerError, "Incompatible Whisper version")
|
|
||||||
|
|
||||||
whisperPeer.powRequirement = cast[float64](m.powConverted)
|
|
||||||
|
|
||||||
if m.bloom.len > 0:
|
|
||||||
if m.bloom.len != bloomSize:
|
|
||||||
raise newException(UselessPeerError, "Bloomfilter size mismatch")
|
|
||||||
else:
|
|
||||||
whisperPeer.bloom.bytesCopy(m.bloom)
|
|
||||||
else:
|
|
||||||
# If no bloom filter is send we allow all
|
|
||||||
whisperPeer.bloom = fullBloom()
|
|
||||||
|
|
||||||
whisperPeer.isLightNode = m.isLightNode
|
|
||||||
if whisperPeer.isLightNode and whisperNet.config.isLightNode:
|
|
||||||
# No sense in connecting two light nodes so we disconnect
|
|
||||||
raise newException(UselessPeerError, "Two light nodes connected")
|
|
||||||
|
|
||||||
whisperPeer.received.init()
|
|
||||||
whisperPeer.trusted = false
|
|
||||||
whisperPeer.initialized = true
|
|
||||||
|
|
||||||
if not whisperNet.config.isLightNode:
|
|
||||||
asyncSpawn peer.run()
|
|
||||||
|
|
||||||
debug "Whisper peer initialized", peer
|
|
||||||
|
|
||||||
handshake:
|
|
||||||
proc status(peer: Peer,
|
|
||||||
protocolVersion: uint,
|
|
||||||
powConverted: uint64,
|
|
||||||
bloom: seq[byte],
|
|
||||||
isLightNode: bool)
|
|
||||||
|
|
||||||
proc messages(peer: Peer, envelopes: openArray[Envelope]) =
|
|
||||||
if not peer.state.initialized:
|
|
||||||
warn "Handshake not completed yet, discarding messages"
|
|
||||||
return
|
|
||||||
|
|
||||||
for envelope in envelopes:
|
|
||||||
# check if expired or in future, or ttl not 0
|
|
||||||
if not envelope.valid():
|
|
||||||
warn "Expired or future timed envelope", peer
|
|
||||||
# disconnect from peers sending bad envelopes
|
|
||||||
# await peer.disconnect(SubprotocolReason)
|
|
||||||
continue
|
|
||||||
|
|
||||||
let msg = initMessage(envelope)
|
|
||||||
if not msg.allowed(peer.networkState.config):
|
|
||||||
# disconnect from peers sending bad envelopes
|
|
||||||
# await peer.disconnect(SubprotocolReason)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# This peer send this message thus should not receive it again.
|
|
||||||
# If this peer has the message in the `received` set already, this means
|
|
||||||
# it was either already received here from this peer or send to this peer.
|
|
||||||
# Either way it will be in our queue already (and the peer should know
|
|
||||||
# this) and this peer is sending duplicates.
|
|
||||||
# Note: geth does not check if a peer has send a message to them before
|
|
||||||
# broadcasting this message. This too is seen here as a duplicate message
|
|
||||||
# (see above comment). If we want to seperate these cases (e.g. when peer
|
|
||||||
# rating), then we have to add a "peer.state.send" HashSet.
|
|
||||||
# Note: it could also be a race between the arrival of a message send by
|
|
||||||
# this node to a peer and that same message arriving from that peer (after
|
|
||||||
# it was received from another peer) here.
|
|
||||||
if peer.state.received.containsOrIncl(msg.hash):
|
|
||||||
envelopes_dropped.inc(labelValues = ["duplicate"])
|
|
||||||
trace "Peer sending duplicate messages", peer, hash = $msg.hash
|
|
||||||
# await peer.disconnect(SubprotocolReason)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# This can still be a duplicate message, but from another peer than
|
|
||||||
# the peer who send the message.
|
|
||||||
if peer.networkState.queue[].add(msg):
|
|
||||||
# notify filters of this message
|
|
||||||
peer.networkState.filters.notify(msg)
|
|
||||||
|
|
||||||
proc powRequirement(peer: Peer, value: uint64) =
|
|
||||||
if not peer.state.initialized:
|
|
||||||
warn "Handshake not completed yet, discarding powRequirement"
|
|
||||||
return
|
|
||||||
|
|
||||||
peer.state.powRequirement = cast[float64](value)
|
|
||||||
|
|
||||||
proc bloomFilterExchange(peer: Peer, bloom: openArray[byte]) =
|
|
||||||
if not peer.state.initialized:
|
|
||||||
warn "Handshake not completed yet, discarding bloomFilterExchange"
|
|
||||||
return
|
|
||||||
|
|
||||||
if bloom.len == bloomSize:
|
|
||||||
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) =
|
|
||||||
if peer.state.trusted:
|
|
||||||
# when trusted we can bypass any checks on envelope
|
|
||||||
let msg = Message(env: envelope, isP2P: true)
|
|
||||||
peer.networkState.filters.notify(msg)
|
|
||||||
|
|
||||||
# Following message IDs are not part of EIP-627, but are added and used by
|
|
||||||
# the Status application, we ignore them for now.
|
|
||||||
nextID 11
|
|
||||||
proc batchAcknowledged(peer: Peer) = discard
|
|
||||||
proc messageResponse(peer: Peer) = discard
|
|
||||||
|
|
||||||
nextID 123
|
|
||||||
requestResponse:
|
|
||||||
proc p2pSyncRequest(peer: Peer) = discard
|
|
||||||
proc p2pSyncResponse(peer: Peer) = discard
|
|
||||||
|
|
||||||
proc p2pRequestComplete(peer: Peer) = discard
|
|
||||||
|
|
||||||
# 'Runner' calls ---------------------------------------------------------------
|
|
||||||
|
|
||||||
proc processQueue(peer: Peer) =
|
|
||||||
# Send to peer all valid and previously not send envelopes in the queue.
|
|
||||||
var
|
|
||||||
envelopes: seq[Envelope] = @[]
|
|
||||||
whisperPeer = peer.state(Whisper)
|
|
||||||
whisperNet = peer.networkState(Whisper)
|
|
||||||
|
|
||||||
for message in whisperNet.queue.items:
|
|
||||||
if whisperPeer.received.contains(message.hash):
|
|
||||||
# trace "message was already send to peer", hash = $message.hash, peer
|
|
||||||
continue
|
|
||||||
|
|
||||||
if message.pow < whisperPeer.powRequirement:
|
|
||||||
trace "Message PoW too low for peer", pow = message.pow,
|
|
||||||
powReq = whisperPeer.powRequirement
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not bloomFilterMatch(whisperPeer.bloom, message.bloom):
|
|
||||||
trace "Message does not match peer bloom filter"
|
|
||||||
continue
|
|
||||||
|
|
||||||
trace "Adding envelope"
|
|
||||||
envelopes.add(message.env)
|
|
||||||
whisperPeer.received.incl(message.hash)
|
|
||||||
|
|
||||||
if envelopes.len() > 0:
|
|
||||||
trace "Sending envelopes", amount=envelopes.len
|
|
||||||
# Ignore failure of sending messages, this could occur when the connection
|
|
||||||
# gets dropped
|
|
||||||
traceAsyncErrors peer.messages(envelopes)
|
|
||||||
|
|
||||||
proc run(peer: Peer) {.async.} =
|
|
||||||
while peer.connectionState notin {Disconnecting, Disconnected}:
|
|
||||||
peer.processQueue()
|
|
||||||
await sleepAsync(messageInterval)
|
|
||||||
|
|
||||||
proc pruneReceived(node: EthereumNode) =
|
|
||||||
if node.peerPool != nil: # XXX: a bit dirty to need to check for this here ...
|
|
||||||
var whisperNet = node.protocolState(Whisper)
|
|
||||||
|
|
||||||
for peer in node.protocolPeers(Whisper):
|
|
||||||
if not peer.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.received = intersection(peer.received, whisperNet.queue.itemHashes)
|
|
||||||
|
|
||||||
proc run(node: EthereumNode, network: WhisperNetwork) {.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(pruneInterval)
|
|
||||||
|
|
||||||
# Private EthereumNode calls ---------------------------------------------------
|
|
||||||
|
|
||||||
proc sendP2PMessage(node: EthereumNode, peerId: NodeId, env: Envelope): bool =
|
|
||||||
for peer in node.peers(Whisper):
|
|
||||||
if peer.remote.id == peerId:
|
|
||||||
let f = peer.p2pMessage(env)
|
|
||||||
# Can't make p2pMessage not raise so this is the "best" option I can think
|
|
||||||
# of instead of using asyncSpawn and still keeping the call not async.
|
|
||||||
f.callback = proc(data: pointer) {.gcsafe, raises: [Defect].} =
|
|
||||||
if f.failed:
|
|
||||||
warn "P2PMessage send failed", msg = f.readError.msg
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc queueMessage(node: EthereumNode, msg: Message): bool =
|
|
||||||
|
|
||||||
var whisperNet = node.protocolState(Whisper)
|
|
||||||
# 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.
|
|
||||||
if not msg.allowed(whisperNet.config):
|
|
||||||
return false
|
|
||||||
|
|
||||||
trace "Adding message to queue", hash = $msg.hash
|
|
||||||
if whisperNet.queue[].add(msg):
|
|
||||||
# Also notify our own filters of the message we are sending,
|
|
||||||
# e.g. msg from local Dapp to Dapp
|
|
||||||
whisperNet.filters.notify(msg)
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|
||||||
# Public EthereumNode calls ----------------------------------------------------
|
|
||||||
|
|
||||||
proc postMessage*(node: EthereumNode, pubKey = none[PublicKey](),
|
|
||||||
symKey = none[SymKey](), src = none[PrivateKey](),
|
|
||||||
ttl: uint32, topic: whisper_types.Topic, payload: seq[byte],
|
|
||||||
padding = none[seq[byte]](), powTime = 1'f,
|
|
||||||
powTarget = defaultMinPow,
|
|
||||||
targetPeer = none[NodeId]()): bool =
|
|
||||||
## Post a message on the message queue which will be processed at the
|
|
||||||
## next `messageInterval`.
|
|
||||||
##
|
|
||||||
## NOTE: This call allows a post without encryption. If encryption is
|
|
||||||
## mandatory it should be enforced a layer up
|
|
||||||
let payload = encode(node.rng[], Payload(
|
|
||||||
payload: payload, src: src, dst: pubKey, symKey: symKey, padding: padding))
|
|
||||||
if payload.isSome():
|
|
||||||
var env = Envelope(expiry:epochTime().uint32 + ttl,
|
|
||||||
ttl: ttl, topic: topic, data: payload.get(), nonce: 0)
|
|
||||||
|
|
||||||
# Allow lightnode to post only direct p2p messages
|
|
||||||
if targetPeer.isSome():
|
|
||||||
return node.sendP2PMessage(targetPeer.get(), env)
|
|
||||||
elif not node.protocolState(Whisper).config.isLightNode:
|
|
||||||
# non direct p2p message can not have ttl of 0
|
|
||||||
if env.ttl == 0:
|
|
||||||
return false
|
|
||||||
var msg = initMessage(env, powCalc = false)
|
|
||||||
# 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
|
|
||||||
# zah: It would be hard to execute this in a background thread at the
|
|
||||||
# moment. We'll need a way to send custom "tasks" to the async message
|
|
||||||
# loop (e.g. AD2 support for AsyncChannels).
|
|
||||||
if not msg.sealEnvelope(powTime, powTarget):
|
|
||||||
return false
|
|
||||||
|
|
||||||
# need to check expiry after mining PoW
|
|
||||||
if not msg.env.valid():
|
|
||||||
return false
|
|
||||||
|
|
||||||
return node.queueMessage(msg)
|
|
||||||
else:
|
|
||||||
warn "Light node not allowed to post messages"
|
|
||||||
return false
|
|
||||||
else:
|
|
||||||
error "Encoding of payload failed"
|
|
||||||
return false
|
|
||||||
|
|
||||||
proc subscribeFilter*(node: EthereumNode, filter: Filter,
|
|
||||||
handler:FilterMsgHandler = nil): string =
|
|
||||||
## Initiate a filter for incoming/outgoing messages. Messages can be
|
|
||||||
## retrieved with the `getFilterMessages` call or with a provided
|
|
||||||
## `FilterMsgHandler`.
|
|
||||||
##
|
|
||||||
## NOTE: This call allows for a filter without decryption. If encryption is
|
|
||||||
## mandatory it should be enforced a layer up.
|
|
||||||
return subscribeFilter(
|
|
||||||
node.rng[], node.protocolState(Whisper).filters, filter, handler)
|
|
||||||
|
|
||||||
proc unsubscribeFilter*(node: EthereumNode, filterId: string): bool =
|
|
||||||
## Remove a previously subscribed filter.
|
|
||||||
var filter: Filter
|
|
||||||
return node.protocolState(Whisper).filters.take(filterId, filter)
|
|
||||||
|
|
||||||
proc getFilterMessages*(node: EthereumNode, filterId: string):
|
|
||||||
seq[ReceivedMessage] {.raises: [KeyError, Defect].} =
|
|
||||||
## Get all the messages currently in the filter queue. This will reset the
|
|
||||||
## filter message queue.
|
|
||||||
return node.protocolState(Whisper).filters.getFilterMessages(filterId)
|
|
||||||
|
|
||||||
proc filtersToBloom*(node: EthereumNode): Bloom =
|
|
||||||
## Returns the bloom filter of all topics of all subscribed filters.
|
|
||||||
return node.protocolState(Whisper).filters.toBloom()
|
|
||||||
|
|
||||||
proc setPowRequirement*(node: EthereumNode, powReq: float64) {.async.} =
|
|
||||||
## Sets the PoW requirement for this node, will also send
|
|
||||||
## this new PoW requirement to all connected peers.
|
|
||||||
##
|
|
||||||
## Failures when sending messages to peers will not be reported.
|
|
||||||
# NOTE: do we need a tolerance of old PoW for some time?
|
|
||||||
node.protocolState(Whisper).config.powRequirement = powReq
|
|
||||||
var futures: seq[Future[void]] = @[]
|
|
||||||
for peer in node.peers(Whisper):
|
|
||||||
futures.add(peer.powRequirement(cast[uint64](powReq)))
|
|
||||||
|
|
||||||
# Exceptions from sendMsg will not be raised
|
|
||||||
await allFutures(futures)
|
|
||||||
|
|
||||||
proc setBloomFilter*(node: EthereumNode, bloom: Bloom) {.async.} =
|
|
||||||
## Sets the bloom filter for this node, will also send
|
|
||||||
## this new bloom filter to all connected peers.
|
|
||||||
##
|
|
||||||
## Failures when sending messages to peers will not be reported.
|
|
||||||
# NOTE: do we need a tolerance of old bloom filter for some time?
|
|
||||||
node.protocolState(Whisper).config.bloom = bloom
|
|
||||||
var futures: seq[Future[void]] = @[]
|
|
||||||
for peer in node.peers(Whisper):
|
|
||||||
futures.add(peer.bloomFilterExchange(@bloom))
|
|
||||||
|
|
||||||
# Exceptions from sendMsg will not be raised
|
|
||||||
await allFutures(futures)
|
|
||||||
|
|
||||||
proc setMaxMessageSize*(node: EthereumNode, size: uint32): bool =
|
|
||||||
## Set the maximum allowed message size.
|
|
||||||
## Can not be set higher than ``defaultMaxMsgSize``.
|
|
||||||
if size > defaultMaxMsgSize:
|
|
||||||
warn "size > defaultMaxMsgSize"
|
|
||||||
return false
|
|
||||||
node.protocolState(Whisper).config.maxMsgSize = size
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc setPeerTrusted*(node: EthereumNode, peerId: NodeId): bool =
|
|
||||||
## Set a connected peer as trusted.
|
|
||||||
for peer in node.peers(Whisper):
|
|
||||||
if peer.remote.id == peerId:
|
|
||||||
peer.state(Whisper).trusted = true
|
|
||||||
return true
|
|
||||||
|
|
||||||
proc setLightNode*(node: EthereumNode, isLightNode: bool) =
|
|
||||||
## Set this node as a Whisper light node.
|
|
||||||
##
|
|
||||||
## NOTE: Should be run before connection is made with peers as this
|
|
||||||
## setting is only communicated at peer handshake.
|
|
||||||
node.protocolState(Whisper).config.isLightNode = isLightNode
|
|
||||||
|
|
||||||
proc configureWhisper*(node: EthereumNode, config: WhisperConfig) =
|
|
||||||
## Apply a Whisper configuration.
|
|
||||||
##
|
|
||||||
## NOTE: Should be run before connection is made with peers as some
|
|
||||||
## of the settings are only communicated at peer handshake.
|
|
||||||
node.protocolState(Whisper).config = config
|
|
||||||
|
|
||||||
proc resetMessageQueue*(node: EthereumNode) =
|
|
||||||
## Full reset of the message queue.
|
|
||||||
##
|
|
||||||
## NOTE: Not something that should be run in normal circumstances.
|
|
||||||
node.protocolState(Whisper).queue[] = initQueue(defaultQueueCapacity)
|
|
|
@ -1,20 +0,0 @@
|
||||||
import
|
|
||||||
std/[options, sequtils],
|
|
||||||
chronicles, testutils/fuzzing,
|
|
||||||
../../../eth/p2p/rlpx_protocols/whisper_protocol as whisper,
|
|
||||||
../../../eth/keys
|
|
||||||
|
|
||||||
test:
|
|
||||||
let
|
|
||||||
rng = newRng()
|
|
||||||
data = @payload.distribute(2)
|
|
||||||
whisperPayload = Payload(payload: data[0], padding: some(data[1]))
|
|
||||||
encoded = whisper.encode(rng[], whisperPayload)
|
|
||||||
|
|
||||||
decoded = whisper.decode(encoded.get())
|
|
||||||
|
|
||||||
doAssert data[0] == decoded.get().payload
|
|
||||||
if data[1].len > 0:
|
|
||||||
doAssert data[1] == decoded.get().padding.get()
|
|
||||||
else:
|
|
||||||
doAssert decoded.get().padding.isNone()
|
|
|
@ -1,52 +0,0 @@
|
||||||
#
|
|
||||||
# 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
|
|
||||||
std/options,
|
|
||||||
unittest2,
|
|
||||||
chronos,
|
|
||||||
../../eth/[rlp, keys, p2p],
|
|
||||||
../../eth/p2p/mock_peers, ../../eth/p2p/rlpx_protocols/[whisper_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 the Whisper protocol":
|
|
||||||
const useCompression = defined(useSnappy)
|
|
||||||
let localKeys = KeyPair.random()[]
|
|
||||||
let localAddress = localAddress(30303)
|
|
||||||
var localNode = newEthereumNode(localKeys, localAddress, 1, nil,
|
|
||||||
addAllCapabilities = false,
|
|
||||||
useCompression = useCompression)
|
|
||||||
localNode.addCapability Whisper
|
|
||||||
localNode.startListening()
|
|
||||||
|
|
||||||
var mock1 = newMockPeer do (m: MockConf):
|
|
||||||
m.addHandshake Whisper.status(protocolVersion: whisperVersion, powConverted: 0,
|
|
||||||
bloom: @[], isLightNode: false)
|
|
||||||
m.expect Whisper.messages
|
|
||||||
|
|
||||||
var mock2 = newMockPeer do (m: MockConf):
|
|
||||||
m.addHandshake Whisper.status(protocolVersion: whisperVersion,
|
|
||||||
powConverted: cast[uint](0.1),
|
|
||||||
bloom: @[], isLightNode: false)
|
|
||||||
m.expect Whisper.messages
|
|
||||||
|
|
||||||
var mock1Peer = await localNode.rlpxConnect(mock1)
|
|
||||||
var mock2Peer = await localNode.rlpxConnect(mock2)
|
|
||||||
|
|
||||||
check:
|
|
||||||
mock1Peer.state(Whisper).powRequirement == 0
|
|
||||||
mock2Peer.state(Whisper).powRequirement == 0.1
|
|
|
@ -1,383 +0,0 @@
|
||||||
#
|
|
||||||
# Ethereum P2P
|
|
||||||
# (c) Copyright 2018
|
|
||||||
# Status Research & Development GmbH
|
|
||||||
#
|
|
||||||
# Licensed under either of
|
|
||||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
||||||
# MIT license (LICENSE-MIT)
|
|
||||||
|
|
||||||
{.used.}
|
|
||||||
|
|
||||||
import
|
|
||||||
std/[sequtils, options, tables],
|
|
||||||
unittest2,
|
|
||||||
nimcrypto/hash,
|
|
||||||
../../eth/[keys, rlp],
|
|
||||||
../../eth/p2p/rlpx_protocols/whisper/whisper_types as whisper
|
|
||||||
|
|
||||||
let rng = newRng()
|
|
||||||
|
|
||||||
suite "Whisper payload":
|
|
||||||
test "should roundtrip without keys":
|
|
||||||
let payload = Payload(payload: @[byte 0, 1, 2])
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().src.isNone()
|
|
||||||
decoded.get().padding.get().len == 251 # 256 -1 -1 -3
|
|
||||||
|
|
||||||
test "should roundtrip with symmetric encryption":
|
|
||||||
var symKey: SymKey
|
|
||||||
let payload = Payload(symKey: some(symKey), payload: @[byte 0, 1, 2])
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get(), symKey = some(symKey))
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().src.isNone()
|
|
||||||
decoded.get().padding.get().len == 251 # 256 -1 -1 -3
|
|
||||||
|
|
||||||
test "should roundtrip with signature":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
|
|
||||||
let payload = Payload(src: some(privKey), payload: @[byte 0, 1, 2])
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
privKey.toPublicKey() == decoded.get().src.get()
|
|
||||||
decoded.get().padding.get().len == 186 # 256 -1 -1 -3 -65
|
|
||||||
|
|
||||||
test "should roundtrip with asymmetric encryption":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
|
|
||||||
let payload = Payload(dst: some(privKey.toPublicKey()),
|
|
||||||
payload: @[byte 0, 1, 2])
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get(), dst = some(privKey))
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().src.isNone()
|
|
||||||
decoded.get().padding.get().len == 251 # 256 -1 -1 -3
|
|
||||||
|
|
||||||
test "should return specified bloom":
|
|
||||||
# Geth test: https://github.com/ethersphere/go-ethereum/blob/d3441ebb563439bac0837d70591f92e2c6080303/whisper/whisperv6/whisper_test.go#L834
|
|
||||||
let top0 = [byte 0, 0, 255, 6]
|
|
||||||
var x: Bloom
|
|
||||||
x[0] = byte 1
|
|
||||||
x[32] = byte 1
|
|
||||||
x[^1] = byte 128
|
|
||||||
check @(top0.topicBloom) == @x
|
|
||||||
|
|
||||||
suite "Whisper payload padding":
|
|
||||||
test "should do max padding":
|
|
||||||
let payload = Payload(payload: repeat(byte 1, 254))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().padding.isSome()
|
|
||||||
decoded.get().padding.get().len == 256 # as dataLen == 256
|
|
||||||
|
|
||||||
test "should do max padding with signature":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
|
|
||||||
let payload = Payload(src: some(privKey), payload: repeat(byte 1, 189))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
privKey.toPublicKey() == decoded.get().src.get()
|
|
||||||
decoded.get().padding.isSome()
|
|
||||||
decoded.get().padding.get().len == 256 # as dataLen == 256
|
|
||||||
|
|
||||||
test "should do min padding":
|
|
||||||
let payload = Payload(payload: repeat(byte 1, 253))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().padding.isSome()
|
|
||||||
decoded.get().padding.get().len == 1 # as dataLen == 255
|
|
||||||
|
|
||||||
test "should do min padding with signature":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
|
|
||||||
let payload = Payload(src: some(privKey), payload: repeat(byte 1, 188))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
privKey.toPublicKey() == decoded.get().src.get()
|
|
||||||
decoded.get().padding.isSome()
|
|
||||||
decoded.get().padding.get().len == 1 # as dataLen == 255
|
|
||||||
|
|
||||||
test "should roundtrip custom padding":
|
|
||||||
let payload = Payload(payload: repeat(byte 1, 10),
|
|
||||||
padding: some(repeat(byte 2, 100)))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().padding.isSome()
|
|
||||||
payload.padding.get() == decoded.get().padding.get()
|
|
||||||
|
|
||||||
test "should roundtrip custom 0 padding":
|
|
||||||
let padding: seq[byte] = @[]
|
|
||||||
let payload = Payload(payload: repeat(byte 1, 10),
|
|
||||||
padding: some(padding))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
decoded.get().padding.isNone()
|
|
||||||
|
|
||||||
test "should roundtrip custom padding with signature":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
let payload = Payload(src: some(privKey), payload: repeat(byte 1, 10),
|
|
||||||
padding: some(repeat(byte 2, 100)))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
privKey.toPublicKey() == decoded.get().src.get()
|
|
||||||
decoded.get().padding.isSome()
|
|
||||||
payload.padding.get() == decoded.get().padding.get()
|
|
||||||
|
|
||||||
test "should roundtrip custom 0 padding with signature":
|
|
||||||
let padding: seq[byte] = @[]
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
let payload = Payload(src: some(privKey), payload: repeat(byte 1, 10),
|
|
||||||
padding: some(padding))
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
|
|
||||||
let decoded = whisper.decode(encoded.get())
|
|
||||||
check:
|
|
||||||
decoded.isSome()
|
|
||||||
payload.payload == decoded.get().payload
|
|
||||||
privKey.toPublicKey() == decoded.get().src.get()
|
|
||||||
decoded.get().padding.isNone()
|
|
||||||
|
|
||||||
# example from https://github.com/paritytech/parity-ethereum/blob/93e1040d07e385d1219d00af71c46c720b0a1acf/whisper/src/message.rs#L439
|
|
||||||
let
|
|
||||||
env0 = Envelope(
|
|
||||||
expiry:100000, ttl: 30, topic: [byte 0, 0, 0, 0],
|
|
||||||
data: repeat(byte 9, 256), nonce: 1010101)
|
|
||||||
env1 = Envelope(
|
|
||||||
expiry:100000, ttl: 30, topic: [byte 0, 0, 0, 0],
|
|
||||||
data: repeat(byte 9, 256), nonce: 1010102)
|
|
||||||
env2 = Envelope(
|
|
||||||
expiry:100000, ttl: 30, topic: [byte 0, 0, 0, 0],
|
|
||||||
data: repeat(byte 9, 256), nonce: 1010103)
|
|
||||||
|
|
||||||
suite "Whisper envelope":
|
|
||||||
|
|
||||||
proc hashAndPow(env: Envelope): (string, float64) =
|
|
||||||
# This is the current implementation of go-ethereum
|
|
||||||
let size = env.toShortRlp().len().uint32
|
|
||||||
# This is our current implementation in `whisper_protocol.nim`
|
|
||||||
# let size = env.len().uint32
|
|
||||||
# This is the EIP-627 specification
|
|
||||||
# let size = env.toRlp().len().uint32
|
|
||||||
let hash = env.calcPowHash()
|
|
||||||
($hash, calcPow(size, env.ttl, hash))
|
|
||||||
|
|
||||||
test "PoW calculation leading zeroes tests":
|
|
||||||
# Test values from Parity, in message.rs
|
|
||||||
let testHashes = [
|
|
||||||
# 256 leading zeroes
|
|
||||||
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
# 255 leading zeroes
|
|
||||||
"0x0000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
# no leading zeroes
|
|
||||||
"0xff00000000000000000000000000000000000000000000000000000000000000"
|
|
||||||
]
|
|
||||||
check:
|
|
||||||
calcPow(1, 1, Hash.fromHex(testHashes[0])) ==
|
|
||||||
115792089237316200000000000000000000000000000000000000000000000000000000000000.0
|
|
||||||
calcPow(1, 1, Hash.fromHex(testHashes[1])) ==
|
|
||||||
57896044618658100000000000000000000000000000000000000000000000000000000000000.0
|
|
||||||
calcPow(1, 1, Hash.fromHex(testHashes[2])) == 1.0
|
|
||||||
|
|
||||||
# Test values from go-ethereum whisperv6 in envelope_test
|
|
||||||
var env = Envelope(ttl: 1, data: @[byte 0xde, 0xad, 0xbe, 0xef])
|
|
||||||
# PoW calculation with no leading zeroes
|
|
||||||
env.nonce = 100000
|
|
||||||
check hashAndPow(env) == ("A788E02A95BFC673709E97CA81E39CA903BAD5638D3388964C51EB64952172D6",
|
|
||||||
0.07692307692307693)
|
|
||||||
# PoW calculation with 8 leading zeroes
|
|
||||||
env.nonce = 276
|
|
||||||
check hashAndPow(env) == ("00E2374C6353C243E4073E209A7F2ACB2506522AF318B3B78CF9A88310A2A11C",
|
|
||||||
19.692307692307693)
|
|
||||||
|
|
||||||
suite "Whisper queue":
|
|
||||||
test "should throw out lower proof-of-work item when full":
|
|
||||||
var queue = initQueue(1)
|
|
||||||
|
|
||||||
let msg0 = initMessage(env0)
|
|
||||||
let msg1 = initMessage(env1)
|
|
||||||
|
|
||||||
discard queue.add(msg0)
|
|
||||||
discard queue.add(msg1)
|
|
||||||
|
|
||||||
check:
|
|
||||||
queue.items.len() == 1
|
|
||||||
queue.items[0].env.nonce ==
|
|
||||||
(if msg0.pow > msg1.pow: msg0.env.nonce else: msg1.env.nonce)
|
|
||||||
|
|
||||||
test "should not throw out messages as long as there is capacity":
|
|
||||||
var queue = initQueue(2)
|
|
||||||
|
|
||||||
check:
|
|
||||||
queue.add(initMessage(env0)) == true
|
|
||||||
queue.add(initMessage(env1)) == true
|
|
||||||
|
|
||||||
queue.items.len() == 2
|
|
||||||
|
|
||||||
test "check if order of queue is by decreasing PoW":
|
|
||||||
var queue = initQueue(3)
|
|
||||||
|
|
||||||
let msg0 = initMessage(env0)
|
|
||||||
let msg1 = initMessage(env1)
|
|
||||||
let msg2 = initMessage(env2)
|
|
||||||
|
|
||||||
discard queue.add(msg0)
|
|
||||||
discard queue.add(msg1)
|
|
||||||
discard queue.add(msg2)
|
|
||||||
|
|
||||||
check:
|
|
||||||
queue.items.len() == 3
|
|
||||||
queue.items[0].pow > queue.items[1].pow and
|
|
||||||
queue.items[1].pow > queue.items[2].pow
|
|
||||||
|
|
||||||
test "check field order against expected rlp order":
|
|
||||||
check rlp.encode(env0) ==
|
|
||||||
rlp.encodeList(env0.expiry, env0.ttl, env0.topic, env0.data, env0.nonce)
|
|
||||||
|
|
||||||
# To test filters we do not care if the msg is valid or allowed
|
|
||||||
proc prepFilterTestMsg(pubKey = none[PublicKey](), symKey = none[SymKey](),
|
|
||||||
src = none[PrivateKey](), topic: Topic,
|
|
||||||
padding = none[seq[byte]]()): Message =
|
|
||||||
let payload = Payload(dst: pubKey, symKey: symKey, src: src,
|
|
||||||
payload: @[byte 0, 1, 2], padding: padding)
|
|
||||||
let encoded = whisper.encode(rng[], payload)
|
|
||||||
let env = Envelope(expiry: 1, ttl: 1, topic: topic, data: encoded.get(),
|
|
||||||
nonce: 0)
|
|
||||||
result = initMessage(env)
|
|
||||||
|
|
||||||
suite "Whisper filter":
|
|
||||||
test "should notify filter on message with symmetric encryption":
|
|
||||||
var symKey: SymKey
|
|
||||||
let topic = [byte 0, 0, 0, 0]
|
|
||||||
let msg = prepFilterTestMsg(symKey = some(symKey), topic = topic)
|
|
||||||
|
|
||||||
var filters = initTable[string, Filter]()
|
|
||||||
let filter = initFilter(symKey = some(symKey), topics = @[topic])
|
|
||||||
let filterId = subscribeFilter(rng[], filters, filter)
|
|
||||||
|
|
||||||
notify(filters, msg)
|
|
||||||
|
|
||||||
let messages = filters.getFilterMessages(filterId)
|
|
||||||
check:
|
|
||||||
messages.len == 1
|
|
||||||
messages[0].decoded.src.isNone()
|
|
||||||
messages[0].dst.isNone()
|
|
||||||
|
|
||||||
test "should notify filter on message with asymmetric encryption":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
let topic = [byte 0, 0, 0, 0]
|
|
||||||
let msg = prepFilterTestMsg(pubKey = some(privKey.toPublicKey()),
|
|
||||||
topic = topic)
|
|
||||||
|
|
||||||
var filters = initTable[string, Filter]()
|
|
||||||
let filter = initFilter(privateKey = some(privKey), topics = @[topic])
|
|
||||||
let filterId = subscribeFilter(rng[], filters, filter)
|
|
||||||
|
|
||||||
notify(filters, msg)
|
|
||||||
|
|
||||||
let messages = filters.getFilterMessages(filterId)
|
|
||||||
check:
|
|
||||||
messages.len == 1
|
|
||||||
messages[0].decoded.src.isNone()
|
|
||||||
messages[0].dst.isSome()
|
|
||||||
|
|
||||||
test "should notify filter on message with signature":
|
|
||||||
let privKey = PrivateKey.random(rng[])
|
|
||||||
let topic = [byte 0, 0, 0, 0]
|
|
||||||
let msg = prepFilterTestMsg(src = some(privKey), topic = topic)
|
|
||||||
|
|
||||||
var filters = initTable[string, Filter]()
|
|
||||||
let filter = initFilter(src = some(privKey.toPublicKey()),
|
|
||||||
topics = @[topic])
|
|
||||||
let filterId = subscribeFilter(rng[], filters, filter)
|
|
||||||
|
|
||||||
notify(filters, msg)
|
|
||||||
|
|
||||||
let messages = filters.getFilterMessages(filterId)
|
|
||||||
check:
|
|
||||||
messages.len == 1
|
|
||||||
messages[0].decoded.src.isSome()
|
|
||||||
messages[0].dst.isNone()
|
|
||||||
|
|
||||||
test "test notify of filter against PoW requirement":
|
|
||||||
let topic = [byte 0, 0, 0, 0]
|
|
||||||
let padding = some(repeat(byte 0, 251))
|
|
||||||
# this message has a PoW of 0.02962962962962963, number should be updated
|
|
||||||
# in case PoW algorithm changes or contents of padding, payload, topic, etc.
|
|
||||||
# update: now with NON rlp encoded envelope size the PoW of this message is
|
|
||||||
# 0.014492753623188406
|
|
||||||
let msg = prepFilterTestMsg(topic = topic, padding = padding)
|
|
||||||
|
|
||||||
var filters = initTable[string, Filter]()
|
|
||||||
let
|
|
||||||
filterId1 = subscribeFilter(rng[], filters,
|
|
||||||
initFilter(topics = @[topic], powReq = 0.014492753623188406))
|
|
||||||
filterId2 = subscribeFilter(rng[], filters,
|
|
||||||
initFilter(topics = @[topic], powReq = 0.014492753623188407))
|
|
||||||
|
|
||||||
notify(filters, msg)
|
|
||||||
|
|
||||||
check:
|
|
||||||
filters.getFilterMessages(filterId1).len == 1
|
|
||||||
filters.getFilterMessages(filterId2).len == 0
|
|
||||||
|
|
||||||
test "test notify of filter on message with certain topic":
|
|
||||||
let
|
|
||||||
topic1 = [byte 0xAB, 0x12, 0xCD, 0x34]
|
|
||||||
topic2 = [byte 0, 0, 0, 0]
|
|
||||||
|
|
||||||
let msg = prepFilterTestMsg(topic = topic1)
|
|
||||||
|
|
||||||
var filters = initTable[string, Filter]()
|
|
||||||
let
|
|
||||||
filterId1 = subscribeFilter(rng[], filters, initFilter(topics = @[topic1]))
|
|
||||||
filterId2 = subscribeFilter(rng[], filters, initFilter(topics = @[topic2]))
|
|
||||||
|
|
||||||
notify(filters, msg)
|
|
||||||
|
|
||||||
check:
|
|
||||||
filters.getFilterMessages(filterId1).len == 1
|
|
||||||
filters.getFilterMessages(filterId2).len == 0
|
|
|
@ -1,72 +0,0 @@
|
||||||
#
|
|
||||||
# Ethereum P2P
|
|
||||||
# (c) Copyright 2018
|
|
||||||
# Status Research & Development GmbH
|
|
||||||
#
|
|
||||||
# Licensed under either of
|
|
||||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
||||||
# MIT license (LICENSE-MIT)
|
|
||||||
|
|
||||||
{.used.}
|
|
||||||
|
|
||||||
import
|
|
||||||
std/[sequtils, options, times],
|
|
||||||
unittest2,
|
|
||||||
../../eth/p2p/rlpx_protocols/whisper_protocol as whisper
|
|
||||||
|
|
||||||
suite "Whisper envelope validation":
|
|
||||||
test "should validate and allow envelope according to config":
|
|
||||||
let ttl = 1'u32
|
|
||||||
let topic = [byte 1, 2, 3, 4]
|
|
||||||
let config = WhisperConfig(powRequirement: 0, bloom: topic.topicBloom(),
|
|
||||||
isLightNode: false, maxMsgSize: defaultMaxMsgSize)
|
|
||||||
|
|
||||||
let env = Envelope(expiry:epochTime().uint32 + ttl, ttl: ttl, topic: topic,
|
|
||||||
data: repeat(byte 9, 256), nonce: 0)
|
|
||||||
check env.valid()
|
|
||||||
|
|
||||||
let msg = initMessage(env)
|
|
||||||
check msg.allowed(config)
|
|
||||||
|
|
||||||
test "should invalidate envelope due to ttl 0":
|
|
||||||
let ttl = 0'u32
|
|
||||||
let topic = [byte 1, 2, 3, 4]
|
|
||||||
let config = WhisperConfig(powRequirement: 0, bloom: topic.topicBloom(),
|
|
||||||
isLightNode: false, maxMsgSize: defaultMaxMsgSize)
|
|
||||||
|
|
||||||
let env = Envelope(expiry:epochTime().uint32 + ttl, ttl: ttl, topic: topic,
|
|
||||||
data: repeat(byte 9, 256), nonce: 0)
|
|
||||||
check env.valid() == false
|
|
||||||
|
|
||||||
test "should invalidate envelope due to expired":
|
|
||||||
let ttl = 1'u32
|
|
||||||
let topic = [byte 1, 2, 3, 4]
|
|
||||||
let config = WhisperConfig(powRequirement: 0, bloom: topic.topicBloom(),
|
|
||||||
isLightNode: false, maxMsgSize: defaultMaxMsgSize)
|
|
||||||
|
|
||||||
let env = Envelope(expiry:epochTime().uint32, ttl: ttl, topic: topic,
|
|
||||||
data: repeat(byte 9, 256), nonce: 0)
|
|
||||||
check env.valid() == false
|
|
||||||
|
|
||||||
test "should invalidate envelope due to in the future":
|
|
||||||
let ttl = 1'u32
|
|
||||||
let topic = [byte 1, 2, 3, 4]
|
|
||||||
let config = WhisperConfig(powRequirement: 0, bloom: topic.topicBloom(),
|
|
||||||
isLightNode: false, maxMsgSize: defaultMaxMsgSize)
|
|
||||||
|
|
||||||
# there is currently a 2 second tolerance, hence the + 3
|
|
||||||
let env = Envelope(expiry:epochTime().uint32 + ttl + 3, ttl: ttl, topic: topic,
|
|
||||||
data: repeat(byte 9, 256), nonce: 0)
|
|
||||||
check env.valid() == false
|
|
||||||
|
|
||||||
test "should not allow envelope due to bloom filter":
|
|
||||||
let topic = [byte 1, 2, 3, 4]
|
|
||||||
let wrongTopic = [byte 9, 8, 7, 6]
|
|
||||||
let config = WhisperConfig(powRequirement: 0, bloom: wrongTopic.topicBloom(),
|
|
||||||
isLightNode: false, maxMsgSize: defaultMaxMsgSize)
|
|
||||||
|
|
||||||
let env = Envelope(expiry:100000 , ttl: 30, topic: topic,
|
|
||||||
data: repeat(byte 9, 256), nonce: 0)
|
|
||||||
|
|
||||||
let msg = initMessage(env)
|
|
||||||
check msg.allowed(config) == false
|
|
Loading…
Reference in New Issue