From 67a43b8ba70d3b9b8fa8896fb99684eed3ce2ff3 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 6 Jul 2022 12:59:38 -0400 Subject: [PATCH] feat: test unit for RLN (static) --- go.mod | 2 +- go.sum | 4 +- waku/v2/protocol/rln/rln_relay_test.go | 197 +++++++++++++++++++++++++ waku/v2/protocol/rln/waku_rln_relay.go | 34 +++-- 4 files changed, 223 insertions(+), 14 deletions(-) create mode 100644 waku/v2/protocol/rln/rln_relay_test.go diff --git a/go.mod b/go.mod index 13dad3b6..1476cb5d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ replace github.com/ethereum/go-ethereum v1.10.18 => github.com/status-im/go-ethe replace github.com/flynn/noise v1.0.0 => github.com/status-im/noise v1.0.1-handshakeMessages -replace github.com/decanus/go-rln => github.com/status-im/go-rln v0.0.4 +replace github.com/decanus/go-rln => github.com/status-im/go-rln v0.0.6 require ( contrib.go.opencensus.io/exporter/prometheus v0.4.1 diff --git a/go.sum b/go.sum index ab81a748..76acf707 100644 --- a/go.sum +++ b/go.sum @@ -1687,8 +1687,8 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc= github.com/status-im/go-discover v0.0.0-20220406135310-85a2ce36f63e h1:fDm8hqKGFy8LMNV8zedT3W+QYVPVDfb0F9Fr7fVf9rQ= github.com/status-im/go-discover v0.0.0-20220406135310-85a2ce36f63e/go.mod h1:u1s0ACIlweIjmJrgXyljRPSOflZLaS6ezb044+92W3c= -github.com/status-im/go-rln v0.0.4 h1:AiBngoSSy2DI98EgTZquWqNHrRcAScC34UM36TpiPMw= -github.com/status-im/go-rln v0.0.4/go.mod h1:X8j1STGa0vtbvZ4AZibPmk9cCI4FafO0+6zhaomgfNI= +github.com/status-im/go-rln v0.0.6 h1:/9IKDEIJAUEpIRDNP65/mSfHGjpAxPfghKFb1U2O99o= +github.com/status-im/go-rln v0.0.6/go.mod h1:X8j1STGa0vtbvZ4AZibPmk9cCI4FafO0+6zhaomgfNI= github.com/status-im/go-waku-rendezvous v0.0.0-20211018070416-a93f3b70c432 h1:cbNFU38iimo9fY4B7CdF/fvIF6tNPJIZjBbpfmW2EY4= github.com/status-im/go-waku-rendezvous v0.0.0-20211018070416-a93f3b70c432/go.mod h1:A8t3i0CUGtXCA0aiLsP7iyikmk/KaD/2XVvNJqGCU20= github.com/status-im/go-watchdog v1.2.0-ios-nolibproc h1:BJwZEF7OVKaXc2zErBUAolFSGzwrTBbWnN8e/6MER5E= diff --git a/waku/v2/protocol/rln/rln_relay_test.go b/waku/v2/protocol/rln/rln_relay_test.go new file mode 100644 index 00000000..cde07688 --- /dev/null +++ b/waku/v2/protocol/rln/rln_relay_test.go @@ -0,0 +1,197 @@ +package rln + +import ( + "context" + "crypto/rand" + "testing" + "time" + + r "github.com/decanus/go-rln/rln" + "github.com/status-im/go-waku/tests" + "github.com/status-im/go-waku/waku/v2/protocol/pb" + "github.com/status-im/go-waku/waku/v2/protocol/relay" + "github.com/status-im/go-waku/waku/v2/utils" + "github.com/stretchr/testify/suite" +) + +const RLNRELAY_PUBSUB_TOPIC = "waku/2/rlnrelay/proto" +const RLNRELAY_CONTENT_TOPIC = "waku/2/rlnrelay/proto" + +func TestWakuRLNRelaySuite(t *testing.T) { + suite.Run(t, new(WakuRLNRelaySuite)) +} + +type WakuRLNRelaySuite struct { + suite.Suite +} + +func (s *WakuRLNRelaySuite) TestOffchainMode() { + port, err := tests.FindFreePort(s.T(), "", 5) + s.NoError(err) + + host, err := tests.MakeHost(context.Background(), port, rand.Reader) + s.NoError(err) + + relay, err := relay.NewWakuRelay(context.Background(), host, nil, 0, utils.Logger()) + defer relay.Stop() + s.NoError(err) + + params, err := parametersKeyBytes() + s.NoError(err) + + groupKeyPairs, root, err := r.CreateMembershipList(100, params) + s.NoError(err) + + var groupIDCommitments []r.IDCommitment + for _, c := range groupKeyPairs { + groupIDCommitments = append(groupIDCommitments, c.IDCommitment) + } + + // index indicates the position of a membership key pair in the static list of group keys i.e., groupKeyPairs + // the corresponding key pair will be used to mount rlnRelay on the current node + // index also represents the index of the leaf in the Merkle tree that contains node's commitment key + index := r.MembershipIndex(5) + + wakuRLNRelay, err := RlnRelayStatic(relay, groupIDCommitments, groupKeyPairs[index], index, RLNRELAY_PUBSUB_TOPIC, RLNRELAY_CONTENT_TOPIC, nil, utils.Logger()) + s.NoError(err) + + // get the root of Merkle tree which is constructed inside the mountRlnRelay proc + calculatedRoot, err := wakuRLNRelay.RLN.GetMerkleRoot() + s.NoError(err) + + // Checks whether the Merkle tree is constructed correctly inside the mountRlnRelay func + // this check is done by comparing the tree root resulted from mountRlnRelay i.e., calculatedRoot + // against the root which is the expected root + s.Equal(root[:], calculatedRoot[:]) +} + +func (s *WakuRLNRelaySuite) TestUpdateLogAndHasDuplicate() { + + rlnRelay := &WakuRLNRelay{ + nullifierLog: make(map[r.Epoch][]r.ProofMetadata), + } + + epoch := r.GetCurrentEpoch() + + // create some dummy nullifiers and secret shares + var nullifier1, nullifier2, nullifier3 r.Nullifier + var shareX1, shareX2, shareX3 r.MerkleNode + var shareY1, shareY2, shareY3 r.MerkleNode + for i := 0; i < 32; i++ { + nullifier1[i] = 1 + nullifier2[i] = 2 + nullifier3[i] = nullifier1[i] + shareX1[i] = 1 + shareX2[i] = 2 + shareX3[i] = 3 + shareY1[i] = 1 + shareY2[i] = shareX2[i] + shareY3[i] = shareX3[i] + } + + wm1 := &pb.WakuMessage{RateLimitProof: &pb.RateLimitProof{Epoch: epoch[:], Nullifier: nullifier1[:], ShareX: shareX1[:], ShareY: shareY1[:]}} + wm2 := &pb.WakuMessage{RateLimitProof: &pb.RateLimitProof{Epoch: epoch[:], Nullifier: nullifier2[:], ShareX: shareX2[:], ShareY: shareY2[:]}} + wm3 := &pb.WakuMessage{RateLimitProof: &pb.RateLimitProof{Epoch: epoch[:], Nullifier: nullifier3[:], ShareX: shareX3[:], ShareY: shareY3[:]}} + + // check whether hasDuplicate correctly finds records with the same nullifiers but different secret shares + // no duplicate for wm1 should be found, since the log is empty + result1, err := rlnRelay.HasDuplicate(wm1) + s.NoError(err) + s.False(result1) // No duplicate is found + + // Add it to the log + added, err := rlnRelay.UpdateLog(wm1) + s.NoError(err) + s.True(added) + + // no duplicate for wm2 should be found, its nullifier differs from wm1 + result2, err := rlnRelay.HasDuplicate(wm2) + s.NoError(err) + s.False(result2) // No duplicate is found + + // Add it to the log + added, err = rlnRelay.UpdateLog(wm2) + s.NoError(err) + s.True(added) + + // wm3 has the same nullifier as wm1 but different secret shares, it should be detected as duplicate + result3, err := rlnRelay.HasDuplicate(wm3) + s.NoError(err) + s.True(result3) // It's a duplicate + +} + +func (s *WakuRLNRelaySuite) TestValidateMessage() { + params, err := parametersKeyBytes() + s.NoError(err) + + groupKeyPairs, _, err := r.CreateMembershipList(100, params) + s.NoError(err) + + var groupIDCommitments []r.IDCommitment + for _, c := range groupKeyPairs { + groupIDCommitments = append(groupIDCommitments, c.IDCommitment) + } + + // index indicates the position of a membership key pair in the static list of group keys i.e., groupKeyPairs + // the corresponding key pair will be used to mount rlnRelay on the current node + // index also represents the index of the leaf in the Merkle tree that contains node's commitment key + index := r.MembershipIndex(5) + + // Create a RLN instance + rlnInstance, err := r.NewRLN(params) + s.NoError(err) + + added := rlnInstance.AddAll(groupIDCommitments) + s.True(added) + + rlnRelay := &WakuRLNRelay{ + membershipIndex: index, + membershipKeyPair: groupKeyPairs[index], + RLN: rlnInstance, + nullifierLog: make(map[r.Epoch][]r.ProofMetadata), + log: utils.Logger(), + } + + //get the current epoch time + now := time.Now() + + // create some messages from the same peer and append rln proof to them, except wm4 + + wm1 := &pb.WakuMessage{Payload: []byte("Valid message")} + err = rlnRelay.AppendRLNProof(wm1, now) + s.NoError(err) + + // another message in the same epoch as wm1, it will break the messaging rate limit + wm2 := &pb.WakuMessage{Payload: []byte("Spam")} + err = rlnRelay.AppendRLNProof(wm2, now) + s.NoError(err) + + // wm3 points to the next epoch + wm3 := &pb.WakuMessage{Payload: []byte("Valid message")} + err = rlnRelay.AppendRLNProof(wm3, now.Add(time.Second*time.Duration(r.EPOCH_UNIT_SECONDS))) + s.NoError(err) + + wm4 := &pb.WakuMessage{Payload: []byte("Invalid message")} + + // valid message + msgValidate1, err := rlnRelay.ValidateMessage(wm1, &now) + s.NoError(err) + + // wm2 is published within the same Epoch as wm1 and should be found as spam + msgValidate2, err := rlnRelay.ValidateMessage(wm2, &now) + s.NoError(err) + + // a valid message should be validated successfully + msgValidate3, err := rlnRelay.ValidateMessage(wm3, &now) + s.NoError(err) + + // wm4 has no rln proof and should not be validated + msgValidate4, err := rlnRelay.ValidateMessage(wm4, &now) + s.NoError(err) + + s.Equal(MessageValidationResult_Valid, msgValidate1) + s.Equal(MessageValidationResult_Spam, msgValidate2) + s.Equal(MessageValidationResult_Valid, msgValidate3) + s.Equal(MessageValidationResult_Invalid, msgValidate4) +} diff --git a/waku/v2/protocol/rln/waku_rln_relay.go b/waku/v2/protocol/rln/waku_rln_relay.go index 078f707d..844dfcc4 100644 --- a/waku/v2/protocol/rln/waku_rln_relay.go +++ b/waku/v2/protocol/rln/waku_rln_relay.go @@ -19,6 +19,14 @@ import ( "go.uber.org/zap" ) +// the rln-relay epoch length in seconds + +// the maximum clock difference between peers in seconds +const MAX_CLOCK_GAP_SECONDS = 20 + +// maximum allowed gap between the epochs of messages' RateLimitProofs +const MAX_EPOCH_GAP = int64(MAX_CLOCK_GAP_SECONDS / r.EPOCH_UNIT_SECONDS) + type WakuRLNRelay struct { membershipKeyPair r.MembershipKeyPair @@ -156,7 +164,7 @@ func (rln *WakuRLNRelay) UpdateLog(msg *pb.WakuMessage) (bool, error) { return true, nil } -func (rln *WakuRLNRelay) ValidateMessage(msg *pb.WakuMessage, optionalTime *time.Duration) (MessageValidationResult, error) { +func (rln *WakuRLNRelay) ValidateMessage(msg *pb.WakuMessage, optionalTime *time.Time) (MessageValidationResult, error) { // validate the supplied `msg` based on the waku-rln-relay routing protocol i.e., // the `msg`'s epoch is within MAX_EPOCH_GAP of the current epoch // the `msg` has valid rate limit proof @@ -179,15 +187,18 @@ func (rln *WakuRLNRelay) ValidateMessage(msg *pb.WakuMessage, optionalTime *time } msgProof := ToRateLimitProof(msg) + if msgProof == nil { + // message does not contain a proof + rln.log.Debug("invalid message: message does not contain a proof") + return MessageValidationResult_Invalid, nil + } - // calculate the gaps + // calculate the gaps and validate the epoch gap := r.Diff(epoch, msgProof.Epoch) - - // validate the epoch - if int64(math.Abs(float64(gap))) >= r.MAX_EPOCH_GAP { + if int64(math.Abs(float64(gap))) >= MAX_EPOCH_GAP { // message's epoch is too old or too ahead // accept messages whose epoch is within +-MAX_EPOCH_GAP from the current epoch - //debug "invalid message: epoch gap exceeds a threshold", gap = gap, payload = string.fromBytes(msg.payload) + rln.log.Debug("invalid message: epoch gap exceeds a threshold", zap.Int64("gap", gap)) return MessageValidationResult_Invalid, nil } @@ -196,18 +207,19 @@ func (rln *WakuRLNRelay) ValidateMessage(msg *pb.WakuMessage, optionalTime *time input := append(msg.Payload, contentTopicBytes...) if !rln.RLN.Verify(input, *msgProof) { // invalid proof - //debug "invalid message: invalid proof", payload = string.fromBytes(msg.payload) + rln.log.Debug("invalid message: invalid proof") return MessageValidationResult_Invalid, nil } // check if double messaging has happened hasDup, err := rln.HasDuplicate(msg) if err != nil { + rln.log.Debug("validation error", zap.Error(err)) return MessageValidationResult_Unknown, err } if hasDup { - // debug "invalid message: message is a spam", payload = string.fromBytes(msg.payload) + rln.log.Debug("spam received") return MessageValidationResult_Spam, nil } @@ -219,11 +231,11 @@ func (rln *WakuRLNRelay) ValidateMessage(msg *pb.WakuMessage, optionalTime *time return MessageValidationResult_Unknown, err } - //debug "message is valid", payload = string.fromBytes(msg.payload) + rln.log.Debug("message is valid") return MessageValidationResult_Valid, nil } -func (rln *WakuRLNRelay) AppendRLNProof(msg *pb.WakuMessage, senderEpochTime time.Duration) error { +func (rln *WakuRLNRelay) AppendRLNProof(msg *pb.WakuMessage, senderEpochTime time.Time) error { // returns error if it could not create and append a `RateLimitProof` to the supplied `msg` // `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds. // The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`) @@ -367,7 +379,7 @@ func toRLNSignal(wakuMessage *pb.WakuMessage) []byte { } func ToRateLimitProof(msg *pb.WakuMessage) *r.RateLimitProof { - if msg == nil { + if msg == nil || msg.RateLimitProof == nil { return nil }