mirror of
https://github.com/status-im/go-waku.git
synced 2025-02-03 17:34:57 +00:00
chore(rln-relay): docs and docker
This commit is contained in:
parent
bfc3083fb1
commit
e1a84aab0e
11
Dockerfile
11
Dockerfile
@ -1,10 +1,5 @@
|
||||
# BUILD IMAGE --------------------------------------------------------
|
||||
FROM golang:1.19-alpine3.16 as builder
|
||||
|
||||
# Get build tools and required header files
|
||||
RUN apk add --no-cache build-base
|
||||
RUN apk add --no-cache bash
|
||||
RUN apk add --no-cache git
|
||||
FROM golang:1.19 as builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
@ -14,7 +9,7 @@ RUN make -j$(nproc) build
|
||||
|
||||
# ACTUAL IMAGE -------------------------------------------------------
|
||||
|
||||
FROM alpine:3.16
|
||||
FROM debian:12.1-slim
|
||||
|
||||
ARG GIT_COMMIT=unknown
|
||||
|
||||
@ -26,6 +21,8 @@ LABEL commit=$GIT_COMMIT
|
||||
# color, nocolor, json
|
||||
ENV GOLOG_LOG_FMT=nocolor
|
||||
|
||||
RUN apt update && apt install -y ca-certificates
|
||||
|
||||
# go-waku default ports
|
||||
EXPOSE 9000 30303 60000 60001 8008 8009
|
||||
|
||||
|
5
Makefile
5
Makefile
@ -124,7 +124,10 @@ build-example-filter2:
|
||||
build-example-c-bindings:
|
||||
cd examples/c-bindings && $(MAKE)
|
||||
|
||||
build-example: build-example-basic2 build-example-chat-2 build-example-filter2 build-example-c-bindings
|
||||
build-example-rln:
|
||||
cd examples/rln && $(MAKE)
|
||||
|
||||
build-example: build-example-basic2 build-example-chat-2 build-example-filter2 build-example-c-bindings build-example-rln
|
||||
|
||||
static-library:
|
||||
@echo "Building static library..."
|
||||
|
@ -5,7 +5,7 @@ A Go implementation of the [Waku v2 protocol](https://rfc.vac.dev/spec/10).
|
||||
<p align="left">
|
||||
<a href="https://goreportcard.com/report/github.com/waku-org/go-waku"><img src="https://goreportcard.com/badge/github.com/waku-org/go-waku" /></a>
|
||||
<a href="https://godoc.org/github.com/waku-org/go-waku"><img src="http://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square" /></a>
|
||||
<a href=""><img src="https://img.shields.io/badge/golang-%3E%3D1.18.0-orange.svg?style=flat-square" /></a>
|
||||
<a href=""><img src="https://img.shields.io/badge/golang-%3E%3D1.19.0-orange.svg?style=flat-square" /></a>
|
||||
<a href="https://codeclimate.com/github/waku-org/go-waku/maintainability"><img src="https://api.codeclimate.com/v1/badges/426bdff6a339ff4d536b/maintainability" /></a>
|
||||
<br>
|
||||
</p>
|
||||
@ -38,7 +38,7 @@ nix develop
|
||||
#### Docker
|
||||
```
|
||||
docker run -i -t -p 60000:60000 -p 9000:9000/udp \
|
||||
statusteam/go-waku:v0.5.2 \ # or, the image:tag of your choice
|
||||
statusteam/go-waku:latest \ # or, the image:tag of your choice
|
||||
--dns-discovery:true \
|
||||
--dns-discovery-url:enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@prod.waku.nodes.status.im \
|
||||
--discv5-discovery
|
||||
@ -47,9 +47,9 @@ docker run -i -t -p 60000:60000 -p 9000:9000/udp \
|
||||
or build and run the image with:
|
||||
|
||||
```
|
||||
docker build -t go-waku:latest .
|
||||
docker build -t statusteam/go-waku:latest .
|
||||
|
||||
docker run go-waku:latest --help
|
||||
docker run statusteam/go-waku:latest --help
|
||||
```
|
||||
|
||||
#### Building on windows
|
||||
|
@ -4,7 +4,7 @@ Go-waku can be built on Linux, macOS and Windows
|
||||
|
||||
## Installing dependencies
|
||||
|
||||
Cloning and building go-waku requires Go +1.17, a C compiler, Make, Bash and Git.
|
||||
Cloning and building go-waku requires Go +1.19, a C compiler, Make, Bash and Git.
|
||||
|
||||
Go can be installed by following [these instructions](https://go.dev/doc/install)
|
||||
|
||||
@ -31,13 +31,13 @@ Assuming you use [Homebrew](https://brew.sh/) to manage packages
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
## Building nwaku
|
||||
## Building go-waku
|
||||
|
||||
### 1. Clone the nwaku repository
|
||||
### 1. Clone the go-waku repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/waku-org/go-waku
|
||||
cd nwaku
|
||||
cd go-waku
|
||||
```
|
||||
|
||||
### 2. Build waku
|
||||
|
74
docs/operators/how-to/run-with-rln.md
Normal file
74
docs/operators/how-to/run-with-rln.md
Normal file
@ -0,0 +1,74 @@
|
||||
# How to run spam prevention on your go-waku node (RLN)
|
||||
|
||||
This guide explains how to run a go-waku node with RLN (Rate Limiting Nullifier) enabled.
|
||||
|
||||
[RLN](https://rfc.vac.dev/spec/32/) is a protocol integrated into waku v2,
|
||||
which prevents spam-based attacks on the network.
|
||||
|
||||
For further background on the research for RLN tailored to waku, refer
|
||||
to [this](https://rfc.vac.dev/spec/17/) RFC.
|
||||
|
||||
Registering to the membership group has been left out for brevity.
|
||||
If you would like to register to the membership group and send messages with RLN,
|
||||
refer to the [on-chain chat2 tutorial](../../tutorial/onchain-rln-relay-chat2.md).
|
||||
|
||||
This guide specifically allows a node to participate in RLN testnet
|
||||
You may alter the rln-specific arguments as required.
|
||||
|
||||
|
||||
## 1. Update the runtime arguments
|
||||
|
||||
Follow the steps from the [build](./build.md) and [run](./run.md) guides while replacing the run command with -
|
||||
|
||||
```bash
|
||||
export WAKU_FLEET=<enrtree of the fleet>
|
||||
export SEPOLIA_WS_NODE_ADDRESS=<WS RPC URL to a Sepolia Node>
|
||||
export RLN_RELAY_CONTRACT_ADDRESS="0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4" # Replace this with any compatible implementation
|
||||
$WAKUNODE_DIR/build/waku \
|
||||
--dns-discovery \
|
||||
--dns-discovery-url="$WAKU_FLEET" \
|
||||
--discv5-discovery=true \
|
||||
--rln-relay=true \
|
||||
--rln-relay-dynamic=true \
|
||||
--rln-relay-eth-contract-address="$RLN_RELAY_CONTRACT_ADDRESS" \
|
||||
--rln-relay-eth-client-address="$SEPOLIA_WS_NODE_ADDRESS"
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
If you installed go-waku using a `.dpkg` or `.rpm` package, you can use the `waku` command instead of building go-waku yourself
|
||||
|
||||
OR
|
||||
|
||||
If you have the go-waku node within docker, you can replace the run command with -
|
||||
|
||||
```bash
|
||||
export WAKU_FLEET=<enrtree of the fleet>
|
||||
export SEPOLIA_WS_NODE_ADDRESS=<WS RPC URL to a Sepolia Node>
|
||||
export RLN_RELAY_CONTRACT_ADDRESS="0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4" # Replace this with any compatible implementation
|
||||
docker run -i -t -p 60000:60000 -p 9000:9000/udp \
|
||||
-v /absolute/path/to/your/rlnKeystore.json:/rlnKeystore.json:ro \
|
||||
statusteam/go-waku:latest \
|
||||
--dns-discovery=true \
|
||||
--dns-discovery-url="$WAKU_FLEET" \
|
||||
--discv5-discovery \
|
||||
--rln-relay=true \
|
||||
--rln-relay-dynamic=true \
|
||||
--rln-relay-eth-contract-address="$RLN_RELAY_CONTRACT_ADDRESS" \
|
||||
--rln-relay-eth-client-address="$SEPOLIA_WS_NODE_ADDRESS"
|
||||
```
|
||||
|
||||
Following is the list of additional fields that have been added to the
|
||||
runtime arguments -
|
||||
|
||||
1. `--rln-relay`: Allows waku-rln-relay to be mounted into the setup of the go-waku node. All messages sent and received in this node will require to contain a valid proof that will be verified, and nodes that relay messages with invalid proofs will have their peer scoring affected negatively and will be eventually disconnected.
|
||||
2. `--rln-relay-dynamic`: Enables waku-rln-relay to connect to an ethereum node to fetch the membership group
|
||||
3. `--rln-relay-eth-contract-address`: The contract address of an RLN membership group
|
||||
4. `--rln-relay-eth-client-address`: The websocket url to a Sepolia ethereum node
|
||||
|
||||
The `--dns-discovery-url` flag should contain a valid URL with nodes encoded according to EIP-1459. You can read more about DNS Discovery [here](https://github.com/waku-org/nwaku/blob/master/docs/tutorial/dns-disc.md)
|
||||
|
||||
You should now have go-waku running, with RLN enabled!
|
||||
|
||||
|
||||
> Note: This guide will be updated in the future to include features like slashing.
|
@ -17,13 +17,13 @@ See [this tutorial](./configure-key.md) if you want to generate and configure a
|
||||
- enable `relay` protocol
|
||||
- subscribe to the default pubsub topic, namely `/waku/2/default-waku/proto`
|
||||
- enable `store` protocol, but only as a client.
|
||||
This implies that the nwaku node will not persist any historical messages itself,
|
||||
This implies that the go-waku node will not persist any historical messages itself,
|
||||
but can query `store` service peers who do so.
|
||||
To configure `store` as a service node,
|
||||
see [this tutorial](./configure-store.md).
|
||||
|
||||
> **Note:** The `filter` and `lightpush` protocols are _not_ enabled by default.
|
||||
Consult the [configuration guide](./configure.md) on how to configure your nwaku node to run these protocols.
|
||||
Consult the [configuration guide](./configure.md) on how to configure your go-waku node to run these protocols.
|
||||
|
||||
Some typical non-default configurations are explained below.
|
||||
For more advanced configuration, see the [configuration guide](./configure.md).
|
||||
@ -33,7 +33,7 @@ Different ways to connect to other nodes are expanded upon in our [connection gu
|
||||
|
||||
Find the log entry beginning with `Listening on`.
|
||||
It should be printed at INFO level when you start your node
|
||||
and contains a list of all publically announced listening addresses for the nwaku node.
|
||||
and contains a list of all publically announced listening addresses for the go-waku node.
|
||||
|
||||
For example
|
||||
|
||||
@ -80,7 +80,7 @@ returns a response similar to
|
||||
|
||||
## Finding your discoverable ENR address(es)
|
||||
|
||||
A nwaku node can encode its addressing information in an [Ethereum Node Record (ENR)](https://eips.ethereum.org/EIPS/eip-778) according to [`31/WAKU2-ENR`](https://rfc.vac.dev/spec/31/).
|
||||
A go-waku node can encode its addressing information in an [Ethereum Node Record (ENR)](https://eips.ethereum.org/EIPS/eip-778) according to [`31/WAKU2-ENR`](https://rfc.vac.dev/spec/31/).
|
||||
These ENR are most often used for discovery purposes.
|
||||
|
||||
### ENR for DNS discovery and DiscV5
|
||||
@ -111,10 +111,10 @@ to continually discover and connect to random peers for a more robust mesh.
|
||||
|
||||
A typical run configuration for a go-waku node is to connect to existing peers with known listening addresses using the `--staticnode` option.
|
||||
The `--staticnode` option can be repeated for each peer you want to connect to on startup.
|
||||
This is also useful if you want to run several nwaku instances locally
|
||||
This is also useful if you want to run several go-waku instances locally
|
||||
and therefore know the listening addresses of all peers.
|
||||
|
||||
As an example, consider a nwaku node that connects to two known peers
|
||||
As an example, consider a go-waku node that connects to two known peers
|
||||
on the same local host (with IP `0.0.0.0`)
|
||||
with TCP ports `60002` and `60003`,
|
||||
and peer IDs `16Uiu2HAkzjwwgEAXfeGNMKFPSpc6vGBRqCdTLG5q3Gmk2v4pQw7H` and `16Uiu2HAmFBA7LGtwY5WVVikdmXVo3cKLqkmvVtuDu63fe8safeQJ` respectively.
|
||||
@ -180,5 +180,5 @@ See our [store configuration tutorial](./configure-store.md) for more.
|
||||
A running go-waku node can be interacted with using the [Waku v2 JSON RPC API](https://rfc.vac.dev/spec/16/).
|
||||
|
||||
> **Note:** Private and Admin API functionality are disabled by default.
|
||||
To configure a nwaku node with these enabled,
|
||||
To configure a go-waku node with these enabled,
|
||||
use the `--rpc-admin:true` and `--rpc-private:true` CLI options.
|
@ -16,7 +16,7 @@ or download a precompiled binary from our [releases page](https://github.com/wak
|
||||
[Run the go-waku node](./how-to/run.md) using a default or common configuration
|
||||
or [configure](./how-to/configure.md) the node for more advanced use cases.
|
||||
|
||||
[Connect](./how-to/connect.md) the nwaku node to other peers to start communicating.
|
||||
[Connect](./how-to/connect.md) the go-waku node to other peers to start communicating.
|
||||
|
||||
## 3. Interact
|
||||
|
||||
|
@ -11,14 +11,14 @@ To complete this tutorial, you will need
|
||||
1. An rln keystore file with credentials to the rln membership smart contract you wish to use. You may obtain this by registering to the smart contract and generating a keystore. It is possible to use go-waku to register into the smart contract:
|
||||
```
|
||||
make
|
||||
./build/waku generate-rln-credentials --eth-account-private-key=<private-key> --eth-contract-address=<0x000...> --eth-client-address=<eth-client-rpc-or-wss-endpoint> --cred-path=rlnKeystore.json
|
||||
./build/waku generate-rln-credentials --eth-account-private-key=<private-key> --eth-contract-address=<0x000...> --eth-client-address=<eth-client-rpc-or-wss-endpoint> --cred-path=./rlnKeystore.json
|
||||
```
|
||||
Once this command is executed, A keystore file will be generated at the path defined in the `--cred-path` flag. You may now use this keystore with wakunode2 or chat2.
|
||||
|
||||
|
||||
## Overview
|
||||
Figure 1 provides an overview of the interaction of the chat2 clients with the test fleets and the membership contract.
|
||||
At a high level, when a chat2 client is run with Waku-RLN-Relay mounted in on-chain mode, the passed in credential will get displayed on the console of your chat2 client.
|
||||
You may copy the displayed RLN credential and reuse them for the future execution of the chat2 application.
|
||||
Proper instructions in this regard is provided in the following [section](#how-to-persist-and-reuse-rln-credential).
|
||||
Under the hood, the chat2 client constantly listens to the membership contract and keeps itself updated with the latest state of the group.
|
||||
|
||||
In the following test setting, the chat2 clients are to be connected to the Waku test fleets as their first hop.
|
||||
@ -56,7 +56,7 @@ Run the following command to set up your chat2 client.
|
||||
--content-topic=/toy-chat/3/mingde/proto \
|
||||
--rln-relay=true \
|
||||
--rln-relay-dynamic=true \
|
||||
--rln-relay-eth-contract-address=0x9C09146844C1326c2dBC41c451766C7138F88155 \
|
||||
--rln-relay-eth-contract-address=0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4 \
|
||||
--rln-relay-cred-path=xxx/xx/rlnKeystore.json \
|
||||
--rln-relay-cred-password=xxxx \
|
||||
--rln-relay-eth-client-address=xxxx
|
||||
@ -68,8 +68,8 @@ In this command
|
||||
- the `--rln-relay` flag is set to `true` to enable the Waku-RLN-Relay protocol for spam protection.
|
||||
- the `--rln-relay-dynamic` flag is set to `true` to enable the on-chain mode of Waku-RLN-Relay protocol with dynamic group management.
|
||||
- the `--rln-relay-eth-contract-address` option gets the address of the membership contract.
|
||||
The current address of the contract is `0x9C09146844C1326c2dBC41c451766C7138F88155`.
|
||||
You may check the state of the contract on the [Sepolia testnet](https://sepolia.etherscan.io/address/0x9C09146844C1326c2dBC41c451766C7138F88155).
|
||||
The current address of the contract is `0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4`.
|
||||
You may check the state of the contract on the [Sepolia testnet](https://sepolia.etherscan.io/address/0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4).
|
||||
- the `--rln-relay-cred-path` option denotes the path to the keystore file described above
|
||||
- the `--rln-relay-cred-password` option denotes the password to the keystore
|
||||
- the `rln-relay-eth-client-address` is the WebSocket address of the hosted node on the Sepolia testnet.
|
||||
@ -176,7 +176,7 @@ You may provide an index to the membership you wish to use (within the same memb
|
||||
--content-topic=/toy-chat/3/mingde/proto \
|
||||
--rln-relay=true \
|
||||
--rln-relay-dynamic=true \
|
||||
--rln-relay-eth-contract-address=0x9C09146844C1326c2dBC41c451766C7138F88155 \
|
||||
--rln-relay-eth-contract-address=0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4 \
|
||||
--rln-relay-eth-client-address=your_sepolia_node \
|
||||
--rln-relay-cred-path=./rlnKeystore.json \
|
||||
--rln-relay-cred-password=your_password \
|
||||
@ -197,7 +197,7 @@ You can check this fact by looking at `Bob`'s console, where `message3` is missi
|
||||
|
||||
**Alice**
|
||||
```bash
|
||||
./build/chat2 --fleet=test --content-topic=/toy-chat/3/mingde/proto --rln-relay=true --rln-relay-dynamic=true --rln-relay-eth-contract-address=0x9C09146844C1326c2dBC41c451766C7138F88155 --rln-relay-cred-path=rlnKeystore.json --rln-relay-cred-password=password --rln-relay-eth-client-address=wss://sepolia.infura.io/ws/v3/12345678901234567890123456789012 --nickname=Alice
|
||||
./build/chat2 --fleet=test --content-topic=/toy-chat/3/mingde/proto --rln-relay=true --rln-relay-dynamic=true --rln-relay-eth-contract-address=0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4 --rln-relay-cred-path=rlnKeystore.json --rln-relay-cred-password=password --rln-relay-eth-client-address=wss://sepolia.infura.io/ws/v3/12345678901234567890123456789012 --nickname=Alice
|
||||
```
|
||||
|
||||
```
|
||||
@ -247,7 +247,7 @@ INFO RLN Epoch: 165886593
|
||||
|
||||
**Bob**
|
||||
```bash
|
||||
./build/chat2 --fleet=test --content-topic=/toy-chat/3/mingde/proto --rln-relay=true --rln-relay-dynamic=true --rln-relay-eth-contract-address=0x9C09146844C1326c2dBC41c451766C7138F88155 --rln-relay-cred-path=rlnKeystore.json --rln-relay-cred-index=1 --rln-relay-cred-password=password --rln-relay-eth-client-address=wss://sepolia.infura.io/ws/v3/12345678901234567890123456789012 --nickname=Bob
|
||||
./build/chat2 --fleet=test --content-topic=/toy-chat/3/mingde/proto --rln-relay=true --rln-relay-dynamic=true --rln-relay-eth-contract-address=0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4 --rln-relay-cred-path=rlnKeystore.json --rln-relay-cred-index=1 --rln-relay-cred-password=password --rln-relay-eth-client-address=wss://sepolia.infura.io/ws/v3/12345678901234567890123456789012 --nickname=Bob
|
||||
```
|
||||
|
||||
```
|
||||
|
@ -10,7 +10,7 @@ Edit `main.go` and set proper values to these constants and variables:
|
||||
```go
|
||||
const ethClientAddress = "wss://sepolia.infura.io/ws/v3/API_KEY_GOES_HERE"
|
||||
const ethPrivateKey = "PRIVATE_KEY_GOES_HERE"
|
||||
const contractAddress = "0x9C09146844C1326c2dBC41c451766C7138F88155"
|
||||
const contractAddress = "0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4"
|
||||
const credentialsPath = ""
|
||||
const credentialsPassword = ""
|
||||
|
||||
|
@ -25,13 +25,12 @@ var log = utils.Logger().Named("rln")
|
||||
|
||||
// Update these values
|
||||
// ============================================================================
|
||||
const ethClientAddress = "wss://sepolia.infura.io/ws/v3/API_KEY_GOES_HERE"
|
||||
const contractAddress = "0x9C09146844C1326c2dBC41c451766C7138F88155"
|
||||
const keystorePath = "" // Empty to store in current folder
|
||||
const keystorePassword = "" // Empty to use default
|
||||
const membershipIndex = 0
|
||||
|
||||
var contentTopic = protocol.NewContentTopic("rln", 1, "test", "proto").String()
|
||||
var ethClientAddress = "wss://sepolia.infura.io/ws/v3/API_KEY_GOES_HERE"
|
||||
var contractAddress = "0xF471d71E9b1455bBF4b85d475afb9BB0954A29c4"
|
||||
var keystorePath = "./rlnKeystore.json"
|
||||
var keystorePassword = "password"
|
||||
var membershipIndex = uint(0)
|
||||
var contentTopic, _ = protocol.NewContentTopic("rln", 1, "test", "proto")
|
||||
var pubsubTopic = protocol.DefaultPubsubTopic()
|
||||
|
||||
// ============================================================================
|
||||
@ -51,7 +50,7 @@ func main() {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
spamHandler := func(message *pb.WakuMessage) error {
|
||||
spamHandler := func(message *pb.WakuMessage, topic string) error {
|
||||
fmt.Println("Spam message received")
|
||||
return nil
|
||||
}
|
||||
@ -66,7 +65,7 @@ func main() {
|
||||
keystorePassword,
|
||||
"", // Will use default tree path
|
||||
common.HexToAddress(contractAddress),
|
||||
membershipIndex,
|
||||
&membershipIndex,
|
||||
spamHandler,
|
||||
ethClientAddress,
|
||||
),
|
||||
@ -120,7 +119,7 @@ func write(ctx context.Context, wakuNode *node.WakuNode, msgContent string) {
|
||||
msg := &pb.WakuMessage{
|
||||
Payload: payload,
|
||||
Version: version,
|
||||
ContentTopic: contentTopic,
|
||||
ContentTopic: contentTopic.String(),
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
|
||||
@ -150,7 +149,7 @@ func readLoop(ctx context.Context, wakuNode *node.WakuNode) {
|
||||
}
|
||||
|
||||
for envelope := range sub.Ch {
|
||||
if envelope.Message().ContentTopic != contentTopic {
|
||||
if envelope.Message().ContentTopic != contentTopic.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# BUILD IMAGE --------------------------------------------------------
|
||||
FROM ubuntu:18.04
|
||||
FROM ubuntu:22.04
|
||||
ARG UNAME=jenkins
|
||||
ARG UID=1001
|
||||
ARG GID=1001
|
||||
|
@ -87,6 +87,11 @@ func (k *AppKeystore) GetMembershipCredentials(keystorePassword string, index *r
|
||||
|
||||
var key Key
|
||||
var err error
|
||||
|
||||
if len(k.Credentials) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(k.Credentials) == 1 {
|
||||
// Only one credential, the tree index does not matter.
|
||||
k.logger.Warn("automatically loading the only credential found on the keystore")
|
||||
|
Loading…
x
Reference in New Issue
Block a user