feat: test unit for RLN (static)

This commit is contained in:
Richard Ramos 2022-07-06 12:59:38 -04:00
parent 1636a33835
commit 67a43b8ba7
4 changed files with 223 additions and 14 deletions

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

View File

@ -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)
}

View File

@ -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
}