From c2ce8326ff69b151992e154683ee9cf37e2a1795 Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 10 Aug 2022 09:03:25 -0400 Subject: [PATCH] test: rln onchain --- Makefile | 16 +- waku/v2/protocol/rln/onchain_test.go | 300 +++++++++++++++++++++++++ waku/v2/protocol/rln/rln_relay_test.go | 66 +++--- 3 files changed, 347 insertions(+), 35 deletions(-) create mode 100644 waku/v2/protocol/rln/onchain_test.go diff --git a/Makefile b/Makefile index 320fdf95..c6113cff 100644 --- a/Makefile +++ b/Makefile @@ -57,7 +57,7 @@ lint: @golangci-lint --exclude=SA1019 run ./... --deadline=5m test: - ${GOBIN} test ./waku/... -coverprofile=${GO_TEST_OUTFILE}.tmp + ${GOBIN} test -timeout 300s ./waku/... -coverprofile=${GO_TEST_OUTFILE}.tmp cat ${GO_TEST_OUTFILE}.tmp | grep -v ".pb.go" > ${GO_TEST_OUTFILE} ${GOBIN} tool cover -html=${GO_TEST_OUTFILE} -o ${GO_HTML_COV} @@ -77,7 +77,7 @@ generate: coverage: - ${GOBIN} test -count 1 -coverprofile=coverage.out ./... + ${GOBIN} test -timeout 300s -count 1 -coverprofile=coverage.out ./... ${GOBIN} tool cover -html=coverage.out -o=coverage.html # build a docker image for the fleet @@ -149,3 +149,15 @@ install-gomobile: install-xtools build-linux-pkg: ./scripts/linux/docker-run.sh ls -la ./build/*.rpm ./build/*.deb + +TEST_MNEMONIC="swim relax risk shy chimney please usual search industry board music segment" + +start-ganache: + docker run -p 8545:8545 --name ganache-cli --rm -d trufflesuite/ganache-cli:latest -m ${TEST_MNEMONIC} + +stop-ganache: + docker stop ganache-cli + +test-rln: + go test -timeout 30s -v -count 1 github.com/status-im/go-waku/waku/v2/protocol/rln + \ No newline at end of file diff --git a/waku/v2/protocol/rln/onchain_test.go b/waku/v2/protocol/rln/onchain_test.go new file mode 100644 index 00000000..237c0a12 --- /dev/null +++ b/waku/v2/protocol/rln/onchain_test.go @@ -0,0 +1,300 @@ +package rln + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "errors" + "math/big" + "sync" + "testing" + "time" + + r "github.com/status-im/go-rln/rln" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/status-im/go-waku/tests" + "github.com/status-im/go-waku/waku/v2/protocol/relay" + "github.com/status-im/go-waku/waku/v2/protocol/rln/contracts" + "github.com/status-im/go-waku/waku/v2/utils" + "github.com/stretchr/testify/suite" +) + +const ETH_CLIENT_ADDRESS = "ws://localhost:8545" + +func TestWakuRLNRelayDynamicSuite(t *testing.T) { + suite.Run(t, new(WakuRLNRelayDynamicSuite)) +} + +type WakuRLNRelayDynamicSuite struct { + suite.Suite + + backend *ethclient.Client + chainID *big.Int + rlnAddr common.Address + rlnContract *contracts.RLN + + u1PrivKey *ecdsa.PrivateKey + u2PrivKey *ecdsa.PrivateKey + u3PrivKey *ecdsa.PrivateKey + u4PrivKey *ecdsa.PrivateKey + u5PrivKey *ecdsa.PrivateKey +} + +func (s *WakuRLNRelayDynamicSuite) SetupTest() { + backend, err := ethclient.Dial(ETH_CLIENT_ADDRESS) + s.Require().NoError(err) + + chainID, err := backend.ChainID(context.TODO()) + s.Require().NoError(err) + + // TODO: obtain account list from ganache mnemonic or from eth_accounts + + s.u1PrivKey, err = crypto.ToECDSA(common.FromHex("0x156ec84a451d8a2d0062993242b6c4e863647f5544ff8030f23578d4142f43f8")) + s.Require().NoError(err) + s.u2PrivKey, err = crypto.ToECDSA(common.FromHex("0xa00da43843ad6b5161ddbace48f293ac3f82f8a8257af34de4c32900bb6e9a97")) + s.Require().NoError(err) + s.u3PrivKey, err = crypto.ToECDSA(common.FromHex("0xa4c8d3ed78cd722521fac9d734c45187a4f5e887570be1f707a7bbce054c01ea")) + s.Require().NoError(err) + s.u4PrivKey, err = crypto.ToECDSA(common.FromHex("0x6b11ba548a7fd1958eb156877cc7bdd02d99d876b55381aa9b106c16b0b7a805")) + s.Require().NoError(err) + s.u5PrivKey, err = crypto.ToECDSA(common.FromHex("0x0410196287d0af405e5c16f610de52416bd48be74836dbca93d73e24bffb5a81")) + s.Require().NoError(err) + + s.backend = backend + s.chainID = chainID + + // Deploying contracts + auth, err := bind.NewKeyedTransactorWithChainID(s.u1PrivKey, chainID) + s.Require().NoError(err) + + poseidonHasherAddr, _, _, err := contracts.DeployPoseidonHasher(auth, backend) + s.Require().NoError(err) + + rlnAddr, _, rlnContract, err := contracts.DeployRLN(auth, backend, MEMBERSHIP_FEE, big.NewInt(20), poseidonHasherAddr) + s.Require().NoError(err) + + s.rlnAddr = rlnAddr + s.rlnContract = rlnContract +} + +func (s *WakuRLNRelayDynamicSuite) register(privKey *ecdsa.PrivateKey, commitment *big.Int) { + auth, err := bind.NewKeyedTransactorWithChainID(privKey, s.chainID) + s.Require().NoError(err) + + auth.Value = MEMBERSHIP_FEE + auth.Context = context.TODO() + + tx, err := s.rlnContract.Register(auth, commitment) + s.Require().NoError(err) + _, err = bind.WaitMined(context.TODO(), s.backend, tx) + s.Require().NoError(err) +} + +func (s *WakuRLNRelayDynamicSuite) TestDynamicGroupManagement() { + params, err := parametersKeyBytes() + s.Require().NoError(err) + + // Create a RLN instance + rlnInstance, err := r.NewRLN(params) + s.Require().NoError(err) + + keyPair, err := rlnInstance.MembershipKeyGen() + s.Require().NoError(err) + + // initialize the WakuRLNRelay + rlnPeer := &WakuRLNRelay{ + ctx: context.TODO(), + membershipIndex: r.MembershipIndex(0), + membershipContractAddress: s.rlnAddr, + ethClientAddress: ETH_CLIENT_ADDRESS, + ethAccountPrivateKey: s.u1PrivKey, + RLN: rlnInstance, + log: utils.Logger(), + nullifierLog: make(map[r.Epoch][]r.ProofMetadata), + membershipKeyPair: *keyPair, + } + + // generate another membership key pair + keyPair2, err := rlnInstance.MembershipKeyGen() + s.Require().NoError(err) + + wg := &sync.WaitGroup{} + wg.Add(2) + + handler := func(pubkey r.IDCommitment, index r.MembershipIndex) error { + if pubkey == keyPair.IDCommitment || pubkey == keyPair2.IDCommitment { + wg.Done() + } + + if !rlnInstance.InsertMember(pubkey) { + return errors.New("couldn't insert member") + } + + return nil + } + + // mount the handler for listening to the contract events + go rlnPeer.HandleGroupUpdates(handler) + + // Register first member + s.register(s.u1PrivKey, toBigInt(keyPair.IDCommitment[:])) + + // Register second member + s.register(s.u2PrivKey, toBigInt(keyPair2.IDCommitment[:])) + + wg.Wait() +} + +func (s *WakuRLNRelayDynamicSuite) TestInsertKeyMembershipContract() { + + s.register(s.u1PrivKey, big.NewInt(20)) + + // Batch Register + auth, err := bind.NewKeyedTransactorWithChainID(s.u2PrivKey, s.chainID) + s.Require().NoError(err) + + auth.Value = MEMBERSHIP_FEE.Mul(big.NewInt(2), MEMBERSHIP_FEE) + auth.Context = context.TODO() + + tx, err := s.rlnContract.RegisterBatch(auth, []*big.Int{big.NewInt(20), big.NewInt(21)}) + s.Require().NoError(err) + + _, err = bind.WaitMined(context.TODO(), s.backend, tx) + s.Require().NoError(err) +} + +func (s *WakuRLNRelayDynamicSuite) TestRegistrationProcedure() { + params, err := parametersKeyBytes() + s.Require().NoError(err) + + // Create a RLN instance + rlnInstance, err := r.NewRLN(params) + s.Require().NoError(err) + + keyPair, err := rlnInstance.MembershipKeyGen() + s.Require().NoError(err) + + // initialize the WakuRLNRelay + rlnPeer := &WakuRLNRelay{ + ctx: context.TODO(), + membershipIndex: r.MembershipIndex(0), + membershipContractAddress: s.rlnAddr, + ethClientAddress: ETH_CLIENT_ADDRESS, + ethAccountPrivateKey: s.u1PrivKey, + RLN: rlnInstance, + log: utils.Logger(), + nullifierLog: make(map[r.Epoch][]r.ProofMetadata), + membershipKeyPair: *keyPair, + } + + _, err = rlnPeer.Register(context.TODO()) + s.Require().NoError(err) +} + +func (s *WakuRLNRelayDynamicSuite) TestMerkleTreeConstruction() { + params, err := parametersKeyBytes() + s.Require().NoError(err) + + // Create a RLN instance + rlnInstance, err := r.NewRLN(params) + s.Require().NoError(err) + + keyPair1, err := rlnInstance.MembershipKeyGen() + s.Require().NoError(err) + + keyPair2, err := rlnInstance.MembershipKeyGen() + s.Require().NoError(err) + + r1 := rlnInstance.InsertMember(keyPair1.IDCommitment) + r2 := rlnInstance.InsertMember(keyPair2.IDCommitment) + s.Require().True(r1) + s.Require().True(r2) + + // get the Merkle root + expectedRoot, err := rlnInstance.GetMerkleRoot() + s.Require().NoError(err) + + // register the members to the contract + s.register(s.u1PrivKey, toBigInt(keyPair1.IDCommitment[:])) + s.register(s.u1PrivKey, toBigInt(keyPair2.IDCommitment[:])) + + port, err := tests.FindFreePort(s.T(), "", 5) + s.Require().NoError(err) + + host, err := tests.MakeHost(context.TODO(), port, rand.Reader) + s.Require().NoError(err) + + relay, err := relay.NewWakuRelay(context.TODO(), host, nil, 0, utils.Logger()) + defer relay.Stop() + s.Require().NoError(err) + + sub, err := relay.SubscribeToTopic(context.TODO(), RLNRELAY_PUBSUB_TOPIC) + s.Require().NoError(err) + defer sub.Unsubscribe() + + // mount the rln relay protocol in the on-chain/dynamic mode + rlnRelay, err := RlnRelayDynamic(context.TODO(), relay, ETH_CLIENT_ADDRESS, nil, s.rlnAddr, keyPair1, r.MembershipIndex(0), RLNRELAY_PUBSUB_TOPIC, RLNRELAY_CONTENT_TOPIC, nil, utils.Logger()) + s.Require().NoError(err) + + // wait for the event to reach the group handler + time.Sleep(1 * time.Second) + + // rln pks are inserted into the rln peer's Merkle tree and the resulting root + // is expected to be the same as the calculatedRoot i.e., the one calculated outside of the mountRlnRelayDynamic proc + calculatedRoot, err := rlnRelay.RLN.GetMerkleRoot() + s.Require().NoError(err) + + s.Require().Equal(expectedRoot, calculatedRoot) +} + +func (s *WakuRLNRelayDynamicSuite) TestCorrectRegistrationOfPeers() { + + // Node 1 ============================================================ + port1, err := tests.FindFreePort(s.T(), "", 5) + s.Require().NoError(err) + + host1, err := tests.MakeHost(context.TODO(), port1, rand.Reader) + s.Require().NoError(err) + + relay1, err := relay.NewWakuRelay(context.TODO(), host1, nil, 0, utils.Logger()) + defer relay1.Stop() + s.Require().NoError(err) + + sub1, err := relay1.SubscribeToTopic(context.TODO(), RLNRELAY_PUBSUB_TOPIC) + s.Require().NoError(err) + defer sub1.Unsubscribe() + + // mount the rln relay protocol in the on-chain/dynamic mode + rlnRelay1, err := RlnRelayDynamic(context.TODO(), relay1, ETH_CLIENT_ADDRESS, s.u1PrivKey, s.rlnAddr, nil, r.MembershipIndex(0), RLNRELAY_PUBSUB_TOPIC, RLNRELAY_CONTENT_TOPIC, nil, utils.Logger()) + s.Require().NoError(err) + + // Node 2 ============================================================ + port2, err := tests.FindFreePort(s.T(), "", 5) + s.Require().NoError(err) + + host2, err := tests.MakeHost(context.TODO(), port2, rand.Reader) + s.Require().NoError(err) + + relay2, err := relay.NewWakuRelay(context.TODO(), host2, nil, 0, utils.Logger()) + defer relay2.Stop() + s.Require().NoError(err) + + sub2, err := relay2.SubscribeToTopic(context.TODO(), RLNRELAY_PUBSUB_TOPIC) + s.Require().NoError(err) + defer sub2.Unsubscribe() + + // mount the rln relay protocol in the on-chain/dynamic mode + rlnRelay2, err := RlnRelayDynamic(context.TODO(), relay2, ETH_CLIENT_ADDRESS, s.u2PrivKey, s.rlnAddr, nil, r.MembershipIndex(0), RLNRELAY_PUBSUB_TOPIC, RLNRELAY_CONTENT_TOPIC, nil, utils.Logger()) + s.Require().NoError(err) + + // ================================== + // the two nodes should be registered into the contract + // since nodes are spun up sequentially + // the first node has index 0 whereas the second node gets index 1 + s.Require().Equal(r.MembershipIndex(0), rlnRelay1.membershipIndex) + s.Require().Equal(r.MembershipIndex(1), rlnRelay2.membershipIndex) +} diff --git a/waku/v2/protocol/rln/rln_relay_test.go b/waku/v2/protocol/rln/rln_relay_test.go index b4fd0c7c..b8f865c1 100644 --- a/waku/v2/protocol/rln/rln_relay_test.go +++ b/waku/v2/protocol/rln/rln_relay_test.go @@ -27,20 +27,20 @@ type WakuRLNRelaySuite struct { func (s *WakuRLNRelaySuite) TestOffchainMode() { port, err := tests.FindFreePort(s.T(), "", 5) - s.NoError(err) + s.Require().NoError(err) host, err := tests.MakeHost(context.Background(), port, rand.Reader) - s.NoError(err) + s.Require().NoError(err) relay, err := relay.NewWakuRelay(context.Background(), host, nil, 0, utils.Logger()) defer relay.Stop() - s.NoError(err) + s.Require().NoError(err) params, err := parametersKeyBytes() - s.NoError(err) + s.Require().NoError(err) groupKeyPairs, root, err := r.CreateMembershipList(100, params) - s.NoError(err) + s.Require().NoError(err) var groupIDCommitments []r.IDCommitment for _, c := range groupKeyPairs { @@ -53,16 +53,16 @@ func (s *WakuRLNRelaySuite) TestOffchainMode() { index := r.MembershipIndex(5) wakuRLNRelay, err := RlnRelayStatic(context.TODO(), relay, groupIDCommitments, groupKeyPairs[index], index, RLNRELAY_PUBSUB_TOPIC, RLNRELAY_CONTENT_TOPIC, nil, utils.Logger()) - s.NoError(err) + s.Require().NoError(err) // get the root of Merkle tree which is constructed inside the mountRlnRelay proc calculatedRoot, err := wakuRLNRelay.RLN.GetMerkleRoot() - s.NoError(err) + s.Require().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[:]) + s.Require().Equal(root[:], calculatedRoot[:]) } func (s *WakuRLNRelaySuite) TestUpdateLogAndHasDuplicate() { @@ -96,37 +96,37 @@ func (s *WakuRLNRelaySuite) TestUpdateLogAndHasDuplicate() { // 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 + s.Require().NoError(err) + s.Require().False(result1) // No duplicate is found // Add it to the log added, err := rlnRelay.UpdateLog(wm1) - s.NoError(err) - s.True(added) + s.Require().NoError(err) + s.Require().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 + s.Require().NoError(err) + s.Require().False(result2) // No duplicate is found // Add it to the log added, err = rlnRelay.UpdateLog(wm2) - s.NoError(err) - s.True(added) + s.Require().NoError(err) + s.Require().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 + s.Require().NoError(err) + s.Require().True(result3) // It's a duplicate } func (s *WakuRLNRelaySuite) TestValidateMessage() { params, err := parametersKeyBytes() - s.NoError(err) + s.Require().NoError(err) groupKeyPairs, _, err := r.CreateMembershipList(100, params) - s.NoError(err) + s.Require().NoError(err) var groupIDCommitments []r.IDCommitment for _, c := range groupKeyPairs { @@ -140,10 +140,10 @@ func (s *WakuRLNRelaySuite) TestValidateMessage() { // Create a RLN instance rlnInstance, err := r.NewRLN(params) - s.NoError(err) + s.Require().NoError(err) added := rlnInstance.AddAll(groupIDCommitments) - s.True(added) + s.Require().True(added) rlnRelay := &WakuRLNRelay{ membershipIndex: index, @@ -160,38 +160,38 @@ func (s *WakuRLNRelaySuite) TestValidateMessage() { wm1 := &pb.WakuMessage{Payload: []byte("Valid message")} err = rlnRelay.AppendRLNProof(wm1, now) - s.NoError(err) + s.Require().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) + s.Require().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) + s.Require().NoError(err) wm4 := &pb.WakuMessage{Payload: []byte("Invalid message")} // valid message msgValidate1, err := rlnRelay.ValidateMessage(wm1, &now) - s.NoError(err) + s.Require().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) + s.Require().NoError(err) // a valid message should be validated successfully msgValidate3, err := rlnRelay.ValidateMessage(wm3, &now) - s.NoError(err) + s.Require().NoError(err) // wm4 has no rln proof and should not be validated msgValidate4, err := rlnRelay.ValidateMessage(wm4, &now) - s.NoError(err) + s.Require().NoError(err) - s.Equal(MessageValidationResult_Valid, msgValidate1) - s.Equal(MessageValidationResult_Spam, msgValidate2) - s.Equal(MessageValidationResult_Valid, msgValidate3) - s.Equal(MessageValidationResult_Invalid, msgValidate4) + s.Require().Equal(MessageValidationResult_Valid, msgValidate1) + s.Require().Equal(MessageValidationResult_Spam, msgValidate2) + s.Require().Equal(MessageValidationResult_Valid, msgValidate3) + s.Require().Equal(MessageValidationResult_Invalid, msgValidate4) }