feat(waku-stealth-commitments): waku stealth commitment protocol (#2490)

* feat(waku-stealth-commitments): initialize app

* feat: works!

* fix: readme

* feat: send and receive, handle received stealth commitment

* fix: remove empty lines

* chore: move to examples
This commit is contained in:
Aaryamann Challani 2024-03-06 18:44:33 +05:30 committed by GitHub
parent e692edf6c1
commit 0def4904f2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 601 additions and 0 deletions

View File

@ -0,0 +1,38 @@
# wakustealthcommitments
This application/tool/protocol is used to securely communicate requests and responses for the [Stealth Address Scheme](https://eips.ethereum.org/EIPS/eip-5564)
Uses TWN config as default, and content topic: `/wakustealthcommitments/1/app/proto`
## Usage
1. Clone the erc-5564-bn254 repo and build the static lib
```sh
gh repo clone rymnc/erc-5564-bn254
cd erc-5564-bn254
cargo build --release --all-features
cp ./target/release/liberc_5564_bn254.a <path-to-nwaku>
```
> ![NOTE]
> This static library also includes the rln ffi library, so you don't need to build it separately.
> This is because using both of them separately brings in a lot of duplicate symbols.
2. Build the wakustealthcommitments app
```sh
cd <path-to-nwaku>
source env.sh
nim c --out:build/wakustealthcommitments --verbosity:0 --hints:off -d:chronicles_log_level=INFO -d:git_version="v0.24.0-rc.0-62-g7da25c" -d:release --passL:-lm --passL:liberc_5564_bn254.a --debugger:native examples/wakustealthcommitments/wakustealthcommitments.nim
```
3.
```sh
./build/wakustealthcommitments \
--rln-relay-eth-client-address:<insert http rpc url> \
--rln-relay-cred-path:<path-to-credentials-file> \
--rln-relay-cred-password:<password-of-credentials-file>
```
This service listens for requests for stealth commitment/address generation,
partakes in the generation of said stealth commitment and then distributes the response to the mesh.

View File

@ -0,0 +1,135 @@
## Nim wrappers for the functions defined in librln
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import stew/results
######################################################################
## ERC-5564-BN254 module APIs
######################################################################
type CErrorCode* = uint8
type CG1Projective* = object
x0: array[32, uint8]
type CReturn*[T] = object
value: T
err_code: CErrorCode
type CFr* = object
x0: array[32, uint8]
type CStealthCommitment* = object
stealth_commitment: CG1Projective
view_tag: uint64
type CKeyPair* = object
private_key: CFr
public_key: CG1Projective
proc drop_ffi_derive_public_key*(ptrx: ptr CReturn[CG1Projective]) {.importc: "drop_ffi_derive_public_key".}
proc drop_ffi_generate_random_fr*(ptrx: ptr CReturn[CFr]) {.importc: "drop_ffi_generate_random_fr".}
proc drop_ffi_generate_stealth_commitment*(ptrx: ptr CReturn[CStealthCommitment]) {.importc: "drop_ffi_generate_stealth_commitment".}
proc drop_ffi_generate_stealth_private_key*(ptrx: ptr CReturn[CFr]) {.importc: "drop_ffi_generate_stealth_private_key".}
proc drop_ffi_random_keypair*(ptrx: ptr CReturn[CKeyPair]) {.importc: "drop_ffi_random_keypair".}
proc ffi_derive_public_key*(private_key: ptr CFr): (ptr CReturn[CG1Projective]) {.importc: "ffi_derive_public_key".}
proc ffi_generate_random_fr*(): (ptr CReturn[CFr]) {.importc: "ffi_generate_random_fr".}
proc ffi_generate_stealth_commitment*(viewing_public_key: ptr CG1Projective,
spending_public_key: ptr CG1Projective,
ephemeral_private_key: ptr CFr): (ptr CReturn[CStealthCommitment]) {.importc: "ffi_generate_stealth_commitment".}
proc ffi_generate_stealth_private_key*(ephemeral_public_key: ptr CG1Projective,
spending_key: ptr CFr,
viewing_key: ptr CFr,
view_tag: ptr uint64): (ptr CReturn[CFr]) {.importc: "ffi_generate_stealth_private_key".}
proc ffi_random_keypair*(): (ptr CReturn[CKeyPair]) {.importc: "ffi_random_keypair".}
## Nim wrappers and types for the ERC-5564-BN254 module
type FFIResult[T] = Result[T, string]
type Fr = array[32, uint8]
type G1Projective = array[32, uint8]
type KeyPair* = object
private_key*: Fr
public_key*: G1Projective
type StealthCommitment* = object
stealth_commitment*: G1Projective
view_tag*: uint64
type PrivateKey* = Fr
type PublicKey* = G1Projective
proc generateRandomFr*(): FFIResult[Fr] =
let res_ptr = (ffi_generate_random_fr())
let res_value = res_ptr[]
if res_value.err_code != 0:
drop_ffi_generate_random_fr(res_ptr)
return err("Error generating random field element: " & $res_value.err_code)
let ret = res_value.value.x0
drop_ffi_generate_random_fr(res_ptr)
return ok(ret)
proc generateKeypair*(): FFIResult[KeyPair] =
let res_ptr = (ffi_random_keypair())
let res_value = res_ptr[]
if res_value.err_code != 0:
drop_ffi_random_keypair(res_ptr)
return err("Error generating random keypair: " & $res_value.err_code)
let ret = KeyPair(private_key: res_value.value.private_key.x0, public_key: res_value.value.public_key.x0)
drop_ffi_random_keypair(res_ptr)
return ok(ret)
proc generateStealthCommitment*(viewing_public_key: G1Projective,
spending_public_key: G1Projective,
ephemeral_private_key: Fr): FFIResult[StealthCommitment] =
let viewing_public_key = CG1Projective(x0: viewing_public_key)
let viewing_public_key_ptr = unsafeAddr(viewing_public_key)
let spending_public_key = CG1Projective(x0: spending_public_key)
let spending_public_key_ptr = unsafeAddr(spending_public_key)
let ephemeral_private_key = CFr(x0: ephemeral_private_key)
let ephemeral_private_key_ptr = unsafeAddr(ephemeral_private_key)
let res_ptr = (ffi_generate_stealth_commitment(viewing_public_key_ptr, spending_public_key_ptr, ephemeral_private_key_ptr))
let res_value = res_ptr[]
if res_value.err_code != 0:
drop_ffi_generate_stealth_commitment(res_ptr)
return err("Error generating stealth commitment: " & $res_value.err_code)
let ret = StealthCommitment(stealth_commitment: res_value.value.stealth_commitment.x0, view_tag: res_value.value.view_tag)
drop_ffi_generate_stealth_commitment(res_ptr)
return ok(ret)
proc generateStealthPrivateKey*(ephemeral_public_key: G1Projective,
spending_key: Fr,
viewing_key: Fr,
view_tag: uint64): FFIResult[Fr] =
let ephemeral_public_key = CG1Projective(x0: ephemeral_public_key)
let ephemeral_public_key_ptr = unsafeAddr(ephemeral_public_key)
let spending_key = CFr(x0: spending_key)
let spending_key_ptr = unsafeAddr(spending_key)
let viewing_key = CFr(x0: viewing_key)
let viewing_key_ptr = unsafeAddr(viewing_key)
let view_tag_ptr = unsafeAddr(view_tag)
let res_ptr = (ffi_generate_stealth_private_key(ephemeral_public_key_ptr, spending_key_ptr, viewing_key_ptr, view_tag_ptr))
let res_value = res_ptr[]
if res_value.err_code != 0:
drop_ffi_generate_stealth_private_key(res_ptr)
return err("Error generating stealth private key: " & $res_value.err_code)
let ret = res_value.value.x0
drop_ffi_generate_stealth_private_key(res_ptr)
return ok(ret)

View File

@ -0,0 +1,10 @@
-d:chronicles_line_numbers
-d:discv5_protocol_id="d5waku"
-d:chronicles_runtime_filtering=on
-d:chronicles_sinks="textlines,json"
-d:chronicles_default_output_device=dynamic
# Disabling the following topics from nim-eth and nim-dnsdisc since some types cannot be serialized
-d:chronicles_disabled_topics="eth,dnsdisc.client"
-d:chronicles_log_level=INFO
# Results in empty output for some reason
#-d:"chronicles_enabled_topics=GossipSub:TRACE,WakuRelay:TRACE"

View File

@ -0,0 +1,106 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import ../../apps/wakunode2/[networks_config, app, external_config]
import ../../waku/common/logging
import
std/[options, strutils, os, sequtils],
stew/shims/net as stewNet,
chronicles,
chronos,
metrics,
libbacktrace,
libp2p/crypto/crypto
export
networks_config,
app,
logging,
options,
strutils,
os,
sequtils,
stewNet,
chronicles,
chronos,
metrics,
libbacktrace,
crypto
proc setup*(): App =
const versionString = "version / git commit hash: " & app.git_version
let rng = crypto.newRng()
let confRes = WakuNodeConf.load(version = versionString)
if confRes.isErr():
error "failure while loading the configuration", error = $confRes.error
quit(QuitFailure)
var conf = confRes.get()
let twnClusterConf = ClusterConf.TheWakuNetworkConf()
if len(conf.shards) != 0:
conf.pubsubTopics = conf.shards.mapIt(twnClusterConf.pubsubTopics[it.uint16])
else:
conf.pubsubTopics = twnClusterConf.pubsubTopics
# Override configuration
conf.maxMessageSize = twnClusterConf.maxMessageSize
conf.clusterId = twnClusterConf.clusterId
conf.rlnRelay = twnClusterConf.rlnRelay
conf.rlnRelayEthContractAddress = twnClusterConf.rlnRelayEthContractAddress
conf.rlnRelayDynamic = twnClusterConf.rlnRelayDynamic
conf.rlnRelayBandwidthThreshold = twnClusterConf.rlnRelayBandwidthThreshold
conf.discv5Discovery = twnClusterConf.discv5Discovery
conf.discv5BootstrapNodes =
conf.discv5BootstrapNodes & twnClusterConf.discv5BootstrapNodes
conf.rlnEpochSizeSec = twnClusterConf.rlnEpochSizeSec
conf.rlnRelayUserMessageLimit = twnClusterConf.rlnRelayUserMessageLimit
var wakunode2 = App.init(rng, conf)
## Peer persistence
let res1 = wakunode2.setupPeerPersistence()
if res1.isErr():
error "1/5 Setting up storage failed", error = $res1.error
quit(QuitFailure)
debug "2/5 Retrieve dynamic bootstrap nodes"
let res3 = wakunode2.setupDyamicBootstrapNodes()
if res3.isErr():
error "2/5 Retrieving dynamic bootstrap nodes failed", error = $res3.error
quit(QuitFailure)
debug "3/5 Initializing node"
let res4 = wakunode2.setupWakuApp()
if res4.isErr():
error "3/5 Initializing node failed", error = $res4.error
quit(QuitFailure)
debug "4/5 Mounting protocols"
var res5: Result[void, string]
try:
res5 = waitFor wakunode2.setupAndMountProtocols()
if res5.isErr():
error "4/5 Mounting protocols failed", error = $res5.error
quit(QuitFailure)
except Exception:
error "4/5 Mounting protocols failed", error = getCurrentExceptionMsg()
quit(QuitFailure)
debug "5/5 Starting node and mounted protocols"
# set triggerSelf to false, we don't want to process our own stealthCommitments
wakunode2.node.wakuRelay.triggerSelf = false
let res6 = wakunode2.startApp()
if res6.isErr():
error "5/5 Starting node and protocols failed", error = $res6.error
quit(QuitFailure)
info "Node setup complete"
return wakunode2

View File

@ -0,0 +1,154 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
stew/results,
../../waku/common/logging,
../../waku/waku_node,
../../waku/waku_rln_relay,
./erc_5564_interface as StealthCommitmentFFI,
./node_spec,
./wire_spec
export
wire_spec,
logging
type StealthCommitmentProtocol* = object
wakuApp: App
contentTopic: string
spendingKeyPair: StealthCommitmentFFI.KeyPair
viewingKeyPair: StealthCommitmentFFI.KeyPair
proc deserialize(T: type StealthCommitmentFFI.PublicKey, v: SerializedKey): Result[T, string] =
# deserialize seq[byte] into array[32, uint8]
if v.len != 32:
return err("invalid key length")
var buf: array[32, uint8]
for i in 0..<v.len:
buf[i] = v[i]
return ok(buf)
proc serialize(v: StealthCommitmentFFI.PublicKey | StealthCommitmentFFI.PrivateKey): SerializedKey =
# serialize array[32, uint8] into seq[byte]
var buf = newSeq[byte](v.len)
for i in 0..<v.len:
buf[i] = v[i]
return buf
proc sendThruWaku*(self: StealthCommitmentProtocol, msg: seq[byte]): Future[Result[void, string]] {.async.} =
let time = getTime().toUnix()
var message = WakuMessage(payload: msg,
contentTopic: self.contentTopic,
version: 0,
timestamp: getNanosecondTime(time))
(self.wakuApp.node.wakuRlnRelay.appendRLNProof(message, float64(time))).isOkOr:
return err("could not append rate limit proof to the message: " & $error)
(await self.wakuApp.node.publish(some(DefaultPubsubTopic), message)).isOkOr:
return err("failed to publish message: " & $error)
debug "rate limit proof is appended to the message"
return ok()
proc sendRequest*(self: StealthCommitmentProtocol): Future[Result[void, string]] {.async.} =
let request = constructRequest(serialize(self.spendingKeyPair.publicKey), serialize(self.viewingKeyPair.publicKey)).encode()
try:
(await self.sendThruWaku(request.buffer)).isOkOr:
return err("Could not send stealth commitment payload thru waku: " & $error)
except CatchableError:
return err("Could not send stealth commitment payload thru waku: " & getCurrentExceptionMsg())
return ok()
proc sendResponse*(self: StealthCommitmentProtocol, stealthCommitment: StealthCommitmentFFI.PublicKey, ephemeralPubKey: StealthCommitmentFFI.PublicKey, viewTag: uint64): Future[Result[void, string]] {.async.} =
let response = constructResponse(serialize(stealthCommitment), serialize(ephemeralPubKey), viewTag).encode()
try:
(await self.sendThruWaku(response.buffer)).isOkOr:
return err("Could not send stealth commitment payload thru waku: " & $error)
except CatchableError:
return err("Could not send stealth commitment payload thru waku: " & getCurrentExceptionMsg())
return ok()
type SCPHandler* = proc (msg: WakuMessage): Future[void] {.async.}
proc getSCPHandler(self: StealthCommitmentProtocol): SCPHandler =
let handler = proc(msg: WakuMessage): Future[void] {.async.} =
let decodedRes = WakuStealthCommitmentMsg.decode(msg.payload)
if decodedRes.isErr():
error "could not decode scp message"
let decoded = decodedRes.get()
if decoded.request == false:
# check if the generated stealth commitment belongs to the receiver
# if not, continue
let ephemeralPubKeyRes = deserialize(StealthCommitmentFFI.PublicKey, decoded.ephemeralPubKey.get())
if ephemeralPubKeyRes.isErr():
error "could not deserialize ephemeral public key: ", err = ephemeralPubKeyRes.error()
let ephemeralPubKey = ephemeralPubKeyRes.get()
let stealthCommitmentPrivateKeyRes = StealthCommitmentFFI.generateStealthPrivateKey(ephemeralPubKey,
self.spendingKeyPair.privateKey,
self.viewingKeyPair.privateKey,
decoded.viewTag.get())
if stealthCommitmentPrivateKeyRes.isErr():
info "received stealth commitment does not belong to the receiver: ", err = stealthCommitmentPrivateKeyRes.error()
let stealthCommitmentPrivateKey = stealthCommitmentPrivateKeyRes.get()
info "received stealth commitment belongs to the receiver: ", stealthCommitmentPrivateKey, stealthCommitmentPubKey = decoded.stealthCommitment.get()
return
# send response
# deseralize the keys
let spendingKeyRes = deserialize(StealthCommitmentFFI.PublicKey, decoded.spendingPubKey.get())
if spendingKeyRes.isErr():
error "could not deserialize spending key: ", err = spendingKeyRes.error()
let spendingKey = spendingKeyRes.get()
let viewingKeyRes = (deserialize(StealthCommitmentFFI.PublicKey, decoded.viewingPubKey.get()))
if viewingKeyRes.isErr():
error "could not deserialize viewing key: ", err = viewingKeyRes.error()
let viewingKey = viewingKeyRes.get()
info "received spending key", spendingKey
info "received viewing key", viewingKey
let ephemeralKeyPairRes = StealthCommitmentFFI.generateKeyPair()
if ephemeralKeyPairRes.isErr():
error "could not generate ephemeral key pair: ", err = ephemeralKeyPairRes.error()
let ephemeralKeyPair = ephemeralKeyPairRes.get()
let stealthCommitmentRes = StealthCommitmentFFI.generateStealthCommitment(spendingKey, viewingKey, ephemeralKeyPair.privateKey)
if stealthCommitmentRes.isErr():
error "could not generate stealth commitment: ", err = stealthCommitmentRes.error()
let stealthCommitment = stealthCommitmentRes.get()
(await self.sendResponse(stealthCommitment.stealthCommitment, ephemeralKeyPair.publicKey, stealthCommitment.viewTag)).isOkOr:
error "could not send response: ", err = $error
return handler
proc new*(wakuApp: App, contentTopic = ContentTopic("/wakustealthcommitments/1/app/proto")): Result[StealthCommitmentProtocol, string] =
let spendingKeyPair = StealthCommitmentFFI.generateKeyPair().valueOr:
return err("could not generate spending key pair: " & $error)
let viewingKeyPair = StealthCommitmentFFI.generateKeyPair().valueOr:
return err("could not generate viewing key pair: " & $error)
info "spending public key", publicKey = spendingKeyPair.publicKey
info "viewing public key", publicKey = viewingKeyPair.publicKey
let SCP = StealthCommitmentProtocol(wakuApp: wakuApp,
contentTopic: contentTopic,
spendingKeyPair: spendingKeyPair,
viewingKeyPair: viewingKeyPair)
proc handler(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async, gcsafe.} =
let scpHandler = getSCPHandler(SCP)
if msg.contentTopic == contentTopic:
try:
await scpHandler(msg)
except CatchableError:
error "could not handle SCP message: ", err = getCurrentExceptionMsg()
wakuApp.node.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), some(handler))
return ok(SCP)

View File

@ -0,0 +1,43 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
else:
{.push raises: [].}
import
stew/results,
chronicles,
./node_spec as Waku,
./stealth_commitment_protocol as SCP
logScope:
topics = "waku stealthcommitments"
when isMainModule:
## Logging setup
# Adhere to NO_COLOR initiative: https://no-color.org/
let color =
try:
not parseBool(os.getEnv("NO_COLOR", "false"))
except CatchableError:
true
logging.setupLogLevel(logging.LogLevel.INFO)
logging.setupLogFormat(logging.LogFormat.TEXT, color)
info "Starting Waku Stealth Commitment Protocol"
info "Starting Waku Node"
let node = Waku.setup()
info "Waku Node started, listening for StealthCommitmentMessages"
let scp = SCP.new(node).valueOr:
error "Could not start Stealth Commitment Protocol", error = $error
quit(1)
try:
info "Sending stealth commitment request"
(waitFor scp.sendRequest()).isOkOr:
error "Could not send stealth commitment request", error = $error
except:
error "Could not send stealth commitment request", error = getCurrentExceptionMsg()
runForever()

View File

@ -0,0 +1,115 @@
import std/[times, options]
import
confutils,
chronicles,
chronos,
stew/results
import
../../waku/waku_core,
../../waku/common/protobuf
import libp2p/protobuf/minprotobuf
export
times,
options,
confutils,
chronicles,
chronos,
results,
waku_core,
protobuf,
minprotobuf
type SerializedKey* = seq[byte]
type
WakuStealthCommitmentMsg* = object
request*: bool
spendingPubKey*: Option[SerializedKey]
viewingPubKey*: Option[SerializedKey]
ephemeralPubKey*: Option[SerializedKey]
stealthCommitment*: Option[SerializedKey]
viewTag*: Option[uint64]
proc decode*(T: type WakuStealthCommitmentMsg, buffer: seq[byte]): ProtoResult[T] =
var msg = WakuStealthCommitmentMsg()
let pb = initProtoBuffer(buffer)
var request: uint64
discard ? pb.getField(1, request)
msg.request = request == 1
var spendingPubKey = newSeq[byte]()
discard ? pb.getField(2, spendingPubKey)
msg.spendingPubKey = if spendingPubKey.len > 0: some(spendingPubKey) else: none(SerializedKey)
var viewingPubKey = newSeq[byte]()
discard ? pb.getField(3, viewingPubKey)
msg.viewingPubKey = if viewingPubKey.len > 0: some(viewingPubKey) else: none(SerializedKey)
if msg.spendingPubKey.isSome() and msg.viewingPubKey.isSome():
msg.stealthCommitment = none(SerializedKey)
msg.viewTag = none(uint64)
return ok(msg)
if msg.spendingPubKey.isSome() and msg.viewingPubKey.isNone():
return err(ProtoError.RequiredFieldMissing)
if msg.spendingPubKey.isNone() and msg.viewingPubKey.isSome():
return err(ProtoError.RequiredFieldMissing)
if msg.request == true and msg.spendingPubKey.isNone() and msg.viewingPubKey.isNone():
return err(ProtoError.RequiredFieldMissing)
var stealthCommitment = newSeq[byte]()
discard ? pb.getField(4, stealthCommitment)
msg.stealthCommitment = if stealthCommitment.len > 0: some(stealthCommitment) else: none(SerializedKey)
var ephemeralPubKey = newSeq[byte]()
discard ? pb.getField(5, ephemeralPubKey)
msg.ephemeralPubKey = if ephemeralPubKey.len > 0: some(ephemeralPubKey) else: none(SerializedKey)
var viewTag: uint64
discard ? pb.getField(6, viewTag)
msg.viewTag = if viewTag != 0: some(viewTag) else: none(uint64)
if msg.stealthCommitment.isNone() and msg.viewTag.isNone() and msg.ephemeralPubKey.isNone():
return err(ProtoError.RequiredFieldMissing)
if msg.stealthCommitment.isSome() and msg.viewTag.isNone():
return err(ProtoError.RequiredFieldMissing)
if msg.stealthCommitment.isNone() and msg.viewTag.isSome():
return err(ProtoError.RequiredFieldMissing)
if msg.stealthCommitment.isSome() and msg.viewTag.isSome():
msg.spendingPubKey = none(SerializedKey)
msg.viewingPubKey = none(SerializedKey)
ok(msg)
proc encode*(msg: WakuStealthCommitmentMsg): ProtoBuffer =
var serialised = initProtoBuffer()
serialised.write(1, uint64(msg.request))
if msg.spendingPubKey.isSome():
serialised.write(2, msg.spendingPubKey.get())
if msg.viewingPubKey.isSome():
serialised.write(3, msg.viewingPubKey.get())
if msg.stealthCommitment.isSome():
serialised.write(4, msg.stealthCommitment.get())
if msg.ephemeralPubKey.isSome():
serialised.write(5, msg.ephemeralPubKey.get())
if msg.viewTag.isSome():
serialised.write(6, msg.viewTag.get())
return serialised
func toByteSeq*(str: string): seq[byte] {.inline.} =
## Converts a string to the corresponding byte sequence.
@(str.toOpenArrayByte(0, str.high))
proc constructRequest*(spendingPubKey: SerializedKey, viewingPubKey: SerializedKey): WakuStealthCommitmentMsg =
WakuStealthCommitmentMsg(request: true, spendingPubKey: some(spendingPubKey), viewingPubKey: some(viewingPubKey))
proc constructResponse*(stealthCommitment: SerializedKey, ephemeralPubKey: SerializedKey, viewTag: uint64): WakuStealthCommitmentMsg =
WakuStealthCommitmentMsg(request: false, stealthCommitment: some(stealthCommitment), ephemeralPubKey: some(ephemeralPubKey), viewTag: some(viewTag))