diff --git a/Makefile b/Makefile index d9d838a9d..15ab6ddc1 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,8 @@ wrappers: | build deps libnimbus.so go-checks $(CC) wrappers/wrapper_example.c -Wl,-rpath,'$$ORIGIN' -Lbuild -lnimbus -lm -g -o build/C_wrapper_example echo -e $(BUILD_MSG) "build/go_wrapper_example" && \ go build -linkshared -o build/go_wrapper_example wrappers/wrapper_example.go wrappers/cfuncs.go + echo -e $(BUILD_MSG) "build/go_wrapper_whisper_example" && \ + go build -linkshared -o build/go_wrapper_whisper_example wrappers/wrapper_whisper_example.go wrappers/cfuncs.go libnimbus.a: | build deps echo -e $(BUILD_MSG) "build/$@" && \ diff --git a/wrappers/libnimbus.h b/wrappers/libnimbus.h index 94840f6f7..d175e67a4 100644 --- a/wrappers/libnimbus.h +++ b/wrappers/libnimbus.h @@ -9,8 +9,9 @@ extern "C" { #endif typedef struct { - uint8_t* decoded; + int8_t* decoded; size_t decodedLen; + uint8_t source[64]; uint32_t timestamp; uint32_t ttl; uint8_t topic[4]; @@ -18,6 +19,30 @@ typedef struct { uint8_t hash[32]; } received_message; +typedef struct { + const char* symKeyID; + const char* privateKeyID; + uint8_t sig[64]; + double minPow; + uint8_t topic[4]; +} filter_options; + +typedef struct { + const char* symKeyID; + uint8_t pubKey[64]; + const char* sig; + uint32_t ttl; + uint8_t topic[4]; + char* payload; + char* padding; + double powTime; + double powTarget; +} post_message; + +typedef struct { + uint8_t topic[4]; +} topic; + typedef void (*received_msg_handler)(received_message* msg); /** Initialize Nim and the status library */ @@ -38,6 +63,19 @@ void nimbus_poll(); void nimbus_post(const char* channel, const char* payload); void nimbus_subscribe(const char* channel, received_msg_handler msg); +/* Whisper API */ + +topic nimbus_string_to_topic(const char* s); +/* Generate asymmetric keypair */ +const char* nimbus_new_keypair(); +/* Generate symmetric key from password */ +const char* nimbus_add_symkey_from_password(const char* password); +/* Subscribe to given filter */ +void nimbus_whisper_subscribe(filter_options* filter_options, + received_msg_handler msg); +/* Post Whisper message */ +void nimbus_whisper_post(post_message* msg); + #ifdef __cplusplus } #endif diff --git a/wrappers/libnimbus.nim b/wrappers/libnimbus.nim index e5c2279f2..4947d6653 100644 --- a/wrappers/libnimbus.nim +++ b/wrappers/libnimbus.nim @@ -1,5 +1,5 @@ # -# Stratus +# Nimbus # (c) Copyright 2018 # Status Research & Development GmbH # @@ -8,26 +8,68 @@ # MIT license (LICENSE-MIT) import - chronos, chronicles, nimcrypto/[utils, hmac, pbkdf2, hash], + chronos, chronicles, nimcrypto/[utils, hmac, pbkdf2, hash], tables, eth/[keys, rlp, p2p], eth/p2p/rlpx_protocols/whisper_protocol, eth/p2p/[discovery, enode, peer_pool, bootnodes, whispernodes] +from stew/byteutils import hexToSeqByte, hexToByteArray + +# TODO: If we really want/need this type of API for the keys, put it somewhere +# seperate as it is the same code for Whisper RPC +type + WhisperKeys* = ref object + asymKeys*: Table[string, KeyPair] + symKeys*: Table[string, SymKey] + + KeyGenerationError = object of CatchableError + +proc newWhisperKeys*(): WhisperKeys = + new(result) + result.asymKeys = initTable[string, KeyPair]() + result.symKeys = initTable[string, SymKey]() + +# TODO: again, lots of overlap with Nimbus whisper RPC here, however not all +# the same due to type conversion (no use of Option and such) type CReceivedMessage* = object decoded*: ptr byte decodedLen*: csize + source*: PublicKey timestamp*: uint32 ttl*: uint32 topic*: Topic pow*: float64 hash*: Hash + CFilterOptions* = object + symKeyID*: cstring + privateKeyID*: cstring + sig*: PublicKey + minPow*: float64 + topic*: Topic # lets go with one topic for now + + CPostMessage* = object + symKeyID*: cstring + pubKey*: PublicKey + sig*: cstring + ttl*: uint32 + topic*: Topic + payload*: cstring + padding*: cstring + powTime*: float64 + powTarget*: float64 + + CTopic* = object + topic*: Topic + proc `$`*(digest: SymKey): string = for c in digest: result &= hexChar(c.byte) # Don't do this at home, you'll never get rid of ugly globals like this! var node: EthereumNode +# You will only add more instead! +let whisperKeys = newWhisperKeys() # TODO: Return filter ID if we ever want to unsubscribe proc subscribeChannel( @@ -150,3 +192,125 @@ proc nimbus_add_peer(nodeId: cstring) {.exportc.} = var whisperNode = newNode(whisperENode) asyncCheck node.peerPool.connectToNode(whisperNode) + +# Whisper API (Similar to Whisper RPC API) +# Mostly an example for now, lots of things to fix if continued like this. + +proc nimbus_string_to_topic(s: cstring): CTopic {.exportc.} = + let hash = digest(keccak256, $s) + for i in 0..<4: + result.topic[i] = hash.data[i] + +proc nimbus_new_keypair(): cstring {.exportc.} = + result = generateRandomID() + whisperKeys.asymKeys.add($result, newKeyPair()) + +proc nimbus_add_keypair(key: PrivateKey): cstring = discard +proc nimbus_delete_keypair(id: cstring) = discard +proc nimbus_add_symkey(key: SymKey): cstring = discard + +proc nimbus_add_symkey_from_password(password: cstring): cstring {.exportc.} = + setupForeignThreadGc() + + var ctx: HMAC[sha256] + var symKey: SymKey + if pbkdf2(ctx, $password, "", 65356, symKey) != sizeof(SymKey): + raise newException(KeyGenerationError, "Failed generating key") + + result = generateRandomID() + + whisperKeys.symKeys.add($result, symKey) + + tearDownForeignThreadGc() + +proc nimbus_delete_symkey(id: cstring) = discard + +proc nimbus_whisper_post(message: ptr CPostMessage) {.exportc.} = + setupForeignThreadGc() + + var + sigPrivKey: Option[PrivateKey] + asymKey: Option[PublicKey] + symKey: Option[SymKey] + padding: Option[Bytes] + payload: Bytes + + # TODO: + # - check if there is a asymKey and/or pubKey or do we not care? + # - fail if payload is nil? + # - error handling on key access + + # TODO: How to arrange optional pubkey? + # - Ptr with check on Nil? (or just cstring?) + # - Convert also Options? + # - Or just add different API calls? + # asymKey = some(message.pubKey) + asymKey = none(PublicKey) + + if not message.symKeyID.isNil(): + symKey = some(whisperKeys.symKeys[$message.symKeyID]) + if not message.sig.isNil(): + sigPrivKey = some(whisperKeys.asymKeys[$message.sig].seckey) + if not message.payload.isNil(): + # TODO: Is this cast OK? + payload = cast[Bytes]($message.payload) + # payload = cast[Bytes](@($message.payload)) + if not message.padding.isNil(): + padding = some(cast[Bytes]($message.padding)) + + # TODO: Handle error case + discard node.postMessage(asymKey, + symKey, + sigPrivKey, + ttl = message.ttl, + topic = message.topic, + payload = payload, + padding = padding, + powTime = message.powTime, + powTarget = message.powTarget) + + tearDownForeignThreadGc() + +proc nimbus_whisper_subscribe(options: ptr CFilterOptions, + handler: proc (msg: ptr CReceivedMessage) + {.gcsafe, cdecl.}) {.exportc.} = + setupForeignThreadGc() + + # TODO: same remarks as in nimbus_whisper_post() + + var filter: Filter + filter.src = none(PublicKey) + if not options.symKeyID.isNil(): + # if options.symKeyID.len() > 0: + filter.symKey= some(whisperKeys.symKeys[$options.symKeyID]) + if not options.privateKeyID.isNil(): + filter.privateKey= some(whisperKeys.asymKeys[$options.privateKeyID].seckey) + filter.powReq = options.minPow + filter.topics = @[options.topic] + filter.allowP2P = false + + if handler.isNil: + discard node.subscribeFilter(filter, nil) + return + + proc c_handler(msg: ReceivedMessage) {.gcsafe.} = + var cmsg = CReceivedMessage( + decoded: unsafeAddr msg.decoded.payload[0], + decodedLen: csize msg.decoded.payload.len(), + timestamp: msg.timestamp, + ttl: msg.ttl, + topic: msg.topic, + pow: msg.pow, + hash: msg.hash + ) + + if msg.decoded.src.isSome(): + cmsg.source = msg.decoded.src.get() + + handler(addr cmsg) + + discard node.subscribeFilter(filter, c_handler) + + tearDownForeignThreadGc() + +proc nimbus_whisper_unsubscribe(id: cstring) = discard diff --git a/wrappers/wrapper_whisper_example.go b/wrappers/wrapper_whisper_example.go new file mode 100644 index 000000000..2d0c6bfa7 --- /dev/null +++ b/wrappers/wrapper_whisper_example.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "runtime" + "time" + "unsafe" +) + +/* +#cgo LDFLAGS: -Wl,-rpath,'$ORIGIN' -L${SRCDIR}/../build -lnimbus -lm +#include "libnimbus.h" + +void receiveHandler_cgo(received_message * msg); // Forward declaration. +*/ +import "C" + +// Arrange that main.main runs on main thread. +func init() { + runtime.LockOSThread() +} + +func poll() { + + for { + fmt.Println("POLLING") + time.Sleep(1 * time.Microsecond) + C.nimbus_poll() + } +} + +//export receiveHandler +func receiveHandler(msg *C.received_message) { + fmt.Printf("[nim-status] received message %s\n", + C.GoStringN((*C.char)(msg.decoded), (C.int)(msg.decodedLen)) ) + fmt.Printf("[nim-status] source public key %x\n", msg.source) +} + +func Start() { + C.NimMain() + fmt.Println("[nim-status] Start Nimbus") + C.nimbus_start(30306) +} + +func StatusListenAndPost(channel string) { + fmt.Println("[nim-status] Status Public ListenAndPost") + + // TODO: free the CStrings? + // TODO: Is this doing a copy or not? If not, shouldn't we see issues when the + // nim GC kicks in? + symKeyId := C.GoString(C.nimbus_add_symkey_from_password(C.CString(channel))) + asymKeyId := C.GoString(C.nimbus_new_keypair()) + + options := C.filter_options{symKeyID: C.CString(symKeyId), + minPow: 0.002, + topic: C.nimbus_string_to_topic(C.CString(channel)).topic} + C.nimbus_whisper_subscribe(&options, + (C.received_msg_handler)(unsafe.Pointer(C.receiveHandler_cgo))) + + postMessage := C.post_message{symKeyID: C.CString(symKeyId), + sig: C.CString(asymKeyId), + ttl: 20, + topic: C.nimbus_string_to_topic(C.CString(channel)).topic, + powTarget: 0.002, + powTime: 1.0} + + i := 0 + for { + C.nimbus_poll() + t := time.Now().UnixNano() / int64(time.Millisecond) + i = i + 1 + time.Sleep(1 * time.Microsecond) + message := fmt.Sprintf("[\"~#c4\",[\"Message:%d\",\"text/plain\",\"~:public-group-user-message\",%d,%d,[\"^ \",\"~:chat-id\",\"%s\",\"~:text\",\"Message:%d\"]]]", i, t*100, t, channel, i) + if i%1000 == 0 { + fmt.Println("[nim-status] posting", message) + postMessage.payload = (C.CString(message)) + C.nimbus_whisper_post(&postMessage) + } + } +} + +func main() { + nprocs := runtime.GOMAXPROCS(0) + fmt.Println("GOMAXPROCS ", nprocs) + + Start() + StatusListenAndPost("status-test-go") +}