Compare commits

..
467 changed files with 14536 additions and 35464 deletions
+5 -5
View File
@@ -1,16 +1,16 @@
plugins:
# golint:
# enabled: true
golint:
enabled: true
gofmt:
enabled: true
govet:
enabled: true
# golangci-lint:
# enabled: true
# golangci-lint:
#enabled: true
exclude_patterns:
- "."
- "**/*.pb.go"
- "**/rln/contracts/*.go"
- "**/bindata.go"
- "./examples/waku-csharp"
- "./examples/swift-waku"
- "./examples/swift-waku"
+1
View File
@@ -2,5 +2,6 @@ README.md
Dockerfile
.*ignore
LICENSE*
tests
examples
*.db
-1
View File
@@ -1 +0,0 @@
* @richard-ramos @chaitanyaprem
-6
View File
@@ -1,6 +0,0 @@
services:
ganache:
image: "trufflesuite/ganache:latest"
command: ["-m='swim relax risk shy chimney please usual search industry board music segment'"]
ports:
- "8545:8545"
-6
View File
@@ -1,6 +0,0 @@
services:
nwaku:
image: "harbor.status.im/wakuorg/nwaku:latest"
command: ["--relay", "--store", "--nodekey=1122334455667788990011223344556677889900112233445566778899001122", "--cluster-id=99", "--pubsub-topic=/waku/2/rs/99/1"]
ports:
- "60000"
-7
View File
@@ -1,7 +0,0 @@
services:
postgres:
environment:
- POSTGRES_HOST_AUTH_METHOD=trust
image: "postgres:9.6-alpine"
ports:
- "5432:5432"
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
- uses: actions/add-to-project@v0.5.0
with:
project-url: https://github.com/orgs/waku-org/projects/2
github-token: ${{ secrets.ADD_TO_PROJECT_20240815 }}
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
-12
View File
@@ -1,12 +0,0 @@
name: Auto Assign PR to Creator
on:
pull_request:
types:
- opened
jobs:
assign_creator:
runs-on: ubuntu-latest
steps:
- uses: toshimaru/auto-author-assign@v1.6.2
-78
View File
@@ -1,78 +0,0 @@
name: build_linux_pkgs
on:
push:
tags:
- 'v*' # "e.g. v0.4"
workflow_dispatch:
jobs:
env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: xom9ikk/dotenv@v2
with:
path: ".github/"
- run: |
echo "go_version=${{ env.GO_VERSION }}" >> $GITHUB_OUTPUT
- run: |
VERSION=$(cat ./VERSION)
echo "waku_version=$VERSION" >> $GITHUB_OUTPUT
build-linux:
needs: env
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
matrix:
ext: [deb, rpm]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get submodules hash
id: submodules
run: |
echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT
- name: Cache submodules
uses: actions/cache@v3
with:
path: |
vendor/
.git/modules
key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }}
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ needs.env.outputs.go_version }}
cache: false
- name: Build
id: build
run: |
make build
mkdir ./build/linux
cp ./build/waku ./build/linux/.
strip --strip-unneeded ./build/linux/waku
- name: Package ${{ matrix.ext }}
uses: bpicode/github-action-fpm@master
with:
fpm_args: ./build/linux/waku=/usr/bin/waku
fpm_opts: '-p gowaku-${{ needs.env.outputs.waku_version }}-x86_64.${{ matrix.ext }} -n go-waku -t ${{ matrix.ext }} -s dir --license "MIT, Apache 2.0" --version ${{ needs.env.outputs.waku_version }} --architecture x86_64 --depends libc6 --description "Go implementation of Waku v2 protocols" --url "https://github.com/waku-org/go-waku" --maintainer "Richard Ramos <richard@status.im>"'
- name: Upload ${{ matrix.ext }}
uses: actions/upload-artifact@v3
with:
name: gowaku-${{ needs.env.outputs.waku_version }}-x86_64.${{ matrix.ext }}
path: ./gowaku-${{ needs.env.outputs.waku_version }}-x86_64.${{ matrix.ext }}
if-no-files-found: error
-110
View File
@@ -1,110 +0,0 @@
name: build_mobile
on:
push:
tags:
- 'v*' # "e.g. v0.4"
workflow_dispatch:
jobs:
env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: xom9ikk/dotenv@v2
with:
path: ".github/"
- run: |
echo "go_version=${{ env.GO_VERSION }}" >> $GITHUB_OUTPUT
- run: |
VERSION=$(cat ./VERSION)
echo "waku_version=$VERSION" >> $GITHUB_OUTPUT
build-android:
needs: env
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get submodules hash
id: submodules
run: |
echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT
- name: Cache submodules
uses: actions/cache@v3
with:
path: |
vendor/
.git/modules
key: ${{ runner.os }}-vendor-modules-${{ needs.env.outputs.go_version }}
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ needs.env.outputs.go_version }}
cache: false
- name: Build
run: |
make install-gomobile
make mobile-android || make mobile-android
cd ./build/lib
tar -czvf gowaku-${{ needs.env.outputs.waku_version }}-android.tar.gz gowaku.aar gowaku-sources.jar
- name: Upload asset
uses: actions/upload-artifact@v3
with:
name: gowaku-${{ needs.env.outputs.waku_version }}-android.tar.gz
path: ./build/lib/gowaku-${{ needs.env.outputs.waku_version }}-android.tar.gz
if-no-files-found: error
build-ios:
needs: env
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get submodules hash
id: submodules
run: |
echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT
- name: Cache submodules
uses: actions/cache@v3
with:
path: |
vendor/
.git/modules
key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }}
- name: Install Go
uses: actions/setup-go@v4
with:
go-version: ${{ needs.env.outputs.go_version }}
cache: false
- name: Build
run: |
make install-gomobile
make mobile-ios
cd ./build/lib
tar -czvf gowaku-${{ needs.env.outputs.waku_version }}-ios.tar.gz Gowaku.xcframework
- name: Upload asset
uses: actions/upload-artifact@v3
with:
name: gowaku-${{ needs.env.outputs.waku_version }}-ios.tar.gz
path: ./build/lib/gowaku-${{ needs.env.outputs.waku_version }}-ios.tar.gz
if-no-files-found: error
-169
View File
@@ -1,169 +0,0 @@
name: ci
on:
pull_request:
push:
branches:
- master
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
changes: # changes detection
runs-on: ubuntu-latest
permissions:
pull-requests: read
steps:
- uses: actions/checkout@v3
name: Checkout code
id: checkout
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
common:
- '.github/workflows/**'
- 'Makefile'
- 'libs/**'
- 'go.mod'
- 'go.sum'
- 'flake.nix'
- 'examples/**'
v2:
- 'waku/**'
- 'cmd/**'
- 'library/**'
- 'tests/**'
docker:
- 'docker/**'
- 'Dockerfile'
outputs:
common: ${{ steps.filter.outputs.common }}
v2: ${{ steps.filter.outputs.v2 }}
docker: ${{ steps.filter.outputs.docker }}
env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: |
VERSION=$(cat ./VERSION)
echo "waku_version=$VERSION" >> $GITHUB_OUTPUT
golangci:
name: lint
needs: [changes, env]
if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: false
- name: Execute golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: v1.55.2
args: --deadline=5m
build:
needs: [changes, env]
if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
name: build-${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get submodules hash
id: submodules
run: |
echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT
- name: Cache submodules
uses: actions/cache@v3
with:
path: |
vendor/
.git/modules
key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }}
- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: false
- name: Build binary
run: make
- name: Build library
run: make static-library dynamic-library
- name: Build examples
run: make build-example
test:
needs: [changes, env]
if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' }}
strategy:
matrix:
tests: [test-ci, test-with-race]
runs-on: ubuntu-latest
timeout-minutes: 60
name: ${{ matrix.tests }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Get submodules hash
id: submodules
run: |
echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT
- name: Cache submodules
uses: actions/cache@v3
with:
path: |
vendor/
.git/modules
key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }}
- name: Install Go
uses: actions/setup-go@v5
with:
go-version-file: 'go.mod'
cache: false
- name: "Run tests"
run: make ${{ matrix.tests }}
- name: "Run onchain-tests"
run: |
docker compose -f .github/docker-compose/ganache.yml up -d
make test-onchain${{ matrix.tests == 'test-with-race' && '-with-race' || '' }}
- name: "Run storev3 tests"
run: |
docker compose -f .github/docker-compose/nwaku.yml up -d
NWAKU_HOST=$(docker compose -f .github/docker-compose/nwaku.yml port nwaku 60000)
NWAKU_PORT=$(echo $NWAKU_HOST | cut -d ":" -f 2)
sleep 5
make test-storev3 TEST_STOREV3_NODE="/ip4/127.0.0.1/tcp/${NWAKU_PORT}/p2p/16Uiu2HAmMGhfSTUzKbsjMWxc6T1X4wiTWSF1bEWSLjAukCm7KiHV"
-50
View File
@@ -1,50 +0,0 @@
name: container-image-build
on:
workflow_call:
inputs:
image_tag:
type: string
default: ${{ github.event.number }}
outputs:
image:
description: The resulting image link
value: ${{ jobs.build-docker-image.outputs.image }}
workflow_dispatch:
jobs:
build-docker-image:
strategy:
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 60
name: docker-build-${{ matrix.os }}
outputs:
image: ${{ steps.build.outputs.image }}
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build image
id: build
run: |
SHORT_REF=$(git rev-parse --short HEAD)
TAG=$([ "${PR_NUMBER}" == "" ] && echo "${SHORT_REF}" || echo "${PR_NUMBER}")
IMAGE=quay.io/wakuorg/go-waku-pr:${TAG}
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
docker login -u ${QUAY_USER} -p ${QUAY_PASSWORD} quay.io
docker build -t ${IMAGE} -f docker/Dockerfile.test.amd64 --label quay.expires-after=7d .
docker push ${IMAGE}
env:
QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }}
QUAY_USER: ${{ secrets.QUAY_USER }}
PR_NUMBER: ${{ inputs.image_tag }}
-43
View File
@@ -1,43 +0,0 @@
name: "Conventional Commits"
on:
pull_request:
types:
- opened
- edited
- synchronize
jobs:
main:
name: Validate format
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: richard-ramos/action-conventional-commits@v1.1.1
id: lint_pr_commits
- uses: marocchino/sticky-pull-request-comment@v2
# When the previous steps fails, the workflow would stop. By adding this
# condition you can continue the execution with the populated error message.
if: always() && (steps.lint_pr_title.outputs.error_message != null || steps.lint_pr_commits.outputs.error_message != null )
with:
header: pr-title-lint-error
message: |
Thank you for opening this pull request!
We require pull request titles and commits to follow the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your PR needs to be adjusted.
Details:
> ${{ steps.lint_pr_title.outputs.error_message }}
> ${{ steps.lint_pr_commits.outputs.error_message }}
# Delete a previous comment when the issue has been resolved
- if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.lint_pr_commits.outputs.error_message == null }}
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-title-lint-error
delete: true
+2 -9
View File
@@ -4,7 +4,6 @@ rlnKeystore.json
test_onchain.json
*.bkp
*.log
.vscode
# sqlite db
*.db
@@ -27,14 +26,10 @@ pkg/*
# output binaries
go-waku
examples/basic-relay/build/basic-relay
examples/filter2/build/filter2
examples/basic2/basic2
examples/filter2/filter2
examples/noise/build/
examples/noise/noise
examples/basic-light-client/basic2
examples/basic-relay/basic2
examples/filter2/filter2
examples/rln/rln
# Test binary, built with `go test -c`
*.test
@@ -191,5 +186,3 @@ iOSInjectionProject/
**/xcshareddata/WorkspaceSettings.xcsettings
# End of https://www.toptal.com/developers/gitignore/api/swift,xcode,Cobjective-c,osx
.idea
+3 -6
View File
@@ -1,6 +1,3 @@
[submodule "libs/waku-rln-contract"]
path = libs/waku-rln-contract
url = https://github.com/waku-org/waku-rln-contract.git
[submodule "waku/v2/protocol/waku-proto"]
path = waku/v2/protocol/waku-proto
url = https://github.com/waku-org/waku-proto
[submodule "waku/v2/protocol/rln/contracts/waku-rln-contract"]
path = waku/v2/protocol/rln/contracts/waku-rln-contract
url = git@github.com:waku-org/waku-rln-contract.git
-8
View File
@@ -1,8 +0,0 @@
issues:
include:
- EXC0012
- EXC0014
linters:
enable:
- revive
+3
View File
@@ -0,0 +1,3 @@
run:
build-tags:
- gowaku_rln
+7 -4
View File
@@ -1,5 +1,10 @@
# BUILD IMAGE --------------------------------------------------------
FROM golang:1.21 as builder
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
WORKDIR /app
COPY . .
@@ -9,7 +14,7 @@ RUN make -j$(nproc) build
# ACTUAL IMAGE -------------------------------------------------------
FROM debian:12.1-slim
FROM alpine:3.16
ARG GIT_COMMIT=unknown
@@ -21,8 +26,6 @@ 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
+38 -68
View File
@@ -6,9 +6,9 @@ CC_PREFIX := github.com/waku-org/go-waku
SHELL := bash # the shell used internally by Make
GOCMD ?= $(shell which go)
GOBIN ?= $(shell which go)
.PHONY: all build lint lint-full test coverage build-example static-library dynamic-library test-c test-c-template mobile-android mobile-ios
.PHONY: all build lint test coverage build-example static-library dynamic-library test-c test-c-template mobile-android mobile-ios
ifeq ($(OS),Windows_NT) # is Windows_NT on XP, 2000, 7, Vista, 10...
detected_OS := Windows
@@ -42,8 +42,8 @@ BUILD_FLAGS ?= $(shell echo "-ldflags='\
ANDROID_TARGET ?= 23
# control rln code compilation
ifeq ($(NO_RLN), true)
BUILD_TAGS := gowaku_no_rln
ifeq ($(RLN), true)
BUILD_TAGS := gowaku_rln
endif
all: build
@@ -51,38 +51,34 @@ all: build
deps: lint-install
build-with-race:
${GOCMD} build -race -tags="${BUILD_TAGS}" $(BUILD_FLAGS) -o build/waku ./cmd/waku
${GOBIN} build -race -tags="${BUILD_TAGS}" $(BUILD_FLAGS) -o build/waku ./cmd/waku
build:
${GOCMD} build -tags="${BUILD_TAGS}" $(BUILD_FLAGS) -o build/waku ./cmd/waku
${GOBIN} build -tags="${BUILD_TAGS}" $(BUILD_FLAGS) -o build/waku ./cmd/waku
chat2:
pushd ./examples/chat2 && \
${GOCMD} build -o ../../build/chat2 . && \
${GOBIN} build -tags="gowaku_rln" -o ../../build/chat2 . && \
popd
vendor:
${GOCMD} mod tidy
${GOBIN} mod tidy
lint-install:
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | \
bash -s -- -b $(shell ${GOCMD} env GOPATH)/bin v1.59.1
bash -s -- -b $(shell ${GOBIN} env GOPATH)/bin v1.52.2
lint:
@echo "lint"
@golangci-lint run ./...
lint-full:
@echo "lint"
@golangci-lint run ./... --config=./.golangci.full.yaml
@golangci-lint --exclude=SA1019 run ./... --deadline=5m
test-with-race:
${GOCMD} test -race -timeout 300s ./waku/... ./cmd/waku/server/...
${GOBIN} test -race -timeout 300s ./waku/...
test:
${GOCMD} test -timeout 300s ./waku/... ./cmd/waku/server/... -coverprofile=${GO_TEST_OUTFILE}.tmp -coverpkg ./...
${GOBIN} test -timeout 300s ./waku/... -coverprofile=${GO_TEST_OUTFILE}.tmp
cat ${GO_TEST_OUTFILE}.tmp | grep -v ".pb.go" > ${GO_TEST_OUTFILE}
${GOCMD} tool cover -html=${GO_TEST_OUTFILE} -o ${GO_HTML_COV}
${GOBIN} tool cover -html=${GO_TEST_OUTFILE} -o ${GO_HTML_COV}
COVERAGE_FILE := ./coverage/cc-test-reporter
$(COVERAGE_FILE):
@@ -90,33 +86,30 @@ $(COVERAGE_FILE):
chmod +x ./coverage/cc-test-reporter
_before-cc: $(COVERAGE_FILE)
CC_TEST_REPORTER_ID=${CC_TEST_REPORTER_ID} ./coverage/cc-test-reporter before-build
_after-cc:
GIT_COMMIT=$(git log | grep -m1 -oE '[^ ]+$') CC_TEST_REPORTER_ID=${CC_TEST_REPORTER_ID} ./coverage/cc-test-reporter after-build --prefix ${CC_PREFIX}
test-ci: _before-cc test _after-cc
generate:
${GOCMD} generate ./...
${GOBIN} generate ./...
coverage:
${GOCMD} test -count 1 -coverprofile=coverage.out ./...
${GOCMD} tool cover -html=coverage.out -o=coverage.html
${GOBIN} test -count 1 -coverprofile=coverage.out ./...
${GOBIN} tool cover -html=coverage.out -o=coverage.html
# build a docker image for the fleet
docker-image: DOCKER_IMAGE_TAG ?= latest
docker-image: DOCKER_IMAGE_NAME ?= wakuorg/go-waku:$(DOCKER_IMAGE_TAG)
docker-image: DOCKER_IMAGE_NAME ?= statusteam/go-waku:$(DOCKER_IMAGE_TAG)
docker-image:
docker build --tag $(DOCKER_IMAGE_NAME) \
--build-arg="GIT_COMMIT=$(shell git rev-parse HEAD)" .
build-example-basic-relay:
cd examples/basic-relay && $(MAKE)
build-example-basic-light-client:
cd examples/basic-light-client && $(MAKE)
build-example-basic2:
cd examples/basic2 && $(MAKE)
build-example-chat-2:
cd examples/chat2 && $(MAKE)
@@ -127,19 +120,13 @@ build-example-filter2:
build-example-c-bindings:
cd examples/c-bindings && $(MAKE)
build-example-noise:
cd examples/noise && $(MAKE)
build-example-rln:
cd examples/rln && $(MAKE)
build-example: build-example-basic-relay build-example-basic-light-client build-example-chat-2 build-example-filter2 build-example-c-bindings build-example-noise build-example-rln
build-example: build-example-basic2 build-example-chat-2 build-example-filter2 build-example-c-bindings
static-library:
@echo "Building static library..."
${GOCMD} build \
${GOBIN} build \
-buildmode=c-archive \
-tags="${BUILD_TAGS} gowaku_no_rln" \
-tags="${BUILD_TAGS}" \
-o ./build/lib/libgowaku.a \
./library/c/
@echo "Static library built:"
@@ -153,9 +140,9 @@ endif
dynamic-library:
@echo "Building shared library..."
rm -f ./build/lib/libgowaku.$(GOBIN_SHARED_LIB_EXT)*
$(GOBIN_SHARED_LIB_CFLAGS) $(GOBIN_SHARED_LIB_CGO_LDFLAGS) ${GOCMD} build \
$(GOBIN_SHARED_LIB_CFLAGS) $(GOBIN_SHARED_LIB_CGO_LDFLAGS) ${GOBIN} build \
-buildmode=c-shared \
-tags="${BUILD_TAGS} gowaku_no_rln" \
-tags="${BUILD_TAGS}" \
-o ./build/lib/libgowaku.$(GOBIN_SHARED_LIB_EXT) \
./library/c/
ifeq ($(detected_OS),Darwin)
@@ -174,30 +161,30 @@ endif
mobile-android:
@echo "Android target: ${ANDROID_TARGET} (override with ANDROID_TARGET var)"
gomobile init && \
${GOCMD} get -d golang.org/x/mobile/cmd/gomobile && \
CGO=1 gomobile bind -v -target=android -androidapi=${ANDROID_TARGET} -ldflags="-s -w" -tags="${BUILD_TAGS} gowaku_no_rln" $(BUILD_FLAGS) -o ./build/lib/gowaku.aar ./library/mobile
${GOBIN} get -d golang.org/x/mobile/cmd/gomobile && \
gomobile bind -v -target=android -androidapi=${ANDROID_TARGET} -ldflags="-s -w" -tags="${BUILD_TAGS}" $(BUILD_FLAGS) -o ./build/lib/gowaku.aar ./library/mobile
@echo "Android library built:"
@ls -la ./build/lib/*.aar ./build/lib/*.jar
mobile-ios:
gomobile init && \
${GOCMD} get -d golang.org/x/mobile/cmd/gomobile && \
gomobile bind -target=ios -ldflags="-s -w" -tags="nowatchdog ${BUILD_TAGS} gowaku_no_rln" $(BUILD_FLAGS) -o ./build/lib/Gowaku.xcframework ./library/mobile
${GOBIN} get -d golang.org/x/mobile/cmd/gomobile && \
gomobile bind -target=ios -ldflags="-s -w" -tags="nowatchdog ${BUILD_TAGS}" $(BUILD_FLAGS) -o ./build/lib/Gowaku.xcframework ./library/mobile
@echo "IOS library built:"
@ls -la ./build/lib/*.xcframework
install-xtools:
${GOCMD} install golang.org/x/tools/...@v0.1.10
${GOBIN} install golang.org/x/tools/...@v0.1.10
install-bindata:
${GOCMD} install github.com/kevinburke/go-bindata/go-bindata@v3.13.0
${GOBIN} install github.com/kevinburke/go-bindata/go-bindata@v3.13.0
install-gomobile: install-xtools
${GOCMD} install golang.org/x/mobile/cmd/gomobile@v0.0.0-20220518205345-8578da9835fd
${GOCMD} install golang.org/x/mobile/cmd/gobind@v0.0.0-20220518205345-8578da9835fd
${GOBIN} install golang.org/x/mobile/cmd/gomobile@v0.0.0-20220518205345-8578da9835fd
${GOBIN} install golang.org/x/mobile/cmd/gobind@v0.0.0-20220518205345-8578da9835fd
build-linux-pkg:
docker build --build-arg UID=${UID} --build-arg GID=${GID} -f ./scripts/linux/Dockerfile -t wakuorg/gowaku-linux-pkgs:latest .
docker build --build-arg UID=${UID} --build-arg GID=${GID} -f ./scripts/linux/Dockerfile -t statusteam/gowaku-linux-pkgs:latest .
./scripts/linux/docker-run.sh
ls -la ./build/*.rpm ./build/*.deb
@@ -211,24 +198,7 @@ stop-ganache:
test-onchain: BUILD_TAGS += include_onchain_tests
test-onchain:
${GOCMD} test -v -count 1 -tags="${BUILD_TAGS}" github.com/waku-org/go-waku/waku/v2/protocol/rln
${GOBIN} test -v -count 1 -tags="${BUILD_TAGS}" github.com/waku-org/go-waku/waku/v2/protocol/rln
test-onchain-with-race:
${GOCMD} test -race -v -count 1 -tags="${BUILD_TAGS}" github.com/waku-org/go-waku/waku/v2/protocol/rln
test-postgres: PG_BUILD_TAGS = ${BUILD_TAGS} include_postgres_tests
test-postgres:
${GOCMD} test -p 1 -v -count 1 -tags="${PG_BUILD_TAGS}" github.com/waku-org/go-waku/waku/persistence/...
test-postgres-with-race:
${GOCMD} test -race -p 1 -v -count 1 -tags="${PG_BUILD_TAGS}" github.com/waku-org/go-waku/waku/persistence/...
test-filter:
${GOCMD} test -v github.com/waku-org/go-waku/waku/v2/protocol/filter -run TestFilterSuite -count=1
test-filter-api:
${GOCMD} test -v github.com/waku-org/go-waku/waku/v2/api -run TestFilterApiSuite
TEST_STOREV3_NODE ?=
test-storev3:
TEST_STOREV3_NODE=${TEST_STOREV3_NODE} ${GOCMD} test -p 1 -v -count 1 -tags="${BUILD_TAGS} include_storev3_tests" github.com/waku-org/go-waku/waku/v2/protocol/store/...
${GOBIN} test -race -v -count 1 -tags="${BUILD_TAGS}" github.com/waku-org/go-waku/waku/v2/protocol/rln
+8 -9
View File
@@ -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.20.0-orange.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="https://codeclimate.com/github/waku-org/go-waku/maintainability"><img src="https://api.codeclimate.com/v1/badges/426bdff6a339ff4d536b/maintainability" /></a>
<br>
</p>
@@ -38,19 +38,18 @@ nix develop
#### Docker
```
docker run -i -t -p 60000:60000 -p 9000:9000/udp \
wakuorg/go-waku:latest \
--dns-discovery \
--dns-discovery-url enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im \
statusteam/go-waku:v0.5.2 \ # or, the image:tag of your choice
--dns-discovery:true \
--dns-discovery-url:enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@prod.waku.nodes.status.im \
--discv5-discovery
```
or use the [image:tag](https://hub.docker.com/r/wakuorg/go-waku/tags) of your choice.
or build and run the image with:
```
docker build -t wakuorg/go-waku:latest .
docker build -t go-waku:latest .
docker run wakuorg/go-waku:latest --help
docker run go-waku:latest --help
```
#### Building on windows
@@ -85,7 +84,7 @@ make mobile-ios
- [Send messages using Waku Lightpush](docs/api/lightpush.md)
- [Encrypting and decrypting Waku Messages](docs/api/encoding.md)
- [Retrieve message history using Waku Store](docs/api/store.md)
- [C Bindings](library/c/README.md)
- [C Bindings](library/README.md)
- [Waku Specs](https://rfc.vac.dev/spec), has information of [waku topics](https://rfc.vac.dev/spec/23/), wakuv1/[wakuv2](https://rfc.vac.dev/spec/14/) message, [rln relay](https://rfc.vac.dev/spec/58/) etc.
- [Enr](https://eips.ethereum.org/EIPS/eip-778), [Enrtree](https://eips.ethereum.org/EIPS/eip-1459)
- [devp2p](https://github.com/ethereum/go-ethereum/tree/master/cmd/devp2p) tool for playing with enr/entree sync tree. [Tutorial](https://geth.ethereum.org/docs/developers/geth-developer/dns-discovery-setup)
@@ -107,7 +106,7 @@ Thank you for considering to help out with the source code! We welcome contribut
If you'd like to contribute to go-waku, please fork, fix, commit and send a pull request. If you wish to submit more complex changes though, please check up with the core devs first to ensure those changes are in line with the general philosophy of the project and/or get some early feedback which can make both your efforts much lighter as well as our review and merge procedures quick and simple.
To build and test this repository, you need:
- [Go](https://golang.org/) (version 1.20)
- [Go](https://golang.org/) (version 1.19 or 1.20)
- [protoc](https://grpc.io/docs/protoc-installation/)
- [protoc-gen-go](https://protobuf.dev/getting-started/gotutorial/#compiling-protocol-buffers)
+1 -1
View File
@@ -1 +1 @@
0.9.0
0.7.0
+99
View File
@@ -0,0 +1,99 @@
library 'status-jenkins-lib@v1.7.0'
pipeline {
agent { label 'linux' }
options {
timestamps()
disableConcurrentBuilds()
/* Prevent Jenkins jobs from running forever */
timeout(time: 40, unit: 'MINUTES')
/* Limit builds retained */
buildDiscarder(logRotator(
numToKeepStr: '10',
daysToKeepStr: '30',
artifactNumToKeepStr: '10',
))
}
/* WARNING: Defining parameters here with the ?: trick causes them to remember last value. */
parameters {
booleanParam(
name: 'PUBLISH',
description: 'Trigger publishing of build results for nightly or release.',
defaultValue: getPublishDefault(params.PUBLISH),
)
}
stages {
stage('Build') {
parallel {
stage('iOS') { steps { script {
ios = jenkins.Build('go-waku/platforms/ios')
} } }
stage('Android') { steps { script {
android = jenkins.Build('go-waku/platforms/android')
} } }
stage('Linux') { steps { script {
linux = jenkins.Build('go-waku/platforms/linux')
} } }
}
}
stage('Archive') {
steps { script {
sh('rm -f pkg/*')
jenkins.copyArts(ios)
jenkins.copyArts(android)
jenkins.copyArts(linux)
sha = "pkg/${utils.pkgFilename(ext: 'sha256')}"
dir('pkg') {
/* generate sha256 checksums for upload */
sh "sha256sum * | tee ../${sha}"
archiveArtifacts('*')
}
} }
}
stage('Upload') {
steps { script {
/* object for easier URLs handling */
urls = [
/* mobile */
Android: utils.pkgUrl(android),
iOS: utils.pkgUrl(ios),
Linux: utils.pkgUrl(linux),
/* upload the sha256 checksums file too */
SHA: s3.uploadArtifact(sha),
]
/* add URLs to the build description */
jenkins.setBuildDesc(urls)
} }
}
stage('Publish') {
when { expression { params.PUBLISH } }
steps { script {
github.publishReleaseFiles(repo: 'status-desktop');
} }
}
}
}
/* Helper that generates list of available choices for a parameter
* but re-orders them based on the currently set value. First is default. */
def List genChoices(String previousChoice, List defaultChoices) {
if (previousChoice == null) {
return defaultChoices
}
choices = defaultChoices.minus(previousChoice)
choices.add(0, previousChoice)
return choices
}
/* Helper that makes PUBLISH default to 'false' unless:
* - The build is for a release branch
* - A user explicitly specified a value
* Since release builds create and re-create GitHub drafts every time. */
def Boolean getPublishDefault(Boolean previousValue) {
if (env.JOB_NAME.startsWith('go-waku/release')) { return true }
if (previousValue != null) { return previousValue }
return false
}
+82
View File
@@ -0,0 +1,82 @@
library 'status-jenkins-lib@v1.7.0'
pipeline {
agent { label 'linux && nix-2.11 && x86_64' }
options {
timestamps()
disableConcurrentBuilds()
/* Prevent Jenkins jobs from running forever */
timeout(time: 30, unit: 'MINUTES')
/* Go requires a certain directory structure */
checkoutToSubdirectory('src/github.com/waku-org/go-waku')
/* Limit builds retained */
buildDiscarder(logRotator(
numToKeepStr: '10',
daysToKeepStr: '20',
artifactNumToKeepStr: '10',
))
/* Allows combined build to copy */
copyArtifactPermission('/go-waku/*')
}
environment {
CC = "gcc-10"
/* Other stuff */
TARGET = 'android'
REPO = "${env.WORKSPACE}/src/github.com/waku-org/go-waku"
GOCACHE = "${env.WORKSPACE_TMP}/go-build"
GOPATH = "${env.WORKSPACE}"
PATH = "${env.PATH}:${env.GOPATH}/bin"
/* Android SDK */
ANDROID_HOME = '/usr/lib/android-sdk'
ANDROID_SDK_ROOT = '/usr/lib/android-sdk'
/* gomobile requires a specific NDK version */
ANDROID_NDK = "/opt/android-ndk-r23c"
ANDROID_NDK_HOME = "/opt/android-ndk-r23c"
}
stages {
stage('Prep') { steps { script { dir(env.REPO) {
env.ARTIFACT = "${env.REPO}/pkg/" + utils.pkgFilename(
name: "go-waku",
type: "android",
ext: "tar.gz"
)
nix.develop('make install-gomobile', pure: false)
} } } }
stage('Build') { steps { script { dir(env.REPO) {
/* First gomobile run always fails.
* https://github.com/golang/go/issues/37372 */
nix.develop('make mobile-android || make mobile-android', pure: false)
dir('build/lib') {
sh 'tar -czvf gowaku-android.tar.gz gowaku.aar gowaku-sources.jar'
sh "cp gowaku-android.tar.gz ${env.ARTIFACT}"
}
} } } }
stage('Parallel Upload') {
parallel {
stage('Archive') {
steps { script {
archiveArtifacts(env.ARTIFACT.minus("${env.WORKSPACE}/"))
} }
}
stage('Upload') {
steps { script {
env.PKG_URL = s3.uploadArtifact(env.ARTIFACT)
jenkins.setBuildDesc(android: env.PKG_URL)
} }
}
}
}
}
post {
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
always { cleanWs() }
}
}
+60 -26
View File
@@ -11,25 +11,14 @@ pipeline {
)
string(
name: 'IMAGE_NAME',
defaultValue: 'statusteam/go-waku',
description: 'Docker image name.',
defaultValue: params.IMAGE_NAME ?: 'waku-org/go-waku',
)
string(
name: 'IMAGE_TAG',
defaultValue: env.JOB_BASE_NAME == 'release' ? 'stable' : 'deploy-test',
description: 'Docker image tag.',
defaultValue: getDefaultImageTag(params.IMAGE_TAG)
)
string(
name: 'DOCKER_CRED',
description: 'Name of Docker Registry credential.',
defaultValue: params.DOCKER_CRED ?: 'harbor-wakuorg-robot',
)
string(
name: 'DOCKER_REGISTRY_URL',
description: 'URL of the Docker Registry',
defaultValue: params.DOCKER_REGISTRY_URL ?: 'https://harbor.status.im'
)
}
options {
@@ -43,38 +32,83 @@ pipeline {
stages {
stage('Build') {
steps { script {
def imageTag = GIT_COMMIT.take(8)
if (env.JOB_BASE_NAME == 'release') {
imageTag = params.GIT_REF
}
image = docker.build(
"${params.IMAGE_NAME}:${params.IMAGE_TAG ?: GIT_COMMIT.take(8)}",
"${params.IMAGE_NAME}:${imageTag}",
"--build-arg='GIT_COMMIT=${GIT_COMMIT.take(8)}' ."
)
} }
}
stage('Push') {
when { expression { params.IMAGE_TAG != '' } }
steps { script {
withDockerRegistry([
credentialsId: params.DOCKER_CRED, url: params.DOCKER_REGISTRY_URL
credentialsId: "dockerhub-statusteam-auto", url: ""
]) {
image.push()
/* If Git ref is a tag push it as Docker tag too. */
if (params.GIT_REF ==~ /v\d+\.\d+\.\d+.*/) {
image.push(params.GIT_REF)
}
}
} }
}
stage('Deploy') {
steps { script {
withDockerRegistry([
credentialsId: "dockerhub-statusteam-auto", url: ""
]) {
image.push(env.IMAGE_TAG)
}
} }
}
}
post {
success { script {
discordNotify(
header: 'Go-Waku build successful!',
cred: 'discord-waku-deployments-webhook',
)
} }
always { cleanWs() }
}
}
def getDefaultImageTag(currentValue) {
switch (env.JOB_BASE_NAME) {
case 'docker-latest': return 'latest'
case 'docker-release': return 'stable'
case 'docker-manual': return ''
default: return currentValue
def discordNotify(Map args=[:]) {
def opts = [
header: args.header ?: 'Deployment successful!',
cred: args.cred ?: null,
]
def repo = [
url: GIT_URL.minus('.git'),
branch: GIT_BRANCH.minus('origin/'),
commit: GIT_COMMIT.take(8),
prev: (
env.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: env.GIT_PREVIOUS_COMMIT ?: 'master'
).take(8),
]
wrap([$class: 'BuildUser']) {
BUILD_USER_ID = env.BUILD_USER_ID
}
withCredentials([
string(
credentialsId: opts.cred,
variable: 'DISCORD_WEBHOOK',
),
]) {
discordSend(
link: env.BUILD_URL,
result: currentBuild.currentResult,
webhookURL: env.DISCORD_WEBHOOK,
title: "${env.JOB_NAME}#${env.BUILD_NUMBER}",
description: """
${opts.header}
Image: [`${IMAGE_NAME}:${IMAGE_TAG}`](https://hub.docker.com/r/${IMAGE_NAME}/tags?name=${IMAGE_TAG})
Branch: [`${repo.branch}`](${repo.url}/commits/${repo.branch})
Commit: [`${repo.commit}`](${repo.url}/commit/${repo.commit})
Diff: [`${repo.prev}...${repo.commit}`](${repo.url}/compare/${repo.prev}...${repo.commit})
By: [`${BUILD_USER_ID}`](${repo.url}/commits?author=${BUILD_USER_ID})
""",
)
}
}
+78
View File
@@ -0,0 +1,78 @@
library 'status-jenkins-lib@v1.7.0'
pipeline {
agent { label 'macos && nix-2.11 && aarch64' }
options {
timestamps()
/* Prevent Jenkins jobs from running forever */
timeout(time: 30, unit: 'MINUTES')
/* Go requires a certain directory structure */
checkoutToSubdirectory('src/github.com/waku-org/go-waku')
/* Limit builds retained */
buildDiscarder(logRotator(
numToKeepStr: '10',
daysToKeepStr: '20',
artifactNumToKeepStr: '10',
))
/* Allows combined build to copy */
copyArtifactPermission('/go-waku/*')
}
environment {
TARGET = 'ios'
REPO = "${env.WORKSPACE}/src/github.com/waku-org/go-waku"
GOCACHE = "${env.WORKSPACE_TMP}/go-build"
GOPATH = "${env.WORKSPACE}"
PATH = "${env.PATH}:${env.GOPATH}/bin"
}
stages {
stage('Prep') { steps { script { dir(env.REPO) {
env.ARTIFACT = "${env.REPO}/pkg/" + utils.pkgFilename(
name: "go-waku",
type: "ios",
ext: "tar.gz"
)
sh 'make install-gomobile'
} } } }
stage('Build') {
steps { script { dir(env.REPO) {
nix.develop('which xcodebuild', pure: false)
nix.develop('make mobile-ios', pure: false)
} } }
}
stage('Package') {
steps { dir(env.REPO) {
dir('build/lib') {
sh 'tar -czvf gowaku-ios.tar.gz Gowaku.xcframework'
sh "cp gowaku-ios.tar.gz ${env.ARTIFACT}"
}
} }
}
stage('Parallel Upload') {
parallel {
stage('Archive') {
steps { script {
archiveArtifacts(env.ARTIFACT.minus("${env.WORKSPACE}/"))
} }
}
stage('Upload') {
steps { script {
env.PKG_URL = s3.uploadArtifact(env.ARTIFACT)
jenkins.setBuildDesc(ios: env.PKG_URL)
} }
}
}
}
}
post {
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
always { cleanWs() }
}
}
+89
View File
@@ -0,0 +1,89 @@
library 'status-jenkins-lib@v1.7.0'
pipeline {
agent {
label 'linux && nix-2.11 && x86_64'
}
options {
timestamps()
disableConcurrentBuilds()
/* Prevent Jenkins jobs from running forever */
timeout(time: 30, unit: 'MINUTES')
/* Go requires a certain directory structure */
checkoutToSubdirectory('src/github.com/waku-org/go-waku')
/* Limit builds retained */
buildDiscarder(logRotator(
numToKeepStr: '10',
daysToKeepStr: '20',
artifactNumToKeepStr: '10',
))
/* Allows combined build to copy */
copyArtifactPermission('/go-waku/*')
}
environment {
TARGET = 'linux'
REPO = "${env.WORKSPACE}/src/github.com/waku-org/go-waku"
GOCACHE = "${env.WORKSPACE_TMP}/go-build"
GOPATH = "${env.WORKSPACE}"
PATH = "${env.PATH}:${env.GOPATH}/bin"
}
stages {
stage('Prep') {
steps { script { dir(env.REPO) {
env.DEB_ARTIFACT = "${env.REPO}/pkg/" + utils.pkgFilename(
name: "go-waku",
type: "x86_64",
ext: "deb"
)
} } }
}
stage('Build') {
steps { script { dir(env.REPO) {
nix.develop('make build')
} } }
}
stage('Build examples') {
steps { script { dir(env.REPO) {
nix.develop('make build-example')
} } }
}
stage('Package') {
steps { script { dir(env.REPO) {
dir('./scripts/linux') {
nix.develop('./fpm-build.sh', attr: 'fpm')
}
dir('build') {
sh "cp gowaku*.deb ${env.DEB_ARTIFACT}"
}
} } }
}
stage('Parallel Upload') {
parallel {
stage('Archive') {
steps { script {
archiveArtifacts(env.DEB_ARTIFACT.minus("${env.WORKSPACE}/"))
} }
}
stage('Upload') {
steps { script {
env.PKG_URL = s3.uploadArtifact(env.DEB_ARTIFACT)
jenkins.setBuildDesc(x86_64_deb: env.PKG_URL)
} }
}
}
}
}
post {
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
always { cleanWs() }
}
}
+11 -5
View File
@@ -1,8 +1,8 @@
library 'status-jenkins-lib@v1.9.3'
library 'status-jenkins-lib@v1.7.0'
pipeline {
agent {
label 'linux && nix-2.19 && x86_64'
label 'linux && nix-2.11 && x86_64'
}
options {
@@ -27,7 +27,10 @@ pipeline {
stages {
stage('Build') {
steps { script {
nix.flake('node')
sh("""#!/usr/bin/env bash
${nix._sourceProfileInline()}
nix build --print-out-paths .#node
""")
} }
}
stage('Check') {
@@ -42,12 +45,15 @@ pipeline {
stages {
stage('Build') {
steps { script {
nix.flake('static-library')
sh("""#!/usr/bin/env bash
${nix._sourceProfileInline()}
nix build --print-out-paths .#library
""")
} }
}
stage('Check') {
steps {
sh 'readelf -h ./result/bin/libgowaku.a'
sh 'ldd ./result/bin/c'
}
}
}
+102
View File
@@ -0,0 +1,102 @@
library 'status-jenkins-lib@v1.7.0'
pipeline {
agent {
label 'linux && nix-2.11 && x86_64'
}
options {
timestamps()
disableConcurrentBuilds()
/* Prevent Jenkins jobs from running forever */
timeout(time: 30, unit: 'MINUTES')
/* Go requires a certain directory structure */
checkoutToSubdirectory('src/github.com/waku-org/go-waku')
buildDiscarder(logRotator(
numToKeepStr: '10',
daysToKeepStr: '30',
))
}
/* WARNING: Defining parameters here with the ?: trick causes them to remember last value. */
parameters {
booleanParam(
name: 'RACE',
description: 'Run tests with check for race condition.',
defaultValue: getRaceDefault()
)
}
environment {
TARGET = 'tests'
REPO = "${env.WORKSPACE}/src/github.com/waku-org/go-waku"
GOCACHE = "${env.WORKSPACE_TMP}/go-build"
GOPATH = "${env.WORKSPACE}/go"
PATH = "${env.PATH}:${env.GOPATH}/bin"
/* Necesary to avoid cache poisoning by other builds. */
GOLANGCI_LINT_CACHE = "${env.WORKSPACE_TMP}/golangci-lint"
/* Ganache config */
GANACHE_RPC_PORT = "${8989 + env.EXECUTOR_NUMBER.toInteger()}"
GANACHE_MNEMONIC = 'swim relax risk shy chimney please usual search industry board music segment'
}
stages {
stage('Lint') {
steps { script { dir(env.REPO) {
nix.develop('make lint', pure: false)
} } }
}
stage('Test') {
steps { script { dir(env.REPO) {
if (params.RACE) {
nix.develop('make test-with-race', pure: false)
}else {
nix.develop('make test-ci', pure: false)
}
} } }
}
stage('Ganache') {
steps { script {
ganache = docker.image(
'trufflesuite/ganache:v7.4.1'
).run(
"-p 127.0.0.1:${env.GANACHE_RPC_PORT}:8545",
"-m='${GANACHE_MNEMONIC}'"
)
} }
}
stage('On-chain tests') {
environment {
GANACHE_NETWORK_RPC_URL = "ws://localhost:${env.GANACHE_RPC_PORT}"
}
steps { script { dir(env.REPO) {
if (params.RACE) {
nix.develop('make test-onchain-with-race', pure: false)
}else {
nix.develop('make test-onchain', pure: false)
}
} } }
}
}
post {
always { script { /* No artifact but a PKG_URL is necessary. */
env.PKG_URL = "${currentBuild.absoluteUrl}consoleText"
} }
success { script { github.notifyPR(true) } }
failure { script { github.notifyPR(false) } }
cleanup { script {
cleanWs()
catchError {
ganache.stop()
}
} }
}
}
def Boolean getRaceDefault() {
return env.JOB_NAME.split('/').contains('race')
}
+63 -46
View File
@@ -6,7 +6,6 @@ import (
cli "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"github.com/waku-org/go-waku/waku/cliutils"
"github.com/waku-org/go-waku/waku/v2/node"
)
var (
@@ -33,12 +32,6 @@ var (
Destination: &options.MaxPeerConnections,
EnvVars: []string{"WAKUNODE2_MAX_CONNECTIONS"},
})
PeerStoreCapacity = altsrc.NewIntFlag(&cli.IntFlag{
Name: "peer-store-capacity",
Usage: "Maximum stored peers in the peerstore.",
Destination: &options.PeerStoreCapacity,
EnvVars: []string{"WAKUNODE2_PEERSTORE_CAPACITY"},
})
WebsocketSupport = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "websocket-support",
Aliases: []string{"ws"},
@@ -122,13 +115,6 @@ var (
Destination: &options.KeyPasswd,
EnvVars: []string{"WAKUNODE2_KEY_PASSWORD"},
})
ClusterID = altsrc.NewUintFlag(&cli.UintFlag{
Name: "cluster-id",
Value: 0,
Usage: "Cluster id that the node is running in. Node in a different cluster id is disconnected.",
Destination: &options.ClusterID,
EnvVars: []string{"WAKUNODE2_CLUSTER_ID"},
})
StaticNode = cliutils.NewGenericFlagMultiValue(&cli.GenericFlag{
Name: "staticnode",
Usage: "Multiaddr of peer to directly connect with. Option may be repeated",
@@ -185,14 +171,6 @@ var (
Destination: &options.CircuitRelay,
EnvVars: []string{"WAKUNODE2_CIRCUIT_RELAY"},
})
ForceReachability = altsrc.NewStringFlag(&cli.StringFlag{
Name: "force-reachability",
Usage: "Force the node reachability. WARNING: This flag is created for testing circuit relay and is not meant to be used in production. Use 'public' or 'private'",
Value: "",
Hidden: true,
Destination: &options.ForceReachability,
EnvVars: []string{"WAKUNODE2_REACHABILITY"},
})
ResourceScalingMemoryPercent = altsrc.NewFloat64Flag(&cli.Float64Flag{
Name: "resource-scaling-memory-percentage",
Usage: "Determines the percentage of total accessible memory that wil be dedicated to go-waku. A dedicated node with a lot of RAM could allocate 25% or more memory to go-waku",
@@ -235,18 +213,11 @@ var (
})
AgentString = altsrc.NewStringFlag(&cli.StringFlag{
Name: "agent-string",
Value: node.UserAgent,
Value: "go-waku",
Usage: "client id to advertise",
Destination: &options.UserAgent,
EnvVars: []string{"WAKUNODE2_AGENT_STRING"},
})
IPColocationLimit = altsrc.NewIntFlag(&cli.IntFlag{
Name: "ip-colocation-limit",
Value: node.DefaultMaxConnectionsPerIP,
Usage: "max number of allowed peers from the same IP. Set it to 0 to remove the limitation.",
Destination: &options.IPColocationLimit,
EnvVars: []string{"WAKUNODE2_IP_COLOCATION_LIMIT"},
})
Relay = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "relay",
Value: true,
@@ -294,13 +265,6 @@ var (
Destination: &options.Relay.MinRelayPeersToPublish,
EnvVars: []string{"WAKUNODE2_MIN_RELAY_PEERS_TO_PUBLISH"},
})
MaxRelayMsgSize = altsrc.NewStringFlag(&cli.StringFlag{
Name: "max-msg-size",
Value: "150KB",
Usage: "Maximum message size. Supported formats are B, KiB, KB, MiB. If no suffix, default is bytes",
Destination: &options.Relay.MaxMsgSize,
EnvVars: []string{"WAKUNODE2_MAX_RELAY_MSG_SIZE"},
})
StoreNodeFlag = cliutils.NewGenericFlagMultiValue(&cli.GenericFlag{
Name: "storenode",
Usage: "Multiaddr of a peer that supports store protocol. Option may be repeated",
@@ -336,6 +300,12 @@ var (
Destination: &options.Store.DatabaseURL,
EnvVars: []string{"WAKUNODE2_STORE_MESSAGE_DB_URL"},
})
StoreMessageDBVacuum = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "store-message-db-vacuum",
Usage: "Enable database vacuuming at start.",
Destination: &options.Store.Vacuum,
EnvVars: []string{"WAKUNODE2_STORE_MESSAGE_DB_VACUUM"},
})
StoreMessageDBMigration = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "store-message-db-migration",
Usage: "Enable database migration at start.",
@@ -364,6 +334,26 @@ var (
Destination: &options.Filter.Timeout,
EnvVars: []string{"WAKUNODE2_FILTER_TIMEOUT"},
})
FilterLegacyFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "legacy-filter",
Usage: "Use filter protocol (legacy)",
Destination: &options.Filter.UseV1,
EnvVars: []string{"WAKUNODE2_USE_LEGACY_FILTER"},
})
FilterLegacyLightClient = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "legacy-filter-light-client",
Usage: "Don't accept legacy filter subscribers",
Destination: &options.Filter.DisableFullNode,
EnvVars: []string{"WAKUNODE2_LEGACY_FILTER_LIGHT_CLIENT"},
})
FilterLegacyNode = cliutils.NewGenericFlagMultiValue(&cli.GenericFlag{
Name: "legacy-filternode",
Usage: "Multiaddr of a peer that supports legacy filter protocol. Option may be repeated",
Value: &cliutils.MultiaddrSlice{
Values: &options.Filter.NodesV1,
},
EnvVars: []string{"WAKUNODE2_LEGACY_FILTERNODE"},
})
LightPush = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "lightpush",
Usage: "Enable lightpush protocol",
@@ -445,7 +435,7 @@ var (
})
DNSDiscoveryUrl = altsrc.NewStringSliceFlag(&cli.StringSliceFlag{
Name: "dns-discovery-url",
Usage: "URL for DNS node list in format 'enrtree://<key>@<fqdn>'. Option may be repeated",
Usage: "URL for DNS node list in format 'enrtree://<key>@<fqdn>'",
Destination: &options.DNSDiscovery.URLs,
EnvVars: []string{"WAKUNODE2_DNS_DISCOVERY_URL"},
})
@@ -479,6 +469,40 @@ var (
Destination: &options.Metrics.Port,
EnvVars: []string{"WAKUNODE2_METRICS_SERVER_PORT"},
})
RPCFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "rpc",
Usage: "Enable the rpc server",
Destination: &options.RPCServer.Enable,
EnvVars: []string{"WAKUNODE2_RPC"},
})
RPCPort = altsrc.NewIntFlag(&cli.IntFlag{
Name: "rpc-port",
Value: 8545,
Usage: "Listening port of the rpc server",
Destination: &options.RPCServer.Port,
EnvVars: []string{"WAKUNODE2_RPC_PORT"},
})
RPCAddress = altsrc.NewStringFlag(&cli.StringFlag{
Name: "rpc-address",
Value: "127.0.0.1",
Usage: "Listening address of the rpc server",
Destination: &options.RPCServer.Address,
EnvVars: []string{"WAKUNODE2_RPC_ADDRESS"},
})
RPCRelayCacheCapacity = altsrc.NewIntFlag(&cli.IntFlag{
Name: "rpc-relay-cache-capacity",
Value: 30,
Usage: "Capacity of the Relay REST API message cache",
Destination: &options.RPCServer.RelayCacheCapacity,
EnvVars: []string{"WAKUNODE2_RPC_RELAY_CACHE_CAPACITY"},
})
RPCAdmin = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "rpc-admin",
Value: false,
Usage: "Enable access to JSON-RPC Admin API",
Destination: &options.RPCServer.Admin,
EnvVars: []string{"WAKUNODE2_RPC_ADMIN"},
})
RESTFlag = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "rest",
Usage: "Enable Waku REST HTTP server",
@@ -501,18 +525,11 @@ var (
})
RESTRelayCacheCapacity = altsrc.NewIntFlag(&cli.IntFlag{
Name: "rest-relay-cache-capacity",
Value: 1000,
Value: 30,
Usage: "Capacity of the Relay REST API message cache",
Destination: &options.RESTServer.RelayCacheCapacity,
EnvVars: []string{"WAKUNODE2_REST_RELAY_CACHE_CAPACITY"},
})
RESTFilterCacheCapacity = altsrc.NewIntFlag(&cli.IntFlag{
Name: "rest-filter-cache-capacity",
Value: 30,
Usage: "Capacity of the Filter REST API message cache",
Destination: &options.RESTServer.FilterCacheCapacity,
EnvVars: []string{"WAKUNODE2_REST_FILTER_CACHE_CAPACITY"},
})
RESTAdmin = altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "rest-admin",
Value: false,
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build gowaku_no_rln
// +build gowaku_no_rln
//go:build !gowaku_rln
// +build !gowaku_rln
package main
+10 -10
View File
@@ -1,11 +1,12 @@
//go:build !gowaku_no_rln
// +build !gowaku_no_rln
//go:build gowaku_rln
// +build gowaku_rln
package main
import (
cli "github.com/urfave/cli/v2"
wcli "github.com/waku-org/go-waku/waku/cliutils"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore"
)
func rlnFlags() []cli.Flag {
@@ -16,12 +17,11 @@ func rlnFlags() []cli.Flag {
Usage: "Enable spam protection through rln-relay",
Destination: &options.RLNRelay.Enable,
},
&cli.GenericFlag{
Name: "rln-relay-cred-index",
Usage: "the index of the onchain commitment to use",
Value: &wcli.OptionalUint{
Value: &options.RLNRelay.MembershipIndex,
},
&cli.UintFlag{
Name: "rln-relay-membership-index",
Value: 0,
Usage: "the index of credentials to use",
Destination: &options.RLNRelay.MembershipIndex,
},
&cli.BoolFlag{
Name: "rln-relay-dynamic",
@@ -31,12 +31,12 @@ func rlnFlags() []cli.Flag {
&cli.PathFlag{
Name: "rln-relay-cred-path",
Usage: "RLN relay membership credentials file",
Value: "",
Value: keystore.DefaultCredentialsFilename,
Destination: &options.RLNRelay.CredentialsPath,
},
&cli.StringFlag{
Name: "rln-relay-cred-password",
Value: "",
Value: keystore.DefaultCredentialsPassword,
Usage: "Password for encrypting RLN credentials",
Destination: &options.RLNRelay.CredentialsPassword,
},
+10 -18
View File
@@ -8,8 +8,6 @@ import (
"github.com/waku-org/go-waku/cmd/waku/keygen"
"github.com/waku-org/go-waku/cmd/waku/rlngenerate"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/utils"
"go.uber.org/zap"
)
var options NodeOptions
@@ -24,7 +22,6 @@ func main() {
TcpPort,
Address,
MaxPeerConnections,
PeerStoreCapacity,
WebsocketSupport,
WebsocketPort,
WebsocketSecurePort,
@@ -36,7 +33,6 @@ func main() {
NodeKey,
KeyFile,
KeyPassword,
ClusterID,
StaticNode,
KeepAlive,
PersistPeers,
@@ -45,10 +41,8 @@ func main() {
ExtMultiaddresses,
ShowAddresses,
CircuitRelay,
ForceReachability,
ResourceScalingMemoryPercent,
ResourceScalingFDPercent,
IPColocationLimit,
LogLevel,
LogEncoding,
LogOutput,
@@ -60,16 +54,19 @@ func main() {
ProtectedTopics,
RelayPeerExchange,
MinRelayPeersToPublish,
MaxRelayMsgSize,
StoreNodeFlag,
StoreFlag,
StoreMessageDBURL,
StoreMessageRetentionTime,
StoreMessageRetentionCapacity,
StoreMessageDBVacuum,
StoreMessageDBMigration,
FilterFlag,
FilterNode,
FilterTimeout,
FilterLegacyFlag,
FilterLegacyNode,
FilterLegacyLightClient,
LightPush,
LightPushNode,
Discv5Discovery,
@@ -87,11 +84,15 @@ func main() {
MetricsServer,
MetricsServerAddress,
MetricsServerPort,
RPCFlag,
RPCPort,
RPCAddress,
RPCRelayCacheCapacity,
RPCAdmin,
RESTFlag,
RESTAddress,
RESTPort,
RESTRelayCacheCapacity,
RESTFilterCacheCapacity,
RESTAdmin,
PProf,
}
@@ -110,16 +111,7 @@ func main() {
Before: altsrc.InitInputSourceWithContext(cliFlags, altsrc.NewTomlSourceFromFlagFunc("config-file")),
Flags: cliFlags,
Action: func(c *cli.Context) error {
err := Execute(options)
if err != nil {
utils.Logger().Error("failure while executing wakunode", zap.Error(err))
switch e := err.(type) {
case cli.ExitCoder:
return e
case error:
return cli.Exit(err.Error(), 1)
}
}
Execute(options)
return nil
},
Commands: []*cli.Command{
+207 -177
View File
@@ -5,6 +5,7 @@ import (
"crypto/ecdsa"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net"
"os"
@@ -16,15 +17,15 @@ import (
rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager"
"github.com/pbnjay/memory"
"github.com/prometheus/client_golang/prometheus"
"github.com/urfave/cli/v2"
"github.com/waku-org/go-waku/waku/persistence/sqlite"
dbutils "github.com/waku-org/go-waku/waku/persistence/utils"
"github.com/waku-org/go-waku/waku/v2/dnsdisc"
wakupeerstore "github.com/waku-org/go-waku/waku/v2/peerstore"
"github.com/waku-org/go-waku/waku/v2/rendezvous"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
dssql "github.com/ipfs/go-ds-sql"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -36,25 +37,32 @@ import (
"github.com/libp2p/go-libp2p/p2p/transport/tcp"
pubsub "github.com/libp2p/go-libp2p-pubsub"
"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoreds" // nolint: staticcheck
"github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoreds"
ws "github.com/libp2p/go-libp2p/p2p/transport/websocket"
"github.com/multiformats/go-multiaddr"
"github.com/waku-org/go-waku/cmd/waku/server/rest"
"github.com/waku-org/go-waku/cmd/waku/rest"
"github.com/waku-org/go-waku/cmd/waku/rpc"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/metrics"
"github.com/waku-org/go-waku/waku/persistence"
"github.com/waku-org/go-waku/waku/v2/dnsdisc"
"github.com/waku-org/go-waku/waku/v2/node"
wprotocol "github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/filter"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_store"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_filter"
"github.com/waku-org/go-waku/waku/v2/protocol/lightpush"
"github.com/waku-org/go-waku/waku/v2/protocol/peer_exchange"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/protocol/store"
"github.com/waku-org/go-waku/waku/v2/utils"
humanize "github.com/dustin/go-humanize"
)
func failOnErr(err error, msg string) {
if err != nil {
utils.Logger().Fatal(msg, zap.Error(err))
}
}
func requiresDB(options NodeOptions) bool {
return options.Store.Enable || options.Rendezvous.Enable
}
@@ -73,51 +81,31 @@ func scalePerc(value float64) float64 {
const dialTimeout = 7 * time.Second
func nonRecoverErrorMsg(format string, a ...any) error {
err := fmt.Errorf(format, a...)
return nonRecoverError(err)
}
func nonRecoverError(err error) error {
return cli.Exit(err.Error(), 166)
}
// Execute starts a go-waku node with settings determined by the Options parameter
func Execute(options NodeOptions) error {
func Execute(options NodeOptions) {
// Set encoding for logs (console, json, ...)
// Note that libp2p reads the encoding from GOLOG_LOG_FMT env var.
lvl, err := zapcore.ParseLevel(options.LogLevel)
if err != nil {
return err
}
utils.InitLogger(options.LogEncoding, options.LogOutput, "gowaku", lvl)
utils.InitLogger(options.LogEncoding, options.LogOutput)
hostAddr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", options.Address, options.Port))
if err != nil {
return nonRecoverErrorMsg("invalid host address: %w", err)
}
failOnErr(err, "invalid host address")
prvKey, err := getPrivKey(options)
if err != nil {
return err
}
failOnErr(err, "nodekey error")
p2pPrvKey := utils.EcdsaPrivKeyToSecp256k1PrivKey(prvKey)
id, err := peer.IDFromPublicKey(p2pPrvKey.GetPublic())
if err != nil {
return err
}
failOnErr(err, "deriving peer ID from private key")
logger := utils.Logger().With(logging.HostID("node", id))
var db *sql.DB
var migrationFn func(*sql.DB, *zap.Logger) error
var migrationFn func(*sql.DB) error
if requiresDB(options) && options.Store.Migration {
dbSettings := dbutils.DBSettings{}
db, migrationFn, err = dbutils.ParseURL(options.Store.DatabaseURL, dbSettings, logger)
if err != nil {
return nonRecoverErrorMsg("could not connect to DB: %w", err)
dbSettings := dbutils.DBSettings{
Vacuum: options.Store.Vacuum,
}
db, migrationFn, err = dbutils.ExtractDBAndMigration(options.Store.DatabaseURL, dbSettings, logger)
failOnErr(err, "Could not connect to DB")
}
ctx := context.Background()
@@ -128,17 +116,19 @@ func Execute(options NodeOptions) error {
go metricsServer.Start()
}
lvl, err := zapcore.ParseLevel(options.LogLevel)
if err != nil {
failOnErr(err, "log level error")
}
nodeOpts := []node.WakuNodeOption{
node.WithLogger(logger),
node.WithLogLevel(lvl),
node.WithPrivateKey(prvKey),
node.WithHostAddress(hostAddr),
node.WithKeepAlive(10*time.Second, options.KeepAlive),
node.WithKeepAlive(options.KeepAlive),
node.WithMaxPeerConnections(options.MaxPeerConnections),
node.WithPrometheusRegisterer(prometheus.DefaultRegisterer),
node.WithPeerStoreCapacity(options.PeerStoreCapacity),
node.WithMaxConnectionsPerIP(options.IPColocationLimit),
node.WithClusterID(uint16(options.ClusterID)),
}
if len(options.AdvertiseAddresses) != 0 {
nodeOpts = append(nodeOpts, node.WithAdvertiseAddresses(options.AdvertiseAddresses...))
@@ -147,14 +137,13 @@ func Execute(options NodeOptions) error {
if options.ExtIP != "" {
ip := net.ParseIP(options.ExtIP)
if ip == nil {
return nonRecoverErrorMsg("could not set external IP address: invalid IP")
failOnErr(errors.New("invalid IP address"), "could not set external IP address")
}
nodeOpts = append(nodeOpts, node.WithExternalIP(ip))
}
if options.DNS4DomainName != "" {
nodeOpts = append(nodeOpts, node.WithDNS4Domain(options.DNS4DomainName))
nodeOpts = append(nodeOpts, node.WithDns4Domain(options.DNS4DomainName))
}
libp2pOpts := node.DefaultLibP2POptions
@@ -166,9 +155,7 @@ func Execute(options NodeOptions) error {
limits := rcmgr.DefaultLimits // Default memory limit: 1/8th of total memory, minimum 128MB, maximum 1GB
scaledLimits := limits.Scale(int64(float64(memory.TotalMemory())*memPerc/100), int(float64(getNumFDs())*fdPerc/100))
resourceManager, err := rcmgr.NewResourceManager(rcmgr.NewFixedLimiter(scaledLimits))
if err != nil {
return fmt.Errorf("could not set resource limits: %w", err)
}
failOnErr(err, "setting resource limits")
libp2pOpts = append(libp2pOpts, libp2p.ResourceManager(resourceManager))
libp2p.SetDefaultServiceLimits(&limits)
@@ -182,20 +169,6 @@ func Execute(options NodeOptions) error {
libp2pOpts = append(libp2pOpts, libp2p.EnableRelayService())
}
if options.ForceReachability != "" {
libp2pOpts = append(libp2pOpts, libp2p.EnableRelay())
nodeOpts = append(nodeOpts, node.WithCircuitRelayParams(2*time.Second, 2*time.Second))
if options.ForceReachability == "private" {
logger.Warn("node forced to be unreachable!")
libp2pOpts = append(libp2pOpts, libp2p.ForceReachabilityPrivate())
} else if options.ForceReachability == "public" {
logger.Warn("node forced to be publicly reachable!")
libp2pOpts = append(libp2pOpts, libp2p.ForceReachabilityPublic())
} else {
return nonRecoverErrorMsg("invalid reachability value")
}
}
if options.UserAgent != "" {
libp2pOpts = append(libp2pOpts, libp2p.UserAgent(options.UserAgent))
}
@@ -210,23 +183,18 @@ func Execute(options NodeOptions) error {
if options.ShowAddresses {
printListeningAddresses(ctx, nodeOpts, options)
return nil
return
}
if options.Store.Enable && options.PersistPeers {
// Create persistent peerstore
queries, err := dbutils.NewQueries("peerstore", db)
if err != nil {
return nonRecoverErrorMsg("could not setup persistent peerstore database: %w", err)
}
queries, err := sqlite.NewQueries("peerstore", db)
failOnErr(err, "Peerstore")
datastore := dssql.NewDatastore(db, queries)
opts := pstoreds.DefaultOpts()
peerStore, err := pstoreds.NewPeerstore(ctx, datastore, opts)
if err != nil {
return nonRecoverErrorMsg("could not create persistent peerstore: %w", err)
}
failOnErr(err, "Peerstore")
nodeOpts = append(nodeOpts, node.WithPeerStore(peerStore))
}
@@ -234,15 +202,10 @@ func Execute(options NodeOptions) error {
nodeOpts = append(nodeOpts, node.WithLibP2POptions(libp2pOpts...))
nodeOpts = append(nodeOpts, node.WithNTP())
maxMsgSize := parseMsgSizeConfig(options.Relay.MaxMsgSize)
if options.Relay.Enable {
var wakurelayopts []pubsub.Option
wakurelayopts = append(wakurelayopts, pubsub.WithPeerExchange(options.Relay.PeerExchange))
wakurelayopts = append(wakurelayopts, pubsub.WithMaxMessageSize(maxMsgSize))
nodeOpts = append(nodeOpts, node.WithWakuRelayAndMinPeers(options.Relay.MinRelayPeersToPublish, wakurelayopts...))
nodeOpts = append(nodeOpts, node.WithMaxMsgSize(maxMsgSize))
}
nodeOpts = append(nodeOpts, node.WithWakuFilterLightNode())
@@ -251,6 +214,10 @@ func Execute(options NodeOptions) error {
nodeOpts = append(nodeOpts, node.WithWakuFilterFullNode(filter.WithTimeout(options.Filter.Timeout)))
}
if options.Filter.UseV1 {
nodeOpts = append(nodeOpts, node.WithLegacyWakuFilter(!options.Filter.DisableFullNode, legacy_filter.WithTimeout(options.Filter.Timeout)))
}
var dbStore *persistence.DBStore
if requiresDB(options) {
dbOptions := []persistence.DBOption{
@@ -263,10 +230,7 @@ func Execute(options NodeOptions) error {
}
dbStore, err = persistence.NewDBStore(prometheus.DefaultRegisterer, logger, dbOptions...)
if err != nil {
return nonRecoverErrorMsg("error setting up db store: %w", err)
}
failOnErr(err, "DBStore")
nodeOpts = append(nodeOpts, node.WithMessageProvider(dbStore))
}
@@ -279,78 +243,148 @@ func Execute(options NodeOptions) error {
nodeOpts = append(nodeOpts, node.WithLightPush())
}
var discoveredNodes []dnsdisc.DiscoveredNode
if options.DNSDiscovery.Enable {
if len(options.DNSDiscovery.URLs.Value()) != 0 {
for _, url := range options.DNSDiscovery.URLs.Value() {
logger.Info("attempting DNS discovery with ", zap.String("URL", url))
nodes, err := dnsdisc.RetrieveNodes(ctx, url, dnsdisc.WithNameserver(options.DNSDiscovery.Nameserver))
if err != nil {
logger.Warn("dns discovery error ", zap.Error(err))
} else {
var discPeerInfo []peer.AddrInfo
for _, n := range nodes {
discPeerInfo = append(discPeerInfo, n.PeerInfo)
}
logger.Info("found dns entries ", zap.Any("nodes", discPeerInfo))
discoveredNodes = append(discoveredNodes, nodes...)
}
}
} else {
logger.Fatal("DNS discovery URL is required")
}
}
if options.DiscV5.Enable {
var bootnodes []*enode.Node
for _, addr := range options.DiscV5.Nodes.Value() {
bootnode, err := enode.Parse(enode.ValidSchemes, addr)
if err != nil {
logger.Fatal("parsing ENR", zap.Error(err))
}
bootnodes = append(bootnodes, bootnode)
}
for _, n := range discoveredNodes {
if n.ENR != nil {
bootnodes = append(bootnodes, n.ENR)
}
}
nodeOpts = append(nodeOpts, node.WithDiscoveryV5(options.DiscV5.Port, bootnodes, options.DiscV5.AutoUpdate))
}
if options.PeerExchange.Enable {
nodeOpts = append(nodeOpts, node.WithPeerExchange())
}
if options.Rendezvous.Enable {
rdb := rendezvous.NewDB(db, logger)
rdb := rendezvous.NewDB(ctx, db, logger)
nodeOpts = append(nodeOpts, node.WithRendezvous(rdb))
}
utils.Logger().Info("Version details ", zap.String("version", node.Version), zap.String("commit", node.GitCommit))
if err = checkForRLN(logger, options, &nodeOpts); err != nil {
return nonRecoverError(err)
}
var discoveredNodes []dnsdisc.DiscoveredNode
if options.DNSDiscovery.Enable {
if len(options.DNSDiscovery.URLs.Value()) == 0 {
return nonRecoverErrorMsg("DNS discovery URL is required")
}
discoveredNodes = node.GetNodesFromDNSDiscovery(logger, ctx, options.DNSDiscovery.Nameserver, options.DNSDiscovery.URLs.Value())
}
if options.DiscV5.Enable {
discv5Opts, err := node.GetDiscv5Option(discoveredNodes, options.DiscV5.Nodes.Value(), options.DiscV5.Port, options.DiscV5.AutoUpdate)
if err != nil {
logger.Fatal("parsing ENR", zap.Error(err))
}
nodeOpts = append(nodeOpts, discv5Opts)
}
checkForRLN(logger, options, &nodeOpts)
wakuNode, err := node.New(nodeOpts...)
if err != nil {
return fmt.Errorf("could not instantiate waku: %w", err)
utils.Logger().Info("Version details ", zap.String("version", node.Version), zap.String("commit", node.GitCommit))
failOnErr(err, "Wakunode")
if options.Filter.UseV1 {
addStaticPeers(wakuNode, options.Filter.NodesV1, legacy_filter.FilterID_v20beta1)
}
//Process pubSub and contentTopics specified and arrive at all corresponding pubSubTopics
pubSubTopicMap, err := processTopics(options)
if err != nil {
return nonRecoverError(err)
}
pubSubTopicMapKeys := make([]string, 0, len(pubSubTopicMap))
for k := range pubSubTopicMap {
pubSubTopicMapKeys = append(pubSubTopicMapKeys, k)
}
pubSubTopicMap := processTopics(options)
if err = wakuNode.Start(ctx); err != nil {
return nonRecoverError(err)
logger.Fatal("starting waku node", zap.Error(err))
}
for _, d := range discoveredNodes {
wakuNode.AddDiscoveredPeer(d.PeerID, d.PeerInfo.Addrs, wakupeerstore.DNSDiscovery, nil, d.ENR, true)
wakuNode.AddDiscoveredPeer(d.PeerID, d.PeerInfo.Addrs, wakupeerstore.DnsDiscovery)
}
//For now assuming that static peers added support/listen on all topics specified via commandLine.
staticPeers := map[protocol.ID][]multiaddr.Multiaddr{
legacy_store.StoreID_v20beta4: options.Store.Nodes,
lightpush.LightPushID_v20beta1: options.LightPush.Nodes,
rendezvous.RendezvousID: options.Rendezvous.Nodes,
filter.FilterSubscribeID_v20beta1: options.Filter.Nodes,
}
for protocolID, peers := range staticPeers {
if err = addStaticPeers(wakuNode, peers, pubSubTopicMapKeys, protocolID); err != nil {
return err
}
}
addStaticPeers(wakuNode, options.Store.Nodes, store.StoreID_v20beta4)
addStaticPeers(wakuNode, options.LightPush.Nodes, lightpush.LightPushID_v20beta1)
addStaticPeers(wakuNode, options.Rendezvous.Nodes, rendezvous.RendezvousID)
addStaticPeers(wakuNode, options.Filter.Nodes, filter.FilterSubscribeID_v20beta1)
var wg sync.WaitGroup
if options.Relay.Enable {
if err = handleRelayTopics(ctx, &wg, wakuNode, pubSubTopicMap); err != nil {
return err
for nodeTopic := range pubSubTopicMap {
nodeTopic := nodeTopic
sub, err := wakuNode.Relay().SubscribeToTopic(ctx, nodeTopic)
failOnErr(err, "Error subscring to topic")
sub.Unsubscribe()
if len(options.Rendezvous.Nodes) != 0 {
// Register the node in rendezvous point
iter := rendezvous.NewRendezvousPointIterator(options.Rendezvous.Nodes)
wg.Add(1)
go func(nodeTopic string) {
t := time.NewTicker(rendezvous.RegisterDefaultTTL)
defer t.Stop()
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case <-t.C:
// Register in rendezvous points periodically
wakuNode.Rendezvous().RegisterWithNamespace(ctx, nodeTopic, iter.RendezvousPoints())
}
}
}(nodeTopic)
wg.Add(1)
go func(nodeTopic string) {
defer wg.Done()
desiredOutDegree := wakuNode.Relay().Params().D
t := time.NewTicker(7 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
peerCnt := len(wakuNode.Relay().PubSub().ListPeers(nodeTopic))
peersToFind := desiredOutDegree - peerCnt
if peersToFind <= 0 {
continue
}
rp := <-iter.Next(ctx)
if rp == nil {
continue
}
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
wakuNode.Rendezvous().DiscoverWithNamespace(ctx, nodeTopic, rp, peersToFind)
cancel()
}
}
}(nodeTopic)
}
}
for _, protectedTopic := range options.Relay.ProtectedTopics {
err := wakuNode.Relay().AddSignedTopicValidator(protectedTopic.Topic, protectedTopic.PublicKey)
failOnErr(err, "Error adding signed topic validator")
}
}
@@ -375,8 +409,7 @@ func Execute(options NodeOptions) error {
if options.PeerExchange.Enable && options.PeerExchange.Node != nil {
logger.Info("retrieving peer info via peer exchange protocol")
peerID, err := wakuNode.AddPeer(*options.PeerExchange.Node, wakupeerstore.Static,
pubSubTopicMapKeys, peer_exchange.PeerExchangeID_v20alpha1)
peerID, err := wakuNode.AddPeer(*options.PeerExchange.Node, wakupeerstore.Static, peer_exchange.PeerExchangeID_v20alpha1)
if err != nil {
logger.Error("adding peer exchange peer", logging.MultiAddrs("node", *options.PeerExchange.Node), zap.Error(err))
} else {
@@ -387,17 +420,30 @@ func Execute(options NodeOptions) error {
}
}
if len(discoveredNodes) != 0 {
for _, n := range discoveredNodes {
go func(ctx context.Context, info peer.AddrInfo) {
ctx, cancel := context.WithTimeout(ctx, dialTimeout)
defer cancel()
err = wakuNode.DialPeerWithInfo(ctx, info)
if err != nil {
logger.Error("dialing peer", logging.HostID("peer", info.ID), zap.Error(err))
}
}(ctx, n.PeerInfo)
}
}
var rpcServer *rpc.WakuRpc
if options.RPCServer.Enable {
rpcServer = rpc.NewWakuRpc(wakuNode, options.RPCServer.Address, options.RPCServer.Port, options.RPCServer.Admin, options.PProf, options.RPCServer.RelayCacheCapacity, logger)
rpcServer.Start()
}
var restServer *rest.WakuRest
if options.RESTServer.Enable {
wg.Add(1)
restConfig := rest.RestConfig{Address: options.RESTServer.Address,
Port: uint(options.RESTServer.Port),
EnablePProf: options.PProf,
EnableAdmin: options.RESTServer.Admin,
RelayCacheCapacity: uint(options.RESTServer.RelayCacheCapacity),
FilterCacheCapacity: uint(options.RESTServer.FilterCacheCapacity)}
restServer = rest.NewWakuRest(wakuNode, restConfig, logger)
restServer = rest.NewWakuRest(wakuNode, options.RESTServer.Address, options.RESTServer.Port, options.RESTServer.Admin, options.PProf, options.RESTServer.RelayCacheCapacity, logger)
restServer.Start(ctx, &wg)
}
@@ -413,69 +459,62 @@ func Execute(options NodeOptions) error {
// shut the node down
wakuNode.Stop()
if options.RPCServer.Enable {
err := rpcServer.Stop(ctx)
failOnErr(err, "RPCClose")
}
if options.RESTServer.Enable {
if err := restServer.Stop(ctx); err != nil {
return err
}
err := restServer.Stop(ctx)
failOnErr(err, "RESTClose")
}
if options.Metrics.Enable {
if err = metricsServer.Stop(ctx); err != nil {
return err
}
err = metricsServer.Stop(ctx)
failOnErr(err, "MetricsClose")
}
if db != nil {
if err = db.Close(); err != nil {
return err
}
if options.Store.Enable {
err = db.Close()
failOnErr(err, "DBClose")
}
return nil
}
func processTopics(options NodeOptions) (map[string][]string, error) {
func processTopics(options NodeOptions) map[string]struct{} {
//Using a map to avoid duplicate pub-sub topics that can result from autosharding
// or same-topic being passed twice.
pubSubTopicMap := make(map[string][]string)
pubSubTopicMap := make(map[string]struct{})
for _, topic := range options.Relay.Topics.Value() {
pubSubTopicMap[topic] = []string{}
pubSubTopicMap[topic] = struct{}{}
}
for _, topic := range options.Relay.PubSubTopics.Value() {
pubSubTopicMap[topic] = []string{}
pubSubTopicMap[topic] = struct{}{}
}
//Get pubSub topics from contentTopics if they are as per autosharding
for _, cTopic := range options.Relay.ContentTopics.Value() {
contentTopic, err := wprotocol.StringToContentTopic(cTopic)
if err != nil {
return nil, err
failOnErr(err, "failed to parse content topic")
}
pTopic := wprotocol.GetShardFromContentTopic(contentTopic, wprotocol.GenerationZeroShardsCount)
if _, ok := pubSubTopicMap[pTopic.String()]; !ok {
pubSubTopicMap[pTopic.String()] = []string{}
}
pubSubTopicMap[pTopic.String()] = append(pubSubTopicMap[pTopic.String()], cTopic)
pubSubTopicMap[pTopic.String()] = struct{}{}
}
//If no topics are passed, then use default waku topic.
if len(pubSubTopicMap) == 0 && options.ClusterID == 0 {
pubSubTopicMap[relay.DefaultWakuTopic] = []string{}
if len(pubSubTopicMap) == 0 {
pubSubTopicMap[relay.DefaultWakuTopic] = struct{}{}
}
return pubSubTopicMap, nil
return pubSubTopicMap
}
func addStaticPeers(wakuNode *node.WakuNode, addresses []multiaddr.Multiaddr, pubSubTopics []string, protocols ...protocol.ID) error {
func addStaticPeers(wakuNode *node.WakuNode, addresses []multiaddr.Multiaddr, protocols ...protocol.ID) {
for _, addr := range addresses {
_, err := wakuNode.AddPeer(addr, wakupeerstore.Static, pubSubTopics, protocols...)
if err != nil {
return fmt.Errorf("could not add static peer: %w", err)
}
_, err := wakuNode.AddPeer(addr, wakupeerstore.Static, protocols...)
failOnErr(err, "error adding peer")
}
return nil
}
func loadPrivateKeyFromFile(path string, passwd string) (*ecdsa.PrivateKey, error) {
@@ -560,12 +599,3 @@ func printListeningAddresses(ctx context.Context, nodeOpts []node.WakuNodeOption
}
}
func parseMsgSizeConfig(msgSizeConfig string) int {
msgSize, err := humanize.ParseBytes(msgSizeConfig)
if err != nil {
msgSize = 0
}
return int(msgSize)
}
+3 -4
View File
@@ -1,5 +1,5 @@
//go:build gowaku_no_rln
// +build gowaku_no_rln
//go:build !gowaku_rln
// +build !gowaku_rln
package main
@@ -8,7 +8,6 @@ import (
"go.uber.org/zap"
)
func checkForRLN(logger *zap.Logger, options NodeOptions, nodeOpts *[]node.WakuNodeOption) error {
func checkForRLN(logger *zap.Logger, options NodeOptions, nodeOpts *[]node.WakuNodeOption) {
// Do nothing
return nil
}
+6 -9
View File
@@ -1,5 +1,5 @@
//go:build !gowaku_no_rln
// +build !gowaku_no_rln
//go:build gowaku_rln
// +build gowaku_rln
package main
@@ -11,14 +11,13 @@ import (
"go.uber.org/zap"
)
func checkForRLN(logger *zap.Logger, options NodeOptions, nodeOpts *[]node.WakuNodeOption) error {
func checkForRLN(logger *zap.Logger, options NodeOptions, nodeOpts *[]node.WakuNodeOption) {
if options.RLNRelay.Enable {
if !options.Relay.Enable {
return errors.New("waku relay is required to enable RLN relay")
failOnErr(errors.New("relay not available"), "Could not enable RLN Relay")
}
if !options.RLNRelay.Dynamic {
*nodeOpts = append(*nodeOpts, node.WithStaticRLNRelay((*rln.MembershipIndex)(options.RLNRelay.MembershipIndex), nil))
*nodeOpts = append(*nodeOpts, node.WithStaticRLNRelay(rln.MembershipIndex(options.RLNRelay.MembershipIndex), nil))
} else {
// TODO: too many parameters in this function
// consider passing a config struct instead
@@ -27,12 +26,10 @@ func checkForRLN(logger *zap.Logger, options NodeOptions, nodeOpts *[]node.WakuN
options.RLNRelay.CredentialsPassword,
options.RLNRelay.TreePath,
options.RLNRelay.MembershipContractAddress,
options.RLNRelay.MembershipIndex,
rln.MembershipIndex(options.RLNRelay.MembershipIndex),
nil,
options.RLNRelay.ETHClientAddress,
))
}
}
return nil
}
+19 -12
View File
@@ -30,7 +30,6 @@ type RelayOptions struct {
ContentTopics cli.StringSlice
PeerExchange bool
MinRelayPeersToPublish int
MaxMsgSize string
}
// RLNRelayOptions are settings used to enable RLN Relay. This is a protocol
@@ -41,7 +40,7 @@ type RLNRelayOptions struct {
CredentialsPath string
CredentialsPassword string
TreePath string
MembershipIndex *uint
MembershipIndex uint
Dynamic bool
ETHClientAddress string
MembershipContractAddress common.Address
@@ -53,8 +52,10 @@ type RLNRelayOptions struct {
// restricted devices.
type FilterOptions struct {
Enable bool
UseV1 bool
DisableFullNode bool
Nodes []multiaddr.Multiaddr
NodesV1 []multiaddr.Multiaddr
Timeout time.Duration
}
@@ -79,6 +80,7 @@ type StoreOptions struct {
RetentionMaxMessages int
//ResumeNodes []multiaddr.Multiaddr
Nodes []multiaddr.Multiaddr
Vacuum bool
Migration bool
}
@@ -99,14 +101,22 @@ type MetricsOptions struct {
Port int
}
// RPCServerOptions are settings used to start a json rpc server
type RPCServerOptions struct {
Enable bool
Port int
Address string
Admin bool
RelayCacheCapacity int
}
// RESTServerOptions are settings used to start a rest http server
type RESTServerOptions struct {
Enable bool
Port int
Address string
Admin bool
RelayCacheCapacity int
FilterCacheCapacity int
Enable bool
Port int
Address string
Admin bool
RelayCacheCapacity int
}
// WSOptions are settings used for enabling websockets and secure websockets
@@ -138,7 +148,6 @@ type RendezvousOptions struct {
type NodeOptions struct {
Port int
Address string
ClusterID uint
DNS4DomainName string
NodeKey *ecdsa.PrivateKey
KeyFile string
@@ -148,7 +157,6 @@ type NodeOptions struct {
AdvertiseAddresses []multiaddr.Multiaddr
ShowAddresses bool
CircuitRelay bool
ForceReachability string
ResourceScalingMemoryPercent float64
ResourceScalingFDPercent float64
LogLevel string
@@ -160,8 +168,6 @@ type NodeOptions struct {
UserAgent string
PProf bool
MaxPeerConnections int
PeerStoreCapacity int
IPColocationLimit int
PeerExchange PeerExchangeOptions
Websocket WSOptions
@@ -174,5 +180,6 @@ type NodeOptions struct {
DNSDiscovery DNSDiscoveryOptions
Rendezvous RendezvousOptions
Metrics MetricsOptions
RPCServer RPCServerOptions
RESTServer RESTServerOptions
}
-82
View File
@@ -1,82 +0,0 @@
package main
import (
"context"
"sync"
"time"
"github.com/waku-org/go-waku/waku/v2/node"
wprotocol "github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/rendezvous"
)
func handleRelayTopics(ctx context.Context, wg *sync.WaitGroup, wakuNode *node.WakuNode, pubSubTopicMap map[string][]string) error {
for nodeTopic, cTopics := range pubSubTopicMap {
nodeTopic := nodeTopic
_, err := wakuNode.Relay().Subscribe(ctx, wprotocol.NewContentFilter(nodeTopic, cTopics...), relay.WithoutConsumer())
if err != nil {
return err
}
if len(options.Rendezvous.Nodes) != 0 {
// Register the node in rendezvous point
iter := rendezvous.NewRendezvousPointIterator(options.Rendezvous.Nodes)
wg.Add(1)
go func(nodeTopic string) {
t := time.NewTicker(rendezvous.RegisterDefaultTTL)
defer t.Stop()
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case <-t.C:
// Register in rendezvous points periodically
wakuNode.Rendezvous().RegisterWithNamespace(ctx, nodeTopic, iter.RendezvousPoints())
}
}
}(nodeTopic)
wg.Add(1)
go func(nodeTopic string) {
defer wg.Done()
desiredOutDegree := wakuNode.Relay().Params().D
t := time.NewTicker(7 * time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
peerCnt := len(wakuNode.Relay().PubSub().ListPeers(nodeTopic))
peersToFind := desiredOutDegree - peerCnt
if peersToFind <= 0 {
continue
}
rp := <-iter.Next(ctx)
if rp == nil {
continue
}
ctx, cancel := context.WithTimeout(ctx, 7*time.Second)
wakuNode.Rendezvous().DiscoverWithNamespace(ctx, nodeTopic, rp, peersToFind)
cancel()
}
}
}(nodeTopic)
}
}
// Protected topics
for _, protectedTopic := range options.Relay.ProtectedTopics {
if err := wakuNode.Relay().AddSignedTopicValidator(protectedTopic.Topic, protectedTopic.PublicKey); err != nil {
return nonRecoverErrorMsg("could not add signed topic validator: %w", err)
}
}
return nil
}
@@ -20,8 +20,8 @@ type InfoReply struct {
ListenAddresses []string `json:"listenAddresses,omitempty"`
}
const routeDebugInfoV1 = "/debug/v1/info"
const routeDebugVersionV1 = "/debug/v1/version"
const ROUTE_DEBUG_INFOV1 = "/debug/v1/info"
const ROUTE_DEBUG_VERSIONV1 = "/debug/v1/info"
func NewDebugService(node *node.WakuNode, m *chi.Mux) *DebugService {
d := &DebugService{
@@ -29,15 +29,15 @@ func NewDebugService(node *node.WakuNode, m *chi.Mux) *DebugService {
mux: m,
}
m.Get(routeDebugInfoV1, d.getV1Info)
m.Get(routeDebugVersionV1, d.getV1Version)
m.Get(ROUTE_DEBUG_INFOV1, d.getV1Info)
m.Get(ROUTE_DEBUG_VERSIONV1, d.getV1Version)
return d
}
type VersionResponse string
func (d *DebugService) getV1Info(w http.ResponseWriter, req *http.Request) {
func (d *DebugService) getV1Info(w http.ResponseWriter, r *http.Request) {
response := new(InfoReply)
response.ENRUri = d.node.ENR().String()
for _, addr := range d.node.ListenAddresses() {
@@ -46,7 +46,7 @@ func (d *DebugService) getV1Info(w http.ResponseWriter, req *http.Request) {
writeErrOrResponse(w, nil, response)
}
func (d *DebugService) getV1Version(w http.ResponseWriter, req *http.Request) {
func (d *DebugService) getV1Version(w http.ResponseWriter, r *http.Request) {
response := VersionResponse(node.GetVersionInfo().String())
writeErrOrResponse(w, nil, response)
}
@@ -1,6 +1,6 @@
openapi: 3.0.3
info:
title: Waku V2 node Debug REST API
title: Waku V2 node REST API
version: 1.0.0
contact:
name: VAC Team
@@ -22,7 +22,7 @@ func TestGetV1Info(t *testing.T) {
node: wakuNode1,
}
request, err := http.NewRequest(http.MethodPost, routeDebugInfoV1, bytes.NewReader([]byte("")))
request, err := http.NewRequest(http.MethodPost, ROUTE_DEBUG_INFOV1, bytes.NewReader([]byte("")))
require.NoError(t, err)
rr := httptest.NewRecorder()
+200
View File
@@ -0,0 +1,200 @@
package rest
import (
"context"
"encoding/json"
"net/http"
"strings"
"sync"
"github.com/go-chi/chi/v5"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"go.uber.org/zap"
)
const ROUTE_RELAY_SUBSCRIPTIONSV1 = "/relay/v1/subscriptions"
const ROUTE_RELAY_MESSAGESV1 = "/relay/v1/messages/{topic}"
type RelayService struct {
node *node.WakuNode
cancel context.CancelFunc
log *zap.Logger
messages map[string][]*pb.WakuMessage
cacheCapacity int
messagesMutex sync.RWMutex
runner *runnerService
}
func NewRelayService(node *node.WakuNode, m *chi.Mux, cacheCapacity int, log *zap.Logger) *RelayService {
s := &RelayService{
node: node,
log: log.Named("relay"),
cacheCapacity: cacheCapacity,
messages: make(map[string][]*pb.WakuMessage),
}
s.runner = newRunnerService(node.Broadcaster(), s.addEnvelope)
m.Post(ROUTE_RELAY_SUBSCRIPTIONSV1, s.postV1Subscriptions)
m.Delete(ROUTE_RELAY_SUBSCRIPTIONSV1, s.deleteV1Subscriptions)
m.Get(ROUTE_RELAY_MESSAGESV1, s.getV1Messages)
m.Post(ROUTE_RELAY_MESSAGESV1, s.postV1Message)
return s
}
func (r *RelayService) addEnvelope(envelope *protocol.Envelope) {
r.messagesMutex.Lock()
defer r.messagesMutex.Unlock()
if _, ok := r.messages[envelope.PubsubTopic()]; !ok {
return
}
// Keep a specific max number of messages per topic
if len(r.messages[envelope.PubsubTopic()]) >= r.cacheCapacity {
r.messages[envelope.PubsubTopic()] = r.messages[envelope.PubsubTopic()][1:]
}
r.messages[envelope.PubsubTopic()] = append(r.messages[envelope.PubsubTopic()], envelope.Message())
}
func (r *RelayService) Start(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
r.cancel = cancel
r.messagesMutex.Lock()
// Node may already be subscribed to some topics when Relay API handlers are installed. Let's add these
for _, topic := range r.node.Relay().Topics() {
r.log.Info("adding topic handler for existing subscription", zap.String("topic", topic))
r.messages[topic] = []*pb.WakuMessage{}
}
r.messagesMutex.Unlock()
r.runner.Start(ctx)
}
func (r *RelayService) Stop() {
if r.cancel == nil {
return
}
r.cancel()
}
func (d *RelayService) deleteV1Subscriptions(w http.ResponseWriter, r *http.Request) {
var topics []string
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&topics); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
d.messagesMutex.Lock()
defer d.messagesMutex.Unlock()
var err error
for _, topic := range topics {
err = d.node.Relay().Unsubscribe(r.Context(), topic)
if err != nil {
d.log.Error("unsubscribing from topic", zap.String("topic", strings.Replace(strings.Replace(topic, "\n", "", -1), "\r", "", -1)), zap.Error(err))
} else {
delete(d.messages, topic)
}
}
writeErrOrResponse(w, err, true)
}
func (d *RelayService) postV1Subscriptions(w http.ResponseWriter, r *http.Request) {
var topics []string
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&topics); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
var err error
var sub *relay.Subscription
var topicToSubscribe string
for _, topic := range topics {
if topic == "" {
sub, err = d.node.Relay().Subscribe(r.Context())
topicToSubscribe = relay.DefaultWakuTopic
} else {
sub, err = d.node.Relay().SubscribeToTopic(r.Context(), topic)
topicToSubscribe = topic
}
if err != nil {
d.log.Error("subscribing to topic", zap.String("topic", strings.Replace(topicToSubscribe, "\n", "", -1)), zap.Error(err))
} else {
sub.Unsubscribe()
d.messagesMutex.Lock()
d.messages[topic] = []*pb.WakuMessage{}
d.messagesMutex.Unlock()
}
}
writeErrOrResponse(w, err, true)
}
func (d *RelayService) getV1Messages(w http.ResponseWriter, r *http.Request) {
topic := chi.URLParam(r, "topic")
if topic == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
var err error
d.messagesMutex.Lock()
defer d.messagesMutex.Unlock()
if _, ok := d.messages[topic]; !ok {
w.WriteHeader(http.StatusNotFound)
_, err = w.Write([]byte("not subscribed to topic"))
d.log.Error("writing response", zap.Error(err))
return
}
response := d.messages[topic]
d.messages[topic] = []*pb.WakuMessage{}
writeErrOrResponse(w, nil, response)
}
func (d *RelayService) postV1Message(w http.ResponseWriter, r *http.Request) {
topic := chi.URLParam(r, "topic")
if topic == "" {
w.WriteHeader(http.StatusBadRequest)
return
}
var message *pb.WakuMessage
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&message); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer r.Body.Close()
var err error
if topic == "" {
_, err = d.node.Relay().Publish(r.Context(), message)
} else {
_, err = d.node.Relay().PublishToTopic(r.Context(), message, strings.Replace(topic, "\n", "", -1))
}
if err != nil {
d.log.Error("publishing message", zap.Error(err))
}
writeErrOrResponse(w, err, true)
}
@@ -1,6 +1,6 @@
openapi: 3.0.3
info:
title: Waku V2 node Relay REST API
title: Waku V2 node REST API
version: 1.0.0
contact:
name: VAC Team
@@ -106,103 +106,6 @@ paths:
'5XX':
description: Unexpected error.
/relay/v1/auto/messages/{contentTopic}: # Note the plural in messages
get: # get_waku_v2_relay_v1_auto_messages
summary: Get the latest messages on the polled topic
description: Get a list of messages that were received on a subscribed Content topic after the last time this method was called.
operationId: getMessagesByTopic
tags:
- relay
parameters:
- in: path
name: contentTopic # Note the name is the same as in the path
required: true
schema:
type: string
description: The user ID
responses:
'200':
description: The latest messages on the polled topic.
content:
application/json:
schema:
$ref: '#/components/schemas/RelayGetMessagesResponse'
'4XX':
description: Bad request.
'5XX':
description: Unexpected error.
/relay/v1/auto/messages: # Note the plural in messages
post: # post_waku_v2_relay_v1_auto_message
summary: Publish a message to be relayed
description: Publishes a message to be relayed on a Content topic.
operationId: postMessagesToTopic
tags:
- relay
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/RelayPostMessagesRequest'
responses:
'200':
description: OK
'4XX':
description: Bad request.
'5XX':
description: Unexpected error.
/relay/v1/auto/subscriptions:
post: # post_waku_v2_relay_v1_auto_subscriptions
summary: Subscribe a node to an array of topics
description: Subscribe a node to an array of Content topics.
operationId: postSubscriptions
tags:
- relay
requestBody:
content:
application/json:
schema:
type array:
items:
$ref: '#/components/schemas/ContentTopic'
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
'4XX':
description: Bad request.
'5XX':
description: Unexpected error.
delete: # delete_waku_v2_relay_v1_auto_subscriptions
summary: Unsubscribe a node from an array of topics
description: Unsubscribe a node from an array of Content topics.
operationId: deleteSubscriptions
tags:
- relay
requestBody:
content:
application/json:
schema:
type array:
items:
$ref: '#/components/schemas/ContentTopic'
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
'4XX':
description: Bad request.
'5XX':
description: Unexpected error.
components:
schemas:
PubSubTopic:
@@ -242,4 +145,4 @@ components:
type: array
items:
$ref: '#/components/schemas/PubSubTopic'
+173
View File
@@ -0,0 +1,173 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/tests"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/utils"
)
func makeRelayService(t *testing.T, mux *chi.Mux) *RelayService {
options := node.WithWakuRelayAndMinPeers(0)
n, err := node.New(options)
require.NoError(t, err)
err = n.Start(context.Background())
require.NoError(t, err)
return NewRelayService(n, mux, 3, utils.Logger())
}
func TestPostV1Message(t *testing.T) {
router := chi.NewRouter()
_ = makeRelayService(t, router)
msg := &pb.WakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "abc",
Version: 0,
Timestamp: utils.GetUnixEpoch(),
}
msgJsonBytes, err := json.Marshal(msg)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/relay/v1/messages/test", bytes.NewReader(msgJsonBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
}
func TestRelaySubscription(t *testing.T) {
router := chi.NewRouter()
d := makeRelayService(t, router)
go d.Start(context.Background())
defer d.Stop()
// Wait for node to start
time.Sleep(500 * time.Millisecond)
topics := []string{"test"}
topicsJSONBytes, err := json.Marshal(topics)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/relay/v1/subscriptions", bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
// Test max messages in subscription
now := utils.GetUnixEpoch()
d.runner.broadcaster.Submit(protocol.NewEnvelope(tests.CreateWakuMessage("test", now+1), now, "test"))
d.runner.broadcaster.Submit(protocol.NewEnvelope(tests.CreateWakuMessage("test", now+2), now, "test"))
d.runner.broadcaster.Submit(protocol.NewEnvelope(tests.CreateWakuMessage("test", now+3), now, "test"))
// Wait for the messages to be processed
time.Sleep(500 * time.Millisecond)
require.Len(t, d.messages["test"], 3)
d.runner.broadcaster.Submit(protocol.NewEnvelope(tests.CreateWakuMessage("test", now+4), now+4, "test"))
time.Sleep(500 * time.Millisecond)
// Should only have 3 messages
require.Len(t, d.messages["test"], 3)
// Test deletion
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodDelete, "/relay/v1/subscriptions", bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
require.Len(t, d.messages["test"], 0)
}
func TestRelayGetV1Messages(t *testing.T) {
router := chi.NewRouter()
serviceA := makeRelayService(t, router)
go serviceA.Start(context.Background())
defer serviceA.Stop()
serviceB := makeRelayService(t, router)
go serviceB.Start(context.Background())
defer serviceB.Stop()
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", serviceB.node.Host().ID().Pretty()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range serviceB.node.Host().Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
err = serviceA.node.DialPeerWithMultiAddress(context.Background(), addr)
require.NoError(t, err)
// Wait for the dial to complete
time.Sleep(1 * time.Second)
topics := []string{"test"}
topicsJSONBytes, err := json.Marshal(topics)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/relay/v1/subscriptions", bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Wait for the subscription to be started
time.Sleep(1 * time.Second)
msg := &pb.WakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "test",
Version: 0,
Timestamp: utils.GetUnixEpoch(),
}
msgJsonBytes, err := json.Marshal(msg)
require.NoError(t, err)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/relay/v1/messages/test", bytes.NewReader(msgJsonBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Wait for the message to be received
time.Sleep(1 * time.Second)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, "/relay/v1/messages/test", bytes.NewReader([]byte{}))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
var messages []*pb.WakuMessage
err = json.Unmarshal(rr.Body.Bytes(), &messages)
require.NoError(t, err)
require.Len(t, messages, 1)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, "/relay/v1/messages/test", bytes.NewReader([]byte{}))
router.ServeHTTP(rr, req)
err = json.Unmarshal(rr.Body.Bytes(), &messages)
require.NoError(t, err)
require.Len(t, messages, 0)
}
@@ -11,7 +11,7 @@ type Adder func(msg *protocol.Envelope)
type runnerService struct {
broadcaster relay.Broadcaster
sub *relay.Subscription
sub relay.Subscription
cancel context.CancelFunc
adder Adder
}
@@ -26,7 +26,7 @@ func newRunnerService(broadcaster relay.Broadcaster, adder Adder) *runnerService
func (r *runnerService) Start(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
r.cancel = cancel
r.sub = r.broadcaster.RegisterForAll(relay.WithBufferSize(relay.DefaultRelaySubscriptionBufferSize))
r.sub = r.broadcaster.RegisterForAll(1024)
for {
select {
case <-ctx.Done():
+213
View File
@@ -0,0 +1,213 @@
package rest
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/multiformats/go-multiaddr"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/peerstore"
"github.com/waku-org/go-waku/waku/v2/protocol/store"
"github.com/waku-org/go-waku/waku/v2/protocol/store/pb"
"github.com/waku-org/go-waku/waku/v2/utils"
)
type StoreService struct {
node *node.WakuNode
mux *chi.Mux
}
type StoreResponse struct {
Messages []StoreWakuMessage `json:"messages"`
Cursor *HistoryCursor `json:"cursor,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type HistoryCursor struct {
PubsubTopic string `json:"pubsub_topic"`
SenderTime string `json:"sender_time"`
StoreTime string `json:"store_time"`
Digest []byte `json:"digest"`
}
type StoreWakuMessage struct {
Payload []byte `json:"payload"`
ContentTopic string `json:"content_topic"`
Version int32 `json:"version"`
Timestamp int64 `json:"timestamp"`
Meta []byte `json:"meta"`
}
const ROUTE_STORE_MESSAGESV1 = "/store/v1/messages"
func NewStoreService(node *node.WakuNode, m *chi.Mux) *StoreService {
s := &StoreService{
node: node,
mux: m,
}
m.Get(ROUTE_STORE_MESSAGESV1, s.getV1Messages)
return s
}
func getStoreParams(r *http.Request) (multiaddr.Multiaddr, *store.Query, []store.HistoryRequestOption, error) {
query := &store.Query{}
var options []store.HistoryRequestOption
peerAddrStr := r.URL.Query().Get("peerAddr")
m, err := multiaddr.NewMultiaddr(peerAddrStr)
if err != nil {
return nil, nil, nil, err
}
peerID, err := utils.GetPeerID(m)
if err != nil {
return nil, nil, nil, err
}
options = append(options, store.WithPeer(peerID))
query.Topic = r.URL.Query().Get("pubsubTopic")
contentTopics := r.URL.Query().Get("contentTopics")
if contentTopics != "" {
query.ContentTopics = strings.Split(contentTopics, ",")
}
startTimeStr := r.URL.Query().Get("startTime")
if startTimeStr != "" {
query.StartTime, err = strconv.ParseInt(startTimeStr, 10, 64)
if err != nil {
return nil, nil, nil, err
}
}
endTimeStr := r.URL.Query().Get("endTime")
if endTimeStr != "" {
query.EndTime, err = strconv.ParseInt(endTimeStr, 10, 64)
if err != nil {
return nil, nil, nil, err
}
}
var cursor *pb.Index
senderTimeStr := r.URL.Query().Get("senderTime")
storeTimeStr := r.URL.Query().Get("storeTime")
digestStr := r.URL.Query().Get("digest")
if senderTimeStr != "" || storeTimeStr != "" || digestStr != "" {
cursor = &pb.Index{}
if senderTimeStr != "" {
cursor.SenderTime, err = strconv.ParseInt(senderTimeStr, 10, 64)
if err != nil {
return nil, nil, nil, err
}
}
if storeTimeStr != "" {
cursor.ReceiverTime, err = strconv.ParseInt(storeTimeStr, 10, 64)
if err != nil {
return nil, nil, nil, err
}
}
if digestStr != "" {
cursor.Digest, err = base64.URLEncoding.DecodeString(digestStr)
if err != nil {
return nil, nil, nil, err
}
}
cursor.PubsubTopic = query.Topic
options = append(options, store.WithCursor(cursor))
}
pageSizeStr := r.URL.Query().Get("pageSize")
ascendingStr := r.URL.Query().Get("ascending")
if ascendingStr != "" || pageSizeStr != "" {
ascending := true
pageSize := uint64(store.MaxPageSize)
if ascendingStr != "" {
ascending, err = strconv.ParseBool(ascendingStr)
if err != nil {
return nil, nil, nil, err
}
}
if pageSizeStr != "" {
pageSize, err = strconv.ParseUint(pageSizeStr, 10, 64)
if err != nil {
return nil, nil, nil, err
}
}
options = append(options, store.WithPaging(ascending, pageSize))
}
return m, query, options, nil
}
func writeStoreError(w http.ResponseWriter, code int, err error) {
writeResponse(w, StoreResponse{ErrorMessage: err.Error()}, code)
}
func toStoreResponse(result *store.Result) StoreResponse {
response := StoreResponse{}
cursor := result.Cursor()
if cursor != nil {
response.Cursor = &HistoryCursor{
PubsubTopic: cursor.PubsubTopic,
SenderTime: fmt.Sprintf("%d", cursor.SenderTime),
StoreTime: fmt.Sprintf("%d", cursor.ReceiverTime),
Digest: cursor.Digest,
}
}
for _, m := range result.Messages {
response.Messages = append(response.Messages, StoreWakuMessage{
Payload: m.Payload,
ContentTopic: m.ContentTopic,
Version: int32(m.Version),
Timestamp: m.Timestamp,
Meta: m.Meta,
})
}
return response
}
func (d *StoreService) getV1Messages(w http.ResponseWriter, r *http.Request) {
peerAddr, query, options, err := getStoreParams(r)
if err != nil {
writeStoreError(w, http.StatusBadRequest, err)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
_, err = d.node.AddPeer(peerAddr, peerstore.Static)
if err != nil {
writeStoreError(w, http.StatusInternalServerError, err)
return
}
result, err := d.node.Store().Query(ctx, *query, options...)
if err != nil {
writeStoreError(w, http.StatusInternalServerError, err)
return
}
writeErrOrResponse(w, nil, toStoreResponse(result))
}
@@ -1,6 +1,6 @@
openapi: 3.0.3
info:
title: Waku V2 node Store REST API
title: Waku V2 node REST API
version: 1.0.0
contact:
name: VAC Team
@@ -17,7 +17,6 @@ import (
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/utils"
"google.golang.org/protobuf/proto"
)
func TestGetMessages(t *testing.T) {
@@ -33,16 +32,16 @@ func TestGetMessages(t *testing.T) {
topic1 := "1"
pubsubTopic1 := "topic1"
now := *utils.GetUnixEpoch()
msg1 := tests.CreateWakuMessage(topic1, proto.Int64(now+1))
msg2 := tests.CreateWakuMessage(topic1, proto.Int64(now+2))
msg3 := tests.CreateWakuMessage(topic1, proto.Int64(now+3))
now := utils.GetUnixEpoch()
msg1 := tests.CreateWakuMessage(topic1, now+1)
msg2 := tests.CreateWakuMessage(topic1, now+2)
msg3 := tests.CreateWakuMessage(topic1, now+3)
node1.Broadcaster().Submit(protocol.NewEnvelope(msg1, *utils.GetUnixEpoch(), pubsubTopic1))
node1.Broadcaster().Submit(protocol.NewEnvelope(msg2, *utils.GetUnixEpoch(), pubsubTopic1))
node1.Broadcaster().Submit(protocol.NewEnvelope(msg3, *utils.GetUnixEpoch(), pubsubTopic1))
node1.Broadcaster().Submit(protocol.NewEnvelope(msg1, utils.GetUnixEpoch(), pubsubTopic1))
node1.Broadcaster().Submit(protocol.NewEnvelope(msg2, utils.GetUnixEpoch(), pubsubTopic1))
node1.Broadcaster().Submit(protocol.NewEnvelope(msg3, utils.GetUnixEpoch(), pubsubTopic1))
n1HostInfo, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", node1.Host().ID().String()))
n1HostInfo, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", node1.Host().ID().Pretty()))
n1Addr := node1.ListenAddresses()[0].Encapsulate(n1HostInfo)
node2, err := node.New()
@@ -52,7 +51,7 @@ func TestGetMessages(t *testing.T) {
defer node2.Stop()
router := chi.NewRouter()
_ = NewLegacyStoreService(node2, router)
_ = NewStoreService(node2, router)
// TEST: get cursor
// TEST: get no messages
@@ -64,12 +63,12 @@ func TestGetMessages(t *testing.T) {
"pubsubTopic": {pubsubTopic1},
"pageSize": {"2"},
}
path := routeLegacyStoreMessagesV1 + "?" + queryParams.Encode()
path := ROUTE_STORE_MESSAGESV1 + "?" + queryParams.Encode()
req, _ := http.NewRequest(http.MethodGet, path, nil)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
response := LegacyStoreResponse{}
response := StoreResponse{}
err = json.Unmarshal(rr.Body.Bytes(), &response)
require.NoError(t, err)
require.Len(t, response.Messages, 2)
@@ -84,12 +83,12 @@ func TestGetMessages(t *testing.T) {
"digest": {base64.URLEncoding.EncodeToString(response.Cursor.Digest)},
"pageSize": {"2"},
}
path = routeLegacyStoreMessagesV1 + "?" + queryParams.Encode()
path = ROUTE_STORE_MESSAGESV1 + "?" + queryParams.Encode()
req, _ = http.NewRequest(http.MethodGet, path, nil)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
response = LegacyStoreResponse{}
response = StoreResponse{}
err = json.Unmarshal(rr.Body.Bytes(), &response)
require.NoError(t, err)
require.Len(t, response.Messages, 1)
+44
View File
@@ -0,0 +1,44 @@
package rest
import (
"encoding/json"
"net/http"
)
func writeErrOrResponse(w http.ResponseWriter, err error, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
jsonResponse, err := json.Marshal(value)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(jsonResponse)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func writeResponse(w http.ResponseWriter, value interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
jsonResponse, err := json.Marshal(value)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(jsonResponse)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(code)
}
@@ -13,7 +13,7 @@ import (
func MemoryDB(t *testing.T) *persistence.DBStore {
var db *sql.DB
db, err := sqlite.NewDB(":memory:", utils.Logger())
db, err := sqlite.NewDB(":memory:", false, utils.Logger())
require.NoError(t, err)
dbStore, err := persistence.NewDBStore(prometheus.DefaultRegisterer, utils.Logger(), persistence.WithDB(db), persistence.WithMigrations(sqlite.Migrations))
+77
View File
@@ -0,0 +1,77 @@
package rest
import (
"context"
"fmt"
"net/http"
"sync"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/waku-org/go-waku/waku/v2/node"
"go.uber.org/zap"
)
type WakuRest struct {
node *node.WakuNode
server *http.Server
log *zap.Logger
relayService *RelayService
}
func NewWakuRest(node *node.WakuNode, address string, port int, enableAdmin bool, enablePProf bool, relayCacheCapacity int, log *zap.Logger) *WakuRest {
wrpc := new(WakuRest)
wrpc.log = log.Named("rest")
mux := chi.NewRouter()
mux.Use(middleware.Logger)
mux.Use(middleware.NoCache)
if enablePProf {
mux.Mount("/debug", middleware.Profiler())
}
_ = NewDebugService(node, mux)
_ = NewStoreService(node, mux)
listenAddr := fmt.Sprintf("%s:%d", address, port)
server := &http.Server{
Addr: listenAddr,
Handler: mux,
}
wrpc.node = node
wrpc.server = server
if node.Relay() != nil {
relayService := NewRelayService(node, mux, relayCacheCapacity, log)
server.RegisterOnShutdown(func() {
relayService.Stop()
})
wrpc.relayService = relayService
}
return wrpc
}
func (r *WakuRest) Start(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
if r.node.Relay() != nil {
go r.relayService.Start(ctx)
}
go func() {
_ = r.server.ListenAndServe()
}()
r.log.Info("server started", zap.String("addr", r.server.Addr))
}
func (r *WakuRest) Stop(ctx context.Context) error {
r.log.Info("shutting down server")
return r.server.Shutdown(ctx)
}
@@ -13,7 +13,7 @@ func TestWakuRest(t *testing.T) {
n, err := node.New(options)
require.NoError(t, err)
rpc := NewWakuRest(n, RestConfig{Address: "127.0.0.1", Port: 8080, EnablePProf: false, EnableAdmin: false, RelayCacheCapacity: 10}, utils.Logger())
rpc := NewWakuRest(n, "127.0.0.1", 8080, true, false, 10, utils.Logger())
require.NotNil(t, rpc.server)
require.Equal(t, rpc.server.Addr, "127.0.0.1:8080")
}
+2 -2
View File
@@ -1,5 +1,5 @@
//go:build gowaku_no_rln
// +build gowaku_no_rln
//go:build !gowaku_rln
// +build !gowaku_rln
package rlngenerate
+7 -7
View File
@@ -1,5 +1,5 @@
//go:build !gowaku_no_rln
// +build !gowaku_no_rln
//go:build gowaku_rln
// +build gowaku_rln
package rlngenerate
@@ -77,14 +77,14 @@ func execute(ctx context.Context) error {
if logger.Level() == zap.DebugLevel {
logger.Info("registered credentials into the membership contract",
logging.HexBytes("IDCommitment", identityCredential.IDCommitment[:]),
logging.HexBytes("IDNullifier", identityCredential.IDNullifier[:]),
logging.HexBytes("IDSecretHash", identityCredential.IDSecretHash[:]),
logging.HexBytes("IDTrapDoor", identityCredential.IDTrapdoor[:]),
logging.HexString("IDCommitment", identityCredential.IDCommitment[:]),
logging.HexString("IDNullifier", identityCredential.IDNullifier[:]),
logging.HexString("IDSecretHash", identityCredential.IDSecretHash[:]),
logging.HexString("IDTrapDoor", identityCredential.IDTrapdoor[:]),
zap.Uint("index", membershipIndex),
)
} else {
logger.Info("registered credentials into the membership contract", logging.HexBytes("idCommitment", identityCredential.IDCommitment[:]), zap.Uint("index", membershipIndex))
logger.Info("registered credentials into the membership contract", logging.HexString("idCommitment", identityCredential.IDCommitment[:]), zap.Uint("index", membershipIndex))
}
web3Config.ETHClient.Close()
+4 -3
View File
@@ -1,18 +1,19 @@
//go:build !gowaku_no_rln
// +build !gowaku_no_rln
//go:build gowaku_rln
// +build gowaku_rln
package rlngenerate
import (
cli "github.com/urfave/cli/v2"
wcli "github.com/waku-org/go-waku/waku/cliutils"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore"
)
var flags = []cli.Flag{
&cli.PathFlag{
Name: "cred-path",
Usage: "RLN relay membership credentials file",
Value: "./rlnKeystore.json",
Value: keystore.DefaultCredentialsFilename,
Destination: &options.CredentialsPath,
},
&cli.StringFlag{
+3 -3
View File
@@ -1,5 +1,5 @@
//go:build !gowaku_no_rln
// +build !gowaku_no_rln
//go:build gowaku_rln
// +build gowaku_rln
package rlngenerate
@@ -124,7 +124,7 @@ func register(ctx context.Context, web3Config *web3.Config, idComm rln.IDCommitm
var eventIDComm rln.IDCommitment = rln.BigIntToBytes32(evt.IdCommitment)
log.Debug("information extracted from tx log", zap.Uint64("blockNumber", evt.Raw.BlockNumber), logging.HexBytes("idCommitment", eventIDComm[:]), zap.Uint64("index", evt.Index.Uint64()))
log.Debug("information extracted from tx log", zap.Uint64("blockNumber", evt.Raw.BlockNumber), logging.HexString("idCommitment", eventIDComm[:]), zap.Uint64("index", evt.Index.Uint64()))
if eventIDComm != idComm {
return 0, errors.New("invalid id commitment key")
+87
View File
@@ -0,0 +1,87 @@
package rpc
import (
"net/http"
"github.com/libp2p/go-libp2p/core/protocol"
ma "github.com/multiformats/go-multiaddr"
"go.uber.org/zap"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/filter"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_filter"
"github.com/waku-org/go-waku/waku/v2/protocol/lightpush"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/protocol/store"
)
type AdminService struct {
node *node.WakuNode
log *zap.Logger
}
type GetPeersArgs struct {
}
type PeersArgs struct {
Peers []string `json:"peers,omitempty"`
}
type PeerReply struct {
Multiaddr string `json:"multiaddr,omitempty"`
Protocol protocol.ID `json:"protocol,omitempty"`
Connected bool `json:"connected,omitempty"`
}
type PeersReply []PeerReply
func (a *AdminService) PostV1Peers(req *http.Request, args *PeersArgs, reply *SuccessReply) error {
for _, peer := range args.Peers {
addr, err := ma.NewMultiaddr(peer)
if err != nil {
a.log.Error("building multiaddr", zap.Error(err))
return err
}
err = a.node.DialPeerWithMultiAddress(req.Context(), addr)
if err != nil {
a.log.Error("dialing peers", zap.Error(err))
return err
}
}
*reply = true
return nil
}
func isWakuProtocol(protocol protocol.ID) bool {
return protocol == legacy_filter.FilterID_v20beta1 ||
protocol == filter.FilterPushID_v20beta1 ||
protocol == filter.FilterSubscribeID_v20beta1 ||
protocol == relay.WakuRelayID_v200 ||
protocol == lightpush.LightPushID_v20beta1 ||
protocol == store.StoreID_v20beta4
}
func (a *AdminService) GetV1Peers(req *http.Request, args *GetPeersArgs, reply *PeersReply) error {
peers, err := a.node.Peers()
if err != nil {
a.log.Error("getting peers", zap.Error(err))
return nil
}
for _, peer := range peers {
for _, addr := range peer.Addrs {
for _, proto := range peer.Protocols {
if !isWakuProtocol(proto) {
continue
}
*reply = append(*reply, PeerReply{
Multiaddr: addr.String(),
Protocol: proto,
Connected: peer.Connected,
})
}
}
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
package rpc
import (
"bytes"
"context"
"crypto/rand"
"fmt"
"net/http"
"testing"
"time"
"github.com/multiformats/go-multiaddr"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/tests"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/timesource"
"github.com/waku-org/go-waku/waku/v2/utils"
)
func makeAdminService(t *testing.T) *AdminService {
options := node.WithWakuRelay()
n, err := node.New(options)
require.NoError(t, err)
err = n.Start(context.Background())
require.NoError(t, err)
return &AdminService{n, utils.Logger()}
}
func TestV1Peers(t *testing.T) {
port, err := tests.FindFreePort(t, "", 5)
require.NoError(t, err)
host, err := tests.MakeHost(context.Background(), port, rand.Reader)
require.NoError(t, err)
relay := relay.NewWakuRelay(nil, 0, timesource.NewDefaultClock(), prometheus.DefaultRegisterer, utils.Logger())
relay.SetHost(host)
err = relay.Start(context.Background())
require.NoError(t, err)
defer relay.Stop()
var reply PeersReply
request, err := http.NewRequest(http.MethodPost, "url", bytes.NewReader([]byte("")))
require.NoError(t, err)
a := makeAdminService(t)
err = a.GetV1Peers(request, &GetPeersArgs{}, &reply)
require.NoError(t, err)
require.Len(t, reply, 0)
var reply2 SuccessReply
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", host.ID().Pretty()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range host.Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
err = a.PostV1Peers(request, &PeersArgs{Peers: []string{addr.String()}}, &reply2)
require.NoError(t, err)
require.True(t, reply2)
time.Sleep(2 * time.Second)
err = a.GetV1Peers(request, &GetPeersArgs{}, &reply)
require.NoError(t, err)
require.Len(t, reply, 1)
}
+195
View File
@@ -0,0 +1,195 @@
package rpc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/gorilla/rpc/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// Based on github.com/gorilla/rpc/v2/json which is governed by a BSD-style license
var null = json.RawMessage([]byte("null"))
// An Error is a wrapper for a JSON interface value. It can be used by either
// a service's handler func to write more complex JSON data to an error field
// of a server's response, or by a client to read it.
type Error struct {
Data interface{}
}
func (e *Error) Error() string {
return fmt.Sprintf("%v", e.Data)
}
// ----------------------------------------------------------------------------
// Request and Response
// ----------------------------------------------------------------------------
// serverRequest represents a JSON-RPC request received by the server.
type serverRequest struct {
// A String containing the name of the method to be invoked.
Method string `json:"method"`
// An Array of objects to pass as arguments to the method.
Params *json.RawMessage `json:"params"`
// The request id. This can be of any type. It is used to match the
// response with the request that it is replying to.
Id *json.RawMessage `json:"id"`
}
// serverResponse represents a JSON-RPC response returned by the server.
type serverResponse struct {
// The Object that was returned by the invoked method. This must be null
// in case there was an error invoking the method.
Result interface{} `json:"result"`
// An Error object if there was an error invoking the method. It must be
// null if there was no error.
Error interface{} `json:"error"`
// This must be the same id as the request it is responding to.
Id *json.RawMessage `json:"id"`
}
// ----------------------------------------------------------------------------
// Codec
// ----------------------------------------------------------------------------
// NewCodec returns a new SnakeCaseCodec Codec.
func NewSnakeCaseCodec() *SnakeCaseCodec {
return &SnakeCaseCodec{}
}
// SnakeCaseCodec creates a CodecRequest to process each request.
type SnakeCaseCodec struct {
}
// NewRequest returns a CodecRequest.
func (c *SnakeCaseCodec) NewRequest(r *http.Request) rpc.CodecRequest {
return newCodecRequest(r)
}
// ----------------------------------------------------------------------------
// CodecRequest
// ----------------------------------------------------------------------------
// newCodecRequest returns a new CodecRequest.
func newCodecRequest(r *http.Request) rpc.CodecRequest {
// Decode the request body and check if RPC method is valid.
req := new(serverRequest)
err := json.NewDecoder(r.Body).Decode(req)
r.Body.Close()
return &CodecRequest{request: req, err: err}
}
// CodecRequest decodes and encodes a single request.
type CodecRequest struct {
request *serverRequest
err error
}
// Method returns the RPC method for the current request.
//
// The method uses a dotted notation as in "Service.Method".
func (c *CodecRequest) Method() (string, error) {
if c.err == nil {
return toWakuMethod(c.request.Method), nil
}
return "", c.err
}
// toWakuMethod transform get_waku_v2_debug_v1_info to Debug.GetV1Info
func toWakuMethod(input string) string {
typ := "get"
if strings.HasPrefix(input, "post") {
typ = "post"
} else if strings.HasPrefix(input, "delete") {
typ = "delete"
}
base := typ + "_waku_v2_"
cleanedInput := strings.Replace(input, base, "", 1)
splited := strings.Split(cleanedInput, "_")
c := cases.Title(language.AmericanEnglish)
method := c.String(typ)
for _, val := range splited[1:] {
method = method + c.String(val)
}
return c.String(splited[0]) + "." + method
}
// ReadRequest fills the request object for the RPC method.
func (c *CodecRequest) ReadRequest(args interface{}) error {
if c.err == nil {
if c.request.Params != nil {
// JSON params is array value. RPC params is struct.
// Attempt to unmarshal into array containing the request struct.
params := [1]interface{}{args}
err := json.Unmarshal(*c.request.Params, &params)
if err != nil {
// This failed so we might have received an array of parameters
// instead of a object
argsValueOf := reflect.Indirect(reflect.ValueOf(args))
if argsValueOf.Kind() == reflect.Struct {
var params []interface{}
for i := 0; i < argsValueOf.NumField(); i++ {
params = append(params, argsValueOf.Field(i).Addr().Interface())
}
c.err = json.Unmarshal(*c.request.Params, &params)
} else {
// Unknown field type...
c.err = err
}
}
} else {
c.err = errors.New("rpc: method request ill-formed: missing params field")
}
}
return c.err
}
// WriteResponse encodes the response and writes it to the ResponseWriter.
func (c *CodecRequest) WriteResponse(w http.ResponseWriter, reply interface{}) {
if c.request.Id != nil {
// Id is null for notifications and they don't have a response.
res := &serverResponse{
Result: reply,
Error: &null,
Id: c.request.Id,
}
c.writeServerResponse(w, 200, res)
}
}
func (c *CodecRequest) WriteError(w http.ResponseWriter, _ int, err error) {
res := &serverResponse{
Result: &null,
Id: c.request.Id,
}
if jsonErr, ok := err.(*Error); ok {
res.Error = jsonErr.Data
} else {
res.Error = err.Error()
}
c.writeServerResponse(w, 400, res)
}
func (c *CodecRequest) writeServerResponse(w http.ResponseWriter, status int, res *serverResponse) {
b, err := json.Marshal(res)
if err == nil {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
_, _ = w.Write(b)
} else {
// Not sure in which case will this happen. But seems harmless.
rpc.WriteError(w, status, err.Error())
}
}
+18
View File
@@ -0,0 +1,18 @@
package rpc
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestConvertWakuMethod(t *testing.T) {
res := toWakuMethod("get_waku_v2_debug_v1_info")
require.Equal(t, "Debug.GetV1Info", res)
res = toWakuMethod("post_waku_v2_relay_v1_message")
require.Equal(t, "Relay.PostV1Message", res)
res = toWakuMethod("delete_waku_v2_relay_v1_subscriptions")
require.Equal(t, "Relay.DeleteV1Subscriptions", res)
}
+40
View File
@@ -0,0 +1,40 @@
package rpc
import (
"net/http"
"github.com/waku-org/go-waku/waku/v2/node"
)
type DebugService struct {
node *node.WakuNode
}
type InfoArgs struct {
}
type InfoReply struct {
ENRUri string `json:"enrUri,omitempty"`
ListenAddresses []string `json:"listenAddresses,omitempty"`
}
func NewDebugService(node *node.WakuNode) *DebugService {
return &DebugService{
node: node,
}
}
func (d *DebugService) GetV1Info(r *http.Request, args *InfoArgs, reply *InfoReply) error {
reply.ENRUri = d.node.ENR().String()
for _, addr := range d.node.ListenAddresses() {
reply.ListenAddresses = append(reply.ListenAddresses, addr.String())
}
return nil
}
type VersionResponse string
func (d *DebugService) GetV1Version(r *http.Request, args *InfoArgs, reply *VersionResponse) error {
*reply = VersionResponse(node.GetVersionInfo().String())
return nil
}
+31
View File
@@ -0,0 +1,31 @@
package rpc
import (
"bytes"
"context"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/waku/v2/node"
)
func TestGetV1Info(t *testing.T) {
var reply InfoReply
request, err := http.NewRequest(http.MethodPost, "url", bytes.NewReader([]byte("")))
require.NoError(t, err)
wakuNode1, err := node.New()
require.NoError(t, err)
defer wakuNode1.Stop()
err = wakuNode1.Start(context.Background())
require.NoError(t, err)
d := &DebugService{
node: wakuNode1,
}
err = d.GetV1Info(request, &InfoArgs{}, &reply)
require.NoError(t, err)
}
+133
View File
@@ -0,0 +1,133 @@
package rpc
import (
"fmt"
"net/http"
"sync"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_filter"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_filter/pb"
wpb "github.com/waku-org/go-waku/waku/v2/protocol/pb"
"go.uber.org/zap"
)
type FilterService struct {
node *node.WakuNode
log *zap.Logger
messages map[string][]*wpb.WakuMessage
cacheCapacity int
messagesMutex sync.RWMutex
runner *runnerService
}
type FilterContentArgs struct {
Topic string `json:"topic,omitempty"`
ContentFilters []*pb.FilterRequest_ContentFilter `json:"contentFilters,omitempty"`
}
type ContentTopicArgs struct {
ContentTopic string `json:"contentTopic,omitempty"`
}
func NewFilterService(node *node.WakuNode, cacheCapacity int, log *zap.Logger) *FilterService {
s := &FilterService{
node: node,
log: log.Named("filter"),
cacheCapacity: cacheCapacity,
messages: make(map[string][]*wpb.WakuMessage),
}
s.runner = newRunnerService(node.Broadcaster(), s.addEnvelope)
return s
}
func makeContentFilter(args *FilterContentArgs) legacy_filter.ContentFilter {
var contentTopics []string
for _, contentFilter := range args.ContentFilters {
contentTopics = append(contentTopics, contentFilter.ContentTopic)
}
return legacy_filter.ContentFilter{
Topic: args.Topic,
ContentTopics: contentTopics,
}
}
func (f *FilterService) addEnvelope(envelope *protocol.Envelope) {
f.messagesMutex.Lock()
defer f.messagesMutex.Unlock()
contentTopic := envelope.Message().ContentTopic
if _, ok := f.messages[contentTopic]; !ok {
return
}
// Keep a specific max number of messages per topic
if len(f.messages[envelope.PubsubTopic()]) >= f.cacheCapacity {
f.messages[envelope.PubsubTopic()] = f.messages[envelope.PubsubTopic()][1:]
}
f.messages[contentTopic] = append(f.messages[contentTopic], envelope.Message())
}
func (f *FilterService) Start() {
f.runner.Start()
}
func (f *FilterService) Stop() {
f.runner.Stop()
}
func (f *FilterService) PostV1Subscription(req *http.Request, args *FilterContentArgs, reply *SuccessReply) error {
_, _, err := f.node.LegacyFilter().Subscribe(
req.Context(),
makeContentFilter(args),
legacy_filter.WithAutomaticPeerSelection(),
)
if err != nil {
f.log.Error("subscribing to topic", zap.String("topic", args.Topic), zap.Error(err))
return err
}
for _, contentFilter := range args.ContentFilters {
f.messages[contentFilter.ContentTopic] = make([]*wpb.WakuMessage, 0)
}
*reply = true
return nil
}
func (f *FilterService) DeleteV1Subscription(req *http.Request, args *FilterContentArgs, reply *SuccessReply) error {
err := f.node.LegacyFilter().UnsubscribeFilter(
req.Context(),
makeContentFilter(args),
)
if err != nil {
f.log.Error("unsubscribing from topic", zap.String("topic", args.Topic), zap.Error(err))
return err
}
for _, contentFilter := range args.ContentFilters {
delete(f.messages, contentFilter.ContentTopic)
}
*reply = true
return nil
}
func (f *FilterService) GetV1Messages(req *http.Request, args *ContentTopicArgs, reply *MessagesReply) error {
f.messagesMutex.Lock()
defer f.messagesMutex.Unlock()
if _, ok := f.messages[args.ContentTopic]; !ok {
return fmt.Errorf("topic %s not subscribed", args.ContentTopic)
}
for i := range f.messages[args.ContentTopic] {
*reply = append(*reply, ProtoToRPC(f.messages[args.ContentTopic][i]))
}
f.messages[args.ContentTopic] = make([]*wpb.WakuMessage, 0)
return nil
}
+172
View File
@@ -0,0 +1,172 @@
package rpc
import (
"context"
"crypto/rand"
"fmt"
"testing"
"time"
"github.com/multiformats/go-multiaddr"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/tests"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/peerstore"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_filter"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_filter/pb"
wpb "github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/timesource"
"github.com/waku-org/go-waku/waku/v2/utils"
)
var testTopic = "test"
func makeFilterService(t *testing.T, isFullNode bool) *FilterService {
var nodeOpts []node.WakuNodeOption
nodeOpts = append(nodeOpts, node.WithLegacyWakuFilter(isFullNode))
if isFullNode {
nodeOpts = append(nodeOpts, node.WithWakuRelay())
}
n, err := node.New(nodeOpts...)
require.NoError(t, err)
err = n.Start(context.Background())
require.NoError(t, err)
if isFullNode {
sub, err := n.Relay().SubscribeToTopic(context.Background(), testTopic)
go func() {
for range sub.Ch {
}
}()
require.NoError(t, err)
}
return NewFilterService(n, 30, utils.Logger())
}
func TestFilterSubscription(t *testing.T) {
port, err := tests.FindFreePort(t, "", 5)
require.NoError(t, err)
host, err := tests.MakeHost(context.Background(), port, rand.Reader)
require.NoError(t, err)
b := relay.NewBroadcaster(10)
require.NoError(t, b.Start(context.Background()))
node := relay.NewWakuRelay(b, 0, timesource.NewDefaultClock(), prometheus.DefaultRegisterer, utils.Logger())
node.SetHost(host)
err = node.Start(context.Background())
require.NoError(t, err)
_, err = node.SubscribeToTopic(context.Background(), testTopic)
require.NoError(t, err)
b2 := relay.NewBroadcaster(10)
require.NoError(t, b2.Start(context.Background()))
f := legacy_filter.NewWakuFilter(b2, false, timesource.NewDefaultClock(), prometheus.DefaultRegisterer, utils.Logger())
f.SetHost(host)
err = f.Start(context.Background(), relay.NoopSubscription())
require.NoError(t, err)
d := makeFilterService(t, true)
defer d.node.Stop()
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", host.ID().Pretty()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range host.Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
_, err = d.node.AddPeer(addr, peerstore.Static, legacy_filter.FilterID_v20beta1)
require.NoError(t, err)
args := &FilterContentArgs{Topic: testTopic, ContentFilters: []*pb.FilterRequest_ContentFilter{{ContentTopic: "ct"}}}
var reply SuccessReply
err = d.PostV1Subscription(
makeRequest(t),
args,
&reply,
)
require.NoError(t, err)
require.True(t, reply)
err = d.DeleteV1Subscription(
makeRequest(t),
args,
&reply,
)
require.NoError(t, err)
require.True(t, reply)
}
func TestFilterGetV1Messages(t *testing.T) {
serviceA := makeFilterService(t, true)
var reply SuccessReply
serviceB := makeFilterService(t, false)
go serviceB.Start()
defer serviceB.Stop()
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", serviceB.node.Host().ID().Pretty()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range serviceB.node.Host().Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
err = serviceA.node.DialPeerWithMultiAddress(context.Background(), addr)
require.NoError(t, err)
// Wait for the dial to complete
time.Sleep(1 * time.Second)
args := &FilterContentArgs{Topic: testTopic, ContentFilters: []*pb.FilterRequest_ContentFilter{{ContentTopic: "ct"}}}
err = serviceB.PostV1Subscription(
makeRequest(t),
args,
&reply,
)
require.NoError(t, err)
require.True(t, reply)
// Wait for the subscription to be started
time.Sleep(1 * time.Second)
_, err = serviceA.node.Relay().PublishToTopic(
context.Background(),
&wpb.WakuMessage{ContentTopic: "ct"},
testTopic,
)
require.NoError(t, err)
require.True(t, reply)
// Wait for the message to be received
time.Sleep(1 * time.Second)
var messagesReply1 MessagesReply
err = serviceB.GetV1Messages(
makeRequest(t),
&ContentTopicArgs{"ct"},
&messagesReply1,
)
require.NoError(t, err)
require.Len(t, messagesReply1, 1)
var messagesReply2 MessagesReply
err = serviceB.GetV1Messages(
makeRequest(t),
&ContentTopicArgs{"ct"},
&messagesReply2,
)
require.NoError(t, err)
require.Len(t, messagesReply2, 0)
}
+163
View File
@@ -0,0 +1,163 @@
package rpc
import (
"fmt"
"net/http"
"sync"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"go.uber.org/zap"
)
type RelayService struct {
node *node.WakuNode
log *zap.Logger
messages map[string][]*pb.WakuMessage
cacheCapacity int
messagesMutex sync.RWMutex
runner *runnerService
}
type RelayMessageArgs struct {
Topic string `json:"topic,omitempty"`
Message *RPCWakuMessage `json:"message,omitempty"`
}
type TopicsArgs struct {
Topics []string `json:"topics,omitempty"`
}
type TopicArgs struct {
Topic string `json:"topic,omitempty"`
}
func NewRelayService(node *node.WakuNode, cacheCapacity int, log *zap.Logger) *RelayService {
s := &RelayService{
node: node,
cacheCapacity: cacheCapacity,
log: log.Named("relay"),
messages: make(map[string][]*pb.WakuMessage),
}
s.runner = newRunnerService(node.Broadcaster(), s.addEnvelope)
return s
}
func (r *RelayService) addEnvelope(envelope *protocol.Envelope) {
r.messagesMutex.Lock()
defer r.messagesMutex.Unlock()
if _, ok := r.messages[envelope.PubsubTopic()]; !ok {
return
}
// Keep a specific max number of messages per topic
if len(r.messages[envelope.PubsubTopic()]) >= r.cacheCapacity {
r.messages[envelope.PubsubTopic()] = r.messages[envelope.PubsubTopic()][1:]
}
r.messages[envelope.PubsubTopic()] = append(r.messages[envelope.PubsubTopic()], envelope.Message())
}
func (r *RelayService) Start() {
r.messagesMutex.Lock()
// Node may already be subscribed to some topics when Relay API handlers are installed. Let's add these
for _, topic := range r.node.Relay().Topics() {
r.log.Info("adding topic handler for existing subscription", zap.String("topic", topic))
r.messages[topic] = make([]*pb.WakuMessage, 0)
}
r.messagesMutex.Unlock()
r.runner.Start()
}
func (r *RelayService) Stop() {
r.runner.Stop()
}
func (r *RelayService) PostV1Message(req *http.Request, args *RelayMessageArgs, reply *SuccessReply) error {
var err error
if args.Topic == "" {
_, err = r.node.Relay().Publish(req.Context(), args.Message.toProto())
} else {
_, err = r.node.Relay().PublishToTopic(req.Context(), args.Message.toProto(), args.Topic)
}
if err != nil {
r.log.Error("publishing message", zap.Error(err))
return err
}
*reply = true
return nil
}
func (r *RelayService) PostV1Subscription(req *http.Request, args *TopicsArgs, reply *SuccessReply) error {
ctx := req.Context()
for _, topic := range args.Topics {
var err error
if topic == "" {
var sub *relay.Subscription
sub, err = r.node.Relay().Subscribe(ctx)
sub.Unsubscribe()
} else {
var sub *relay.Subscription
sub, err = r.node.Relay().SubscribeToTopic(ctx, topic)
if err != nil {
r.log.Error("subscribing to topic", zap.String("topic", topic), zap.Error(err))
return err
}
sub.Unsubscribe()
}
if err != nil {
r.log.Error("subscribing to topic", zap.String("topic", topic), zap.Error(err))
return err
}
r.messagesMutex.Lock()
r.messages[topic] = make([]*pb.WakuMessage, 0)
r.messagesMutex.Unlock()
}
*reply = true
return nil
}
func (r *RelayService) DeleteV1Subscription(req *http.Request, args *TopicsArgs, reply *SuccessReply) error {
ctx := req.Context()
for _, topic := range args.Topics {
err := r.node.Relay().Unsubscribe(ctx, topic)
if err != nil {
r.log.Error("unsubscribing from topic", zap.String("topic", topic), zap.Error(err))
return err
}
delete(r.messages, topic)
}
*reply = true
return nil
}
func (r *RelayService) GetV1Messages(req *http.Request, args *TopicArgs, reply *MessagesReply) error {
r.messagesMutex.Lock()
defer r.messagesMutex.Unlock()
if _, ok := r.messages[args.Topic]; !ok {
return fmt.Errorf("topic %s not subscribed", args.Topic)
}
for i := range r.messages[args.Topic] {
*reply = append(*reply, ProtoToRPC(r.messages[args.Topic][i]))
}
r.messages[args.Topic] = make([]*pb.WakuMessage, 0)
return nil
}
+142
View File
@@ -0,0 +1,142 @@
package rpc
import (
"context"
"fmt"
"testing"
"time"
"github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/utils"
)
func makeRelayService(t *testing.T) *RelayService {
options := node.WithWakuRelayAndMinPeers(0)
n, err := node.New(options)
require.NoError(t, err)
err = n.Start(context.Background())
require.NoError(t, err)
return NewRelayService(n, 30, utils.Logger())
}
func TestPostV1Message(t *testing.T) {
var reply SuccessReply
d := makeRelayService(t)
msg := &pb.WakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "abc",
Version: 0,
Timestamp: utils.GetUnixEpoch(),
}
err := d.PostV1Message(
makeRequest(t),
&RelayMessageArgs{
Message: ProtoToRPC(msg),
},
&reply,
)
require.NoError(t, err)
require.True(t, reply)
}
func TestRelaySubscription(t *testing.T) {
var reply SuccessReply
d := makeRelayService(t)
args := &TopicsArgs{Topics: []string{"test"}}
err := d.PostV1Subscription(
makeRequest(t),
args,
&reply,
)
require.NoError(t, err)
require.True(t, reply)
err = d.DeleteV1Subscription(
makeRequest(t),
args,
&reply,
)
require.NoError(t, err)
require.True(t, reply)
}
func TestRelayGetV1Messages(t *testing.T) {
serviceA := makeRelayService(t)
go serviceA.Start()
defer serviceA.Stop()
var reply SuccessReply
serviceB := makeRelayService(t)
go serviceB.Start()
defer serviceB.Stop()
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", serviceB.node.Host().ID().Pretty()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range serviceB.node.Host().Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
err = serviceA.node.DialPeerWithMultiAddress(context.Background(), addr)
require.NoError(t, err)
// Wait for the dial to complete
time.Sleep(1 * time.Second)
args := &TopicsArgs{Topics: []string{"test"}}
err = serviceB.PostV1Subscription(
makeRequest(t),
args,
&reply,
)
require.NoError(t, err)
require.True(t, reply)
// Wait for the subscription to be started
time.Sleep(1 * time.Second)
err = serviceA.PostV1Message(
makeRequest(t),
&RelayMessageArgs{
Topic: "test",
Message: ProtoToRPC(&pb.WakuMessage{
Payload: []byte("test"),
}),
},
&reply,
)
require.NoError(t, err)
require.True(t, reply)
// Wait for the message to be received
time.Sleep(1 * time.Second)
var messagesReply1 MessagesReply
err = serviceB.GetV1Messages(
makeRequest(t),
&TopicArgs{"test"},
&messagesReply1,
)
require.NoError(t, err)
require.Len(t, messagesReply1, 1)
var messagesReply2 MessagesReply
err = serviceB.GetV1Messages(
makeRequest(t),
&TopicArgs{"test"},
&messagesReply2,
)
require.NoError(t, err)
require.Len(t, messagesReply2, 0)
}
+41
View File
@@ -0,0 +1,41 @@
package rpc
import (
"encoding/base64"
"strings"
)
type SuccessReply = bool
type Empty struct {
}
type MessagesReply = []*RPCWakuMessage
type Base64URLByte []byte
// UnmarshalText is used by json.Unmarshal to decode both url-safe and standard
// base64 encoded strings with and without padding
func (h *Base64URLByte) UnmarshalText(b []byte) error {
inputValue := ""
if b != nil {
inputValue = string(b)
}
enc := base64.StdEncoding
if strings.ContainsAny(inputValue, "-_") {
enc = base64.URLEncoding
}
if len(inputValue)%4 != 0 {
enc = enc.WithPadding(base64.NoPadding)
}
decodedBytes, err := enc.DecodeString(inputValue)
if err != nil {
return err
}
*h = decodedBytes
return nil
}
+32
View File
@@ -0,0 +1,32 @@
package rpc
import (
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
)
type Adder func(msg *protocol.Envelope)
type runnerService struct {
broadcaster relay.Broadcaster
sub relay.Subscription
adder Adder
}
func newRunnerService(broadcaster relay.Broadcaster, adder Adder) *runnerService {
return &runnerService{
broadcaster: broadcaster,
adder: adder,
}
}
func (r *runnerService) Start() {
r.sub = r.broadcaster.RegisterForAll(1024)
for envelope := range r.sub.Ch {
r.adder(envelope)
}
}
func (r *runnerService) Stop() {
r.sub.Unsubscribe()
}
+75
View File
@@ -0,0 +1,75 @@
package rpc
import (
"net/http"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/store"
"github.com/waku-org/go-waku/waku/v2/protocol/store/pb"
"go.uber.org/zap"
)
type StoreService struct {
node *node.WakuNode
log *zap.Logger
}
// cursor *pb.Index
// pageSize uint64
// asc bool
type StorePagingOptions struct {
PageSize uint64 `json:"pageSize,omitempty"`
Cursor *pb.Index `json:"cursor,omitempty"`
Forward bool `json:"forward,omitempty"`
}
type StoreMessagesArgs struct {
Topic string `json:"pubsubTopic,omitempty"`
ContentFilters []string `json:"contentFilters,omitempty"`
StartTime int64 `json:"startTime,omitempty"`
EndTime int64 `json:"endTime,omitempty"`
PagingOptions StorePagingOptions `json:"pagingOptions,omitempty"`
}
type StoreMessagesReply struct {
Messages []*RPCWakuMessage `json:"messages,omitempty"`
PagingInfo StorePagingOptions `json:"pagingInfo,omitempty"`
Error string `json:"error,omitempty"`
}
func (s *StoreService) GetV1Messages(req *http.Request, args *StoreMessagesArgs, reply *StoreMessagesReply) error {
options := []store.HistoryRequestOption{
store.WithAutomaticRequestID(),
store.WithAutomaticPeerSelection(),
store.WithPaging(args.PagingOptions.Forward, args.PagingOptions.PageSize),
store.WithCursor(args.PagingOptions.Cursor),
}
res, err := s.node.Store().Query(
req.Context(),
store.Query{
Topic: args.Topic,
ContentTopics: args.ContentFilters,
StartTime: args.StartTime,
EndTime: args.EndTime,
},
options...,
)
if err != nil {
s.log.Error("querying messages", zap.Error(err))
reply.Error = err.Error()
return nil
}
reply.Messages = make([]*RPCWakuMessage, len(res.Messages))
for i := range res.Messages {
reply.Messages[i] = ProtoToRPC(res.Messages[i])
}
reply.PagingInfo = StorePagingOptions{
PageSize: args.PagingOptions.PageSize,
Cursor: res.Cursor(),
Forward: args.PagingOptions.Forward,
}
return nil
}
+33
View File
@@ -0,0 +1,33 @@
package rpc
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/utils"
)
func makeStoreService(t *testing.T) *StoreService {
options := node.WithWakuStore()
n, err := node.New(options)
require.NoError(t, err)
err = n.Start(context.Background())
require.NoError(t, err)
return &StoreService{n, utils.Logger()}
}
func TestStoreGetV1Messages(t *testing.T) {
var reply StoreMessagesReply
s := makeStoreService(t)
err := s.GetV1Messages(
makeRequest(t),
&StoreMessagesArgs{},
&reply,
)
require.NoError(t, err)
require.NotEmpty(t, reply.Error)
}
+39
View File
@@ -0,0 +1,39 @@
package rpc
import (
"bytes"
"encoding/base64"
"encoding/json"
"net/http"
"testing"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
)
func makeRequest(t *testing.T) *http.Request {
request, err := http.NewRequest(http.MethodPost, "url", bytes.NewReader([]byte("")))
require.NoError(t, err)
return request
}
func TestBase64Encoding(t *testing.T) {
input := "Hello World"
rpcMsg := ProtoToRPC(&pb.WakuMessage{
Payload: []byte(input),
})
jsonBytes, err := json.Marshal(rpcMsg)
require.NoError(t, err)
m := make(map[string]interface{})
err = json.Unmarshal(jsonBytes, &m)
require.NoError(t, err)
require.Equal(t, base64.StdEncoding.EncodeToString([]byte(input)), m["payload"])
decodedRpcMsg := new(RPCWakuMessage)
err = json.Unmarshal(jsonBytes, decodedRpcMsg)
require.NoError(t, err)
require.Equal(t, input, string(decodedRpcMsg.Payload))
}
+80
View File
@@ -0,0 +1,80 @@
package rpc
import (
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
)
type RateLimitProof struct {
Proof Base64URLByte `json:"proof,omitempty"`
MerkleRoot Base64URLByte `json:"merkle_root,omitempty"`
Epoch Base64URLByte `json:"epoch,omitempty"`
ShareX Base64URLByte `json:"share_x,omitempty"`
ShareY Base64URLByte `json:"share_y,omitempty"`
Nullifier Base64URLByte `json:"nullifier,omitempty"`
RlnIdentifier Base64URLByte `json:"rln_identifier,omitempty"`
}
type RPCWakuMessage struct {
Payload Base64URLByte `json:"payload,omitempty"`
ContentTopic string `json:"contentTopic,omitempty"`
Version uint32 `json:"version"`
Timestamp int64 `json:"timestamp,omitempty"`
RateLimitProof *RateLimitProof `json:"rateLimitProof,omitempty"`
Ephemeral bool `json:"ephemeral,omitempty"`
}
func ProtoToRPC(input *pb.WakuMessage) *RPCWakuMessage {
if input == nil {
return nil
}
rpcWakuMsg := &RPCWakuMessage{
Payload: input.Payload,
ContentTopic: input.ContentTopic,
Version: input.Version,
Timestamp: input.Timestamp,
Ephemeral: input.Ephemeral,
}
if input.RateLimitProof != nil {
rpcWakuMsg.RateLimitProof = &RateLimitProof{
Proof: input.RateLimitProof.Proof,
MerkleRoot: input.RateLimitProof.MerkleRoot,
Epoch: input.RateLimitProof.Epoch,
ShareX: input.RateLimitProof.ShareX,
ShareY: input.RateLimitProof.ShareY,
Nullifier: input.RateLimitProof.Nullifier,
RlnIdentifier: input.RateLimitProof.RlnIdentifier,
}
}
return rpcWakuMsg
}
func (r *RPCWakuMessage) toProto() *pb.WakuMessage {
if r == nil {
return nil
}
msg := &pb.WakuMessage{
Payload: r.Payload,
ContentTopic: r.ContentTopic,
Version: r.Version,
Timestamp: r.Timestamp,
Ephemeral: r.Ephemeral,
}
if r.RateLimitProof != nil {
msg.RateLimitProof = &pb.RateLimitProof{
Proof: r.RateLimitProof.Proof,
MerkleRoot: r.RateLimitProof.MerkleRoot,
Epoch: r.RateLimitProof.Epoch,
ShareX: r.RateLimitProof.ShareX,
ShareY: r.RateLimitProof.ShareY,
Nullifier: r.RateLimitProof.Nullifier,
RlnIdentifier: r.RateLimitProof.RlnIdentifier,
}
}
return msg
}
+121
View File
@@ -0,0 +1,121 @@
package rpc
import (
"context"
"fmt"
"net/http"
"net/http/pprof"
"time"
"github.com/gorilla/mux"
"github.com/gorilla/rpc/v2"
"github.com/waku-org/go-waku/waku/v2/node"
"go.uber.org/zap"
)
type WakuRpc struct {
node *node.WakuNode
server *http.Server
log *zap.Logger
relayService *RelayService
filterService *FilterService
adminService *AdminService
}
func NewWakuRpc(node *node.WakuNode, address string, port int, enableAdmin bool, enablePProf bool, cacheCapacity int, log *zap.Logger) *WakuRpc {
wrpc := new(WakuRpc)
wrpc.log = log.Named("rpc")
s := rpc.NewServer()
s.RegisterCodec(NewSnakeCaseCodec(), "application/json")
s.RegisterCodec(NewSnakeCaseCodec(), "application/json;charset=UTF-8")
mux := mux.NewRouter()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
s.ServeHTTP(w, r)
wrpc.log.Info("served request", zap.String("path", r.URL.Path), zap.Duration("duration", time.Since(t)))
})
if enablePProf {
mux.PathPrefix("/debug/").Handler(http.DefaultServeMux)
mux.HandleFunc("/debug/pprof/", pprof.Index)
}
debugService := NewDebugService(node)
err := s.RegisterService(debugService, "Debug")
if err != nil {
wrpc.log.Error("registering debug service", zap.Error(err))
}
var relayService *RelayService
if node.Relay() != nil {
relayService = NewRelayService(node, cacheCapacity, log)
err = s.RegisterService(relayService, "Relay")
if err != nil {
wrpc.log.Error("registering relay service", zap.Error(err))
}
}
err = s.RegisterService(&StoreService{node, log}, "Store")
if err != nil {
wrpc.log.Error("registering store service", zap.Error(err))
}
if enableAdmin {
adminService := &AdminService{node, log.Named("admin")}
err = s.RegisterService(adminService, "Admin")
if err != nil {
wrpc.log.Error("registering admin service", zap.Error(err))
}
wrpc.adminService = adminService
}
filterService := NewFilterService(node, cacheCapacity, log)
err = s.RegisterService(filterService, "Filter")
if err != nil {
wrpc.log.Error("registering filter service", zap.Error(err))
}
listenAddr := fmt.Sprintf("%s:%d", address, port)
server := &http.Server{
Addr: listenAddr,
Handler: mux,
}
server.RegisterOnShutdown(func() {
filterService.Stop()
if relayService != nil {
relayService.Stop()
}
})
wrpc.node = node
wrpc.server = server
wrpc.relayService = relayService
wrpc.filterService = filterService
return wrpc
}
func (r *WakuRpc) Start() {
if r.relayService != nil {
go r.relayService.Start()
}
go r.filterService.Start()
go func() {
_ = r.server.ListenAndServe()
}()
r.log.Info("server started", zap.String("addr", r.server.Addr))
}
func (r *WakuRpc) Stop(ctx context.Context) error {
r.log.Info("shutting down server")
return r.server.Shutdown(ctx)
}
+19
View File
@@ -0,0 +1,19 @@
package rpc
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/utils"
)
func TestWakuRpc(t *testing.T) {
options := node.WithWakuStore()
n, err := node.New(options)
require.NoError(t, err)
rpc := NewWakuRpc(n, "127.0.0.1", 8080, true, false, 30, utils.Logger())
require.NotNil(t, rpc.server)
require.Equal(t, rpc.server.Addr, "127.0.0.1:8080")
}
-13
View File
@@ -1,13 +0,0 @@
//go:build gowaku_no_rln
// +build gowaku_no_rln
package server
import (
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
)
func AppendRLNProof(node *node.WakuNode, msg *pb.WakuMessage) error {
return nil
}
-135
View File
@@ -1,135 +0,0 @@
package rest
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/protocol"
ma "github.com/multiformats/go-multiaddr"
"github.com/waku-org/go-waku/cmd/waku/server"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/peerstore"
waku_proto "github.com/waku-org/go-waku/waku/v2/protocol"
"go.uber.org/zap"
)
type AdminService struct {
node *node.WakuNode
mux *chi.Mux
log *zap.Logger
}
type WakuPeer struct {
ID string `json:"id"`
MultiAddrs []string `json:"multiaddrs"`
Protocols []string `json:"protocols"`
Connected bool `json:"connected"`
PubsubTopics []string `json:"pubsubTopics"`
}
type WakuPeerInfo struct {
MultiAddr string `json:"multiaddr"`
Shards []int `json:"shards"`
Protocols []string `json:"protocols"`
}
const routeAdminV1Peers = "/admin/v1/peers"
func NewAdminService(node *node.WakuNode, m *chi.Mux, log *zap.Logger) *AdminService {
d := &AdminService{
node: node,
mux: m,
log: log,
}
m.Get(routeAdminV1Peers, d.getV1Peers)
m.Post(routeAdminV1Peers, d.postV1Peer)
return d
}
func (a *AdminService) getV1Peers(w http.ResponseWriter, req *http.Request) {
peers, err := a.node.Peers()
if err != nil {
a.log.Error("failed to fetch peers", zap.Error(err))
writeErrOrResponse(w, err, nil)
return
}
a.log.Info("fetched peers", zap.Int("count", len(peers)))
response := make([]WakuPeer, 0)
for _, peer := range peers {
if peer.ID.String() == a.node.Host().ID().String() {
//Skip own node id
continue
}
wPeer := WakuPeer{
ID: peer.ID.String(),
Connected: peer.Connected,
}
for _, addr := range peer.Addrs {
wPeer.MultiAddrs = append(wPeer.MultiAddrs, addr.String())
}
for _, proto := range peer.Protocols {
if !server.IsWakuProtocol(proto) {
a.log.Debug("skipping protocol as it is a non-waku protocol", logging.HostID("peer", peer.ID), zap.String("protocol", string(proto)))
continue
}
wPeer.Protocols = append(wPeer.Protocols, string(proto))
}
wPeer.PubsubTopics = peer.PubsubTopics
response = append(response, wPeer)
}
writeErrOrResponse(w, nil, response)
}
func (a *AdminService) postV1Peer(w http.ResponseWriter, req *http.Request) {
var pInfo WakuPeerInfo
var topics []string
var protos []protocol.ID
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&pInfo); err != nil {
a.log.Error("failed to decode request", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
addr, err := ma.NewMultiaddr(pInfo.MultiAddr)
if err != nil {
a.log.Error("building multiaddr", zap.Error(err))
writeErrOrResponse(w, err, nil)
return
}
for _, shard := range pInfo.Shards {
topic := waku_proto.NewStaticShardingPubsubTopic(a.node.ClusterID(), uint16(shard))
topics = append(topics, topic.String())
}
for _, proto := range pInfo.Protocols {
protos = append(protos, protocol.ID(proto))
}
id, err := a.node.AddPeer(addr, peerstore.Static, topics, protos...)
if err != nil {
a.log.Error("failed to add peer", zap.Error(err))
writeErrOrResponse(w, err, nil)
return
}
a.log.Info("add peer successful", logging.HostID("peerID", id))
pi := peer.AddrInfo{ID: id, Addrs: []ma.Multiaddr{addr}}
err = a.node.Host().Connect(req.Context(), pi)
if err != nil {
a.log.Error("failed to connect to peer", logging.HostID("peerID", id), zap.Error(err))
writeErrOrResponse(w, err, nil)
return
}
writeErrOrResponse(w, nil, nil)
}
-92
View File
@@ -1,92 +0,0 @@
openapi: 3.0.3
info:
title: Waku V2 node REST API
version: 1.0.0
contact:
name: VAC Team
url: https://forum.vac.dev/
tags:
- name: admin
description: Admin REST API for WakuV2 node
paths:
/admin/v1/peers:
get:
summary: Get connected peers info
description: Retrieve information about connected peers.
operationId: getPeerInfo
tags:
- admin
responses:
'200':
description: Information about a Waku v2 node.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/WakuPeer'
'5XX':
description: Unexpected error.
post:
summary: Adds new peer(s) to connect with
description: Adds new peer(s) to connect with.
operationId: postPeerInfo
tags:
- admin
requestBody:
content:
application/json:
schema:
type: object
items:
$ref: '#/components/schemas/WakuPeerInfo'
responses:
'200':
description: Ok
'400':
description: Cannot connect to one or more peers.
'5XX':
description: Unexpected error.
components:
schemas:
WakuPeerInfo:
type: object
required:
- multiaddr
- shards
- protocols
protocols:
type: array
items:
type: string
shards:
type: array
items:
type: integer
WakuPeer:
type: object
required:
- id
- addrs
- protocols
- connected
properties:
connected:
type: string
addrs:
type: array
items:
type: string
protocols:
type: array
items:
type: string
connected:
type: boolean
pubsubTopics:
type: array
items:
type: string
-380
View File
@@ -1,380 +0,0 @@
package rest
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/peermanager"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/filter"
"go.uber.org/zap"
)
const filterV2Subscriptions = "/filter/v2/subscriptions"
const filterv2Messages = "/filter/v2/messages"
// FilterService represents the REST service for Filter client
type FilterService struct {
node *node.WakuNode
cancel context.CancelFunc
log *zap.Logger
cache *filterCache
runner *runnerService
}
// Start starts the RelayService
func (s *FilterService) Start(ctx context.Context) {
for _, sub := range s.node.FilterLightnode().Subscriptions() {
s.cache.subscribe(sub.ContentFilter)
}
ctx, cancel := context.WithCancel(ctx)
s.cancel = cancel
s.runner.Start(ctx)
}
// Stop stops the RelayService
func (r *FilterService) Stop() {
if r.cancel == nil {
return
}
r.cancel()
}
// NewFilterService returns an instance of FilterService
func NewFilterService(node *node.WakuNode, m *chi.Mux, cacheCapacity int, log *zap.Logger) *FilterService {
logger := log.Named("filter")
s := &FilterService{
node: node,
log: logger,
cache: newFilterCache(cacheCapacity, logger),
}
m.Route(filterV2Subscriptions, func(r chi.Router) {
r.Get("/", s.ping)
r.Get("/{requestId}", s.ping)
r.Post("/", s.subscribe)
r.Delete("/", s.unsubscribe)
r.Delete("/all", s.unsubscribeAll)
})
m.Route(filterv2Messages, func(r chi.Router) {
r.Get("/{contentTopic}", s.getMessagesByContentTopic)
r.Get("/{pubsubTopic}/{contentTopic}", s.getMessagesByPubsubTopic)
})
s.runner = newRunnerService(node.Broadcaster(), s.cache.addMessage)
return s
}
func convertFilterErrorToHttpStatus(err error) (int, string) {
code := http.StatusInternalServerError
statusDesc := "ping request failed"
filterErrorCode := filter.ExtractCodeFromFilterError(err.Error())
switch filterErrorCode {
case 404:
code = http.StatusNotFound
statusDesc = "peer has no subscription"
case 300:
case 400:
code = http.StatusBadRequest
statusDesc = "bad request format"
case 504:
code = http.StatusGatewayTimeout
case 503:
code = http.StatusServiceUnavailable
}
return code, statusDesc
}
// 400 for bad requestId
// 404 when request failed or no suitable peers
// 200 when ping successful
func (s *FilterService) ping(w http.ResponseWriter, req *http.Request) {
requestID := chi.URLParam(req, "requestId")
if requestID == "" {
writeResponse(w, &filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "bad request id",
}, http.StatusBadRequest)
return
}
// selecting random peer that supports filter protocol
peerId := s.getRandomFilterPeer(req.Context(), requestID, w)
if peerId == "" {
return
}
if err := s.node.FilterLightnode().Ping(req.Context(), peerId, filter.WithPingRequestId([]byte(requestID))); err != nil {
s.log.Error("ping request failed", zap.Error(err))
code, statusDesc := convertFilterErrorToHttpStatus(err)
writeResponse(w, &filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: statusDesc,
}, code)
return
}
// success
writeResponse(w, &filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: http.StatusText(http.StatusOK),
}, http.StatusOK)
}
// same for FilterUnsubscribeRequest
type filterSubscriptionRequest struct {
RequestID string `json:"requestId"`
ContentFilters []string `json:"contentFilters"`
PubsubTopic string `json:"pubsubTopic"`
}
type filterSubscriptionResponse struct {
RequestID string `json:"requestId"`
StatusDesc string `json:"statusDesc"`
}
// 400 on invalid request
// 404 on failed subscription
// 200 on single returned successful subscription
// NOTE: subscribe on filter client randomly selects a peer if missing for given pubSubTopic
func (s *FilterService) subscribe(w http.ResponseWriter, req *http.Request) {
message := filterSubscriptionRequest{}
if !s.readBody(w, req, &message) {
return
}
contentFilter := protocol.NewContentFilter(message.PubsubTopic, message.ContentFilters...)
//
subscriptions, err := s.node.FilterLightnode().Subscribe(req.Context(),
contentFilter,
filter.WithRequestID([]byte(message.RequestID)))
// on partial subscribe failure
if len(subscriptions) > 0 && err != nil {
s.log.Error("partial subscribe failed", zap.Error(err))
// on partial failure
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: err.Error(),
}, http.StatusOK)
}
if err != nil {
s.log.Error("subscription failed", zap.Error(err))
code := filter.ExtractCodeFromFilterError(err.Error())
if code == -1 {
code = http.StatusBadRequest
}
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: "subscription failed",
}, code)
return
}
// on success
s.cache.subscribe(contentFilter)
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: http.StatusText(http.StatusOK),
}, http.StatusOK)
}
// 400 on invalid request
// 500 on failed subscription
// 200 on successful unsubscribe
// NOTE: unsubscribe on filter client will remove subscription from all peers with matching pubSubTopic, if peerId is not provided
// to match functionality in nwaku, we will randomly select a peer that supports filter protocol.
func (s *FilterService) unsubscribe(w http.ResponseWriter, req *http.Request) {
message := filterSubscriptionRequest{} // as pubSubTopics can also be present
if !s.readBody(w, req, &message) {
return
}
peerId := s.getRandomFilterPeer(req.Context(), message.RequestID, w)
if peerId == "" {
return
}
contentFilter := protocol.NewContentFilter(message.PubsubTopic, message.ContentFilters...)
// unsubscribe on filter
result, err := s.node.FilterLightnode().Unsubscribe(
req.Context(),
contentFilter,
filter.WithRequestID([]byte(message.RequestID)),
filter.WithPeer(peerId),
)
if err != nil {
s.log.Error("unsubscribe failed", zap.Error(err))
if result == nil {
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: err.Error(),
}, http.StatusBadRequest)
}
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: err.Error(),
}, http.StatusServiceUnavailable)
return
}
// on success
for cTopic := range contentFilter.ContentTopics {
if !s.node.FilterLightnode().IsListening(contentFilter.PubsubTopic, cTopic) {
s.cache.unsubscribe(contentFilter.PubsubTopic, cTopic)
}
}
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: s.unsubscribeGetMessage(result),
}, http.StatusOK)
}
func (s *FilterService) unsubscribeGetMessage(result *filter.WakuFilterPushResult) string {
if result == nil {
return http.StatusText(http.StatusOK)
}
var peerIds string
ind := 0
for _, entry := range result.Errors() {
if entry.Err != nil {
s.log.Error("can't unsubscribe", logging.HostID("peer", entry.PeerID), zap.Error(entry.Err))
if ind != 0 {
peerIds += ", "
}
peerIds += entry.PeerID.String()
}
ind++
}
if peerIds != "" {
return "can't unsubscribe from " + peerIds
}
return http.StatusText(http.StatusOK)
}
type filterUnsubscribeAllRequest struct {
RequestID string `json:"requestId"`
}
func (s *FilterService) readBody(w http.ResponseWriter, req *http.Request, message interface{}) bool {
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(message); err != nil {
s.log.Error("bad request", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return false
}
defer req.Body.Close()
return true
}
// 400 on invalid request
// 500 on failed subscription
// 200 on all successful unsubscribe
// unsubscribe all subscriptions for a given peer
func (s *FilterService) unsubscribeAll(w http.ResponseWriter, req *http.Request) {
message := filterUnsubscribeAllRequest{}
if !s.readBody(w, req, &message) {
return
}
peerId := s.getRandomFilterPeer(req.Context(), message.RequestID, w)
if peerId == "" {
return
}
// unsubscribe all subscriptions for a given peer
errCh, err := s.node.FilterLightnode().UnsubscribeAll(
req.Context(),
filter.WithRequestID([]byte(message.RequestID)),
filter.WithPeer(peerId),
)
if err != nil {
s.log.Error("unsubscribeAll failed", zap.Error(err))
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: err.Error(),
}, http.StatusServiceUnavailable)
return
}
// on success
writeResponse(w, filterSubscriptionResponse{
RequestID: message.RequestID,
StatusDesc: s.unsubscribeGetMessage(errCh),
}, http.StatusOK)
}
func (s FilterService) getRandomFilterPeer(ctx context.Context, requestId string, w http.ResponseWriter) peer.ID {
// selecting random peer that supports filter protocol
peerIds, err := s.node.PeerManager().SelectPeers(peermanager.PeerSelectionCriteria{
SelectionType: peermanager.Automatic,
Proto: filter.FilterSubscribeID_v20beta1,
Ctx: ctx,
})
if err != nil {
s.log.Error("selecting peer", zap.Error(err))
writeResponse(w, filterSubscriptionResponse{
RequestID: requestId,
StatusDesc: "No suitable peers",
}, http.StatusServiceUnavailable)
return ""
}
return peerIds[0]
}
func (s *FilterService) getMessagesByContentTopic(w http.ResponseWriter, req *http.Request) {
contentTopic := topicFromPath(w, req, "contentTopic", s.log)
if contentTopic == "" {
return
}
pubsubTopic, err := protocol.GetPubSubTopicFromContentTopic(contentTopic)
if err != nil {
writeGetMessageErr(w, fmt.Errorf("bad content topic"), http.StatusBadRequest, s.log)
return
}
s.getMessages(w, req, pubsubTopic, contentTopic)
}
func (s *FilterService) getMessagesByPubsubTopic(w http.ResponseWriter, req *http.Request) {
contentTopic := topicFromPath(w, req, "contentTopic", s.log)
if contentTopic == "" {
return
}
pubsubTopic := topicFromPath(w, req, "pubsubTopic", s.log)
if pubsubTopic == "" {
return
}
s.getMessages(w, req, pubsubTopic, contentTopic)
}
// 400 on invalid request
// 500 on failed subscription
// 200 on all successful unsubscribe
// unsubscribe all subscriptions for a given peer
func (s *FilterService) getMessages(w http.ResponseWriter, req *http.Request, pubsubTopic, contentTopic string) {
msgs, err := s.cache.getMessages(pubsubTopic, contentTopic)
if err != nil {
writeGetMessageErr(w, err, http.StatusNotFound, s.log)
return
}
writeResponse(w, msgs, http.StatusOK)
}
-337
View File
@@ -1,337 +0,0 @@
openapi: 3.0.3
info:
title: Waku V2 node REST API
version: 1.0.0
contact:
name: VAC Team
url: https://forum.vac.dev/
tags:
- name: filter
description: Filter REST API for WakuV2 node
paths:
/filter/v2/subscriptions/{requestId}:
get: # get_waku_v2_filter_v2_subscription - ping
summary: Subscriber-ping - a peer can query if there is a registered subscription for it
description: |
Subscriber peer can query its subscription existence on service node.
Returns HTTP200 if exists and HTTP404 if not.
Client must not fill anything but requestId in the request body.
operationId: subscriberPing
tags:
- filter
parameters:
- in: path
name: requestId
required: true
schema:
type: string
description: Id of ping request
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'400':
description: Bad request.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'404':
description: Not found.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'5XX':
description: Unexpected error.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
/filter/v2/subscriptions:
post: # post_waku_v2_filter_v2_subscription
summary: Subscribe a peer to an array of content topics under a pubsubTopic
description: |
Subscribe a peer to an array of content topics under a pubsubTopic.
It is allowed to refresh or add new content topic to an existing subscription.
Fields pubsubTopic and contentFilters must be filled.
operationId: postSubscriptions
tags:
- filter
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscribeRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
# TODO: Review the possible errors of this endpoint
'400':
description: Bad request.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'404':
description: Not found.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'5XX':
description: Unexpected error.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
delete: # delete_waku_v2_filter_v2_subscription
summary: Unsubscribe a peer from content topics
description: |
Unsubscribe a peer from content topics
Only that subscription will be removed which matches existing.
operationId: deleteSubscriptions
tags:
- filter
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/FilterUnsubscribeRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'400':
description: Bad request.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'404':
description: Not found.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'5XX':
description: Unexpected error.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
/filter/v2/subscriptions/all:
delete: # delete_waku_v2_filter_v2_subscription
summary: Unsubscribe a peer from all content topics
description: |
Unsubscribe a peer from all content topics
operationId: deleteAllSubscriptions
tags:
- filter
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/FilterUnsubscribeAllRequest'
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'400':
description: Bad request.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'404':
description: Not found.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
'5XX':
description: Unexpected error.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterSubscriptionResponse'
/filter/v2/messages/{contentTopic}:
get: # get_waku_v2_filter_v2_messages
summary: Get the latest messages on the polled content topic
description: Get a list of messages that were received on a subscribed content topic after the last time this method was called.
operationId: getMessagesByTopic
tags:
- filter
parameters:
- in: path
name: contentTopic # Note the name is the same as in the path
required: true
schema:
type: string
description: Content topic of message
responses:
'200':
description: The latest messages on the polled topic.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterGetMessagesResponse'
# TODO: Review the possible errors of this endpoint
'400':
description: Bad request.
content:
text/plain:
schema:
type: string
'404':
description: Not found.
content:
text/plain:
schema:
type: string
'5XX':
description: Unexpected error.
content:
text/plain:
schema:
type: string
/filter/v2/messages/{pubsubTopic}/{contentTopic}:
get: # get_waku_v2_filter_v2_messages
summary: Get the latest messages on the polled pubsub/content topic pair
description: Get a list of messages that were received on a subscribed content topic after the last time this method was called.
operationId: getMessagesByTopic
tags:
- filter
parameters:
- in: path
name: contentTopic # Note the name is the same as in the path
required: true
schema:
type: string
description: Content topic of message
- in: path
name: pubsubTopic # Note the name is the same as in the path
required: true
schema:
type: string
description: pubsub topic of message
responses:
'200':
description: The latest messages on the polled topic.
content:
application/json:
schema:
$ref: '#/components/schemas/FilterGetMessagesResponse'
# TODO: Review the possible errors of this endpoint
'400':
description: Bad request.
content:
text/plain:
schema:
type: string
'404':
description: Not found.
content:
text/plain:
schema:
type: string
'5XX':
description: Unexpected error.
content:
text/plain:
schema:
type: string
components:
PubSubTopic:
type: string
ContentTopic:
type: string
FilterSubscriptionResponse:
type: object
properties:
requestId:
type: string
statusDesc:
type: string
required:
- requestId
FilterSubscribeRequest:
type: object
properties:
requestId:
type: string
contentFilters:
type: array
items:
$ref: '#/components/schemas/ContentTopic'
pubsubTopic:
$ref: "#/components/schemas/PubSubTopic"
required:
- requestId
- contentFilters
- pubsubTopic
FilterUnsubscribeRequest:
type: object
properties:
requestId:
type: string
contentFilters:
type: array
items:
$ref: '#/components/schemas/ContentTopic'
pubsubTopic:
$ref: "#/components/schemas/PubSubTopic"
required:
- requestId
- contentFilters
FilterUnsubscribeAllRequest:
type: object
properties:
requestId:
type: string
required:
- requestId
FilterGetMessagesResponse:
type: array
items:
$ref: '#/components/schemas/FilterWakuMessage'
FilterWakuMessage:
type: object
properties:
payload:
type: string
format: byte
contentTopic:
$ref: '#/components/schemas/ContentTopic'
version:
type: number
timestamp:
type: number
required:
- payload
-84
View File
@@ -1,84 +0,0 @@
package rest
import (
"fmt"
"sync"
"github.com/waku-org/go-waku/waku/v2/protocol"
"go.uber.org/zap"
)
type filterCache struct {
capacity int
mu sync.RWMutex
log *zap.Logger
data map[string]map[string][]*RestWakuMessage
}
func newFilterCache(capacity int, log *zap.Logger) *filterCache {
return &filterCache{
capacity: capacity,
data: make(map[string]map[string][]*RestWakuMessage),
log: log.Named("cache"),
}
}
func (c *filterCache) subscribe(contentFilter protocol.ContentFilter) {
c.mu.Lock()
defer c.mu.Unlock()
pubSubTopicMap, _ := protocol.ContentFilterToPubSubTopicMap(contentFilter)
for pubsubTopic, contentTopics := range pubSubTopicMap {
if c.data[pubsubTopic] == nil {
c.data[pubsubTopic] = make(map[string][]*RestWakuMessage)
}
for _, topic := range contentTopics {
if c.data[pubsubTopic][topic] == nil {
c.data[pubsubTopic][topic] = []*RestWakuMessage{}
}
}
}
}
func (c *filterCache) unsubscribe(pubsubTopic string, contentTopic string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data[pubsubTopic], contentTopic)
}
func (c *filterCache) addMessage(envelope *protocol.Envelope) {
c.mu.Lock()
defer c.mu.Unlock()
pubsubTopic := envelope.PubsubTopic()
contentTopic := envelope.Message().ContentTopic
if c.data[pubsubTopic] == nil || c.data[pubsubTopic][contentTopic] == nil {
return
}
// Keep a specific max number of message per topic
if len(c.data[pubsubTopic][contentTopic]) >= c.capacity {
c.data[pubsubTopic][contentTopic] = c.data[pubsubTopic][contentTopic][1:]
}
message := &RestWakuMessage{}
if err := message.FromProto(envelope.Message()); err != nil {
c.log.Error("converting protobuffer msg into rest msg", zap.Error(err))
return
}
c.data[pubsubTopic][contentTopic] = append(c.data[pubsubTopic][contentTopic], message)
}
func (c *filterCache) getMessages(pubsubTopic string, contentTopic string) ([]*RestWakuMessage, error) {
c.mu.RLock()
defer c.mu.RUnlock()
if c.data[pubsubTopic] == nil || c.data[pubsubTopic][contentTopic] == nil {
return nil, fmt.Errorf("not subscribed to pubsubTopic:%s contentTopic: %s", pubsubTopic, contentTopic)
}
msgs := c.data[pubsubTopic][contentTopic]
c.data[pubsubTopic][contentTopic] = []*RestWakuMessage{}
return msgs, nil
}
-392
View File
@@ -1,392 +0,0 @@
package rest
import (
"bytes"
"context"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/tests"
"github.com/waku-org/go-waku/waku/v2/node"
wakupeerstore "github.com/waku-org/go-waku/waku/v2/peerstore"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/filter"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/utils"
)
func createNode(t *testing.T, opts ...node.WakuNodeOption) *node.WakuNode {
node, err := node.New(opts...)
require.NoError(t, err)
err = node.Start(context.Background())
require.NoError(t, err)
return node
}
// node2 connects to node1
func twoFilterConnectedNodes(t *testing.T, pubSubTopics ...string) (*node.WakuNode, *node.WakuNode) {
node1 := createNode(t, node.WithWakuFilterFullNode()) // full node filter
node2 := createNode(t, node.WithWakuFilterLightNode()) // light node filter
node2.Host().Peerstore().AddAddr(node1.Host().ID(), tests.GetHostAddress(node1.Host()), peerstore.PermanentAddrTTL)
err := node2.Host().Peerstore().AddProtocols(node1.Host().ID(), filter.FilterSubscribeID_v20beta1)
require.NoError(t, err)
err = node2.Host().Peerstore().(*wakupeerstore.WakuPeerstoreImpl).SetPubSubTopics(node1.Host().ID(), pubSubTopics)
require.NoError(t, err)
return node1, node2
}
// test 400, 404 status code for ping rest endpoint
// both requests are not successful
func TestFilterPingFailure(t *testing.T) {
node1, node2 := twoFilterConnectedNodes(t)
defer func() {
node1.Stop()
node2.Stop()
}()
router := chi.NewRouter()
_ = NewFilterService(node2, router, 0, utils.Logger())
// with empty requestID
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, fmt.Sprintf("/filter/v2/subscriptions/%s", ""), nil)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: "",
StatusDesc: "bad request id",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusBadRequest, rr.Code)
// no subscription with peer
requestID := hex.EncodeToString(protocol.GenerateRequestID())
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("/filter/v2/subscriptions/%s", requestID), nil)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "peer has no subscription",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusNotFound, rr.Code)
}
// create a filter subscription to the peer and try peer that peer
// both steps should be successful
func TestFilterSubscribeAndPing(t *testing.T) {
pubsubTopic := "/waku/2/test/proto"
contentTopics := []string{"test"}
requestID := hex.EncodeToString(protocol.GenerateRequestID())
node1, node2 := twoFilterConnectedNodes(t, pubsubTopic)
defer func() {
node1.Stop()
node2.Stop()
}()
router := chi.NewRouter()
_ = NewFilterService(node2, router, 0, utils.Logger())
// create subscription to peer
rr := httptest.NewRecorder()
reqReader := strings.NewReader(toString(t, filterSubscriptionRequest{
RequestID: requestID,
PubsubTopic: pubsubTopic,
ContentFilters: contentTopics,
}))
req, _ := http.NewRequest(http.MethodPost, filterV2Subscriptions, reqReader)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
// trying pinging the peer once there is subscription to it
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", filterV2Subscriptions, requestID), nil)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
}
// create subscription to peer
// delete the subscription to the peer with matching pubSub and contentTopic
func TestFilterSubscribeAndUnsubscribe(t *testing.T) {
pubsubTopic := "/waku/2/test/proto"
contentTopics := []string{"test"}
requestID := hex.EncodeToString(protocol.GenerateRequestID())
node1, node2 := twoFilterConnectedNodes(t, pubsubTopic)
defer func() {
node1.Stop()
node2.Stop()
}()
router := chi.NewRouter()
_ = NewFilterService(node2, router, 0, utils.Logger())
// create subscription to peer
rr := httptest.NewRecorder()
reqReader := strings.NewReader(toString(t, filterSubscriptionRequest{
RequestID: requestID,
PubsubTopic: pubsubTopic,
ContentFilters: contentTopics,
}))
req, _ := http.NewRequest(http.MethodPost, filterV2Subscriptions, reqReader)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
// delete the subscription to the peer with matching pubSub and contentTopic
requestID = hex.EncodeToString(protocol.GenerateRequestID())
rr = httptest.NewRecorder()
reqReader = strings.NewReader(toString(t, filterSubscriptionRequest{
RequestID: requestID,
PubsubTopic: pubsubTopic,
ContentFilters: contentTopics,
}))
req, _ = http.NewRequest(http.MethodDelete, filterV2Subscriptions, reqReader)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
}
// create 2 subscription from filter client to server
// make a unsubscribeAll request
// try pinging the peer, if 404 is returned then unsubscribeAll was successful
func TestFilterAllUnsubscribe(t *testing.T) {
pubsubTopic := "/waku/2/test/proto"
contentTopics1 := "ct_1"
contentTopics2 := "ct_2"
node1, node2 := twoFilterConnectedNodes(t, pubsubTopic)
defer func() {
node1.Stop()
node2.Stop()
}()
router := chi.NewRouter()
_ = NewFilterService(node2, router, 0, utils.Logger())
// create 2 different subscription to peer
for _, ct := range []string{contentTopics1, contentTopics2} {
requestID := hex.EncodeToString(protocol.GenerateRequestID())
rr := httptest.NewRecorder()
reqReader := strings.NewReader(toString(t, filterSubscriptionRequest{
RequestID: requestID,
PubsubTopic: pubsubTopic,
ContentFilters: []string{ct},
}))
req, _ := http.NewRequest(http.MethodPost, filterV2Subscriptions, reqReader)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
}
// delete all subscription to the peer
requestID := hex.EncodeToString(protocol.GenerateRequestID())
rr := httptest.NewRecorder()
reqReader := strings.NewReader(toString(t, filterUnsubscribeAllRequest{
RequestID: requestID,
}))
req, _ := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/all", filterV2Subscriptions), reqReader)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
// check if all subscriptions are deleted to the peer are deleted
requestID = hex.EncodeToString(protocol.GenerateRequestID())
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", filterV2Subscriptions, requestID), nil)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "peer has no subscription",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusNotFound, rr.Code)
}
func checkJSON(t *testing.T, expected, actual interface{}) {
require.JSONEq(t, toString(t, expected), toString(t, actual))
}
func getFilterResponse(t *testing.T, body *bytes.Buffer) filterSubscriptionResponse {
resp := filterSubscriptionResponse{}
err := json.Unmarshal(body.Bytes(), &resp)
require.NoError(t, err)
return resp
}
func getMessageResponse(t *testing.T, body *bytes.Buffer) []*pb.WakuMessage {
resp := []*pb.WakuMessage{}
err := json.Unmarshal(body.Bytes(), &resp)
require.NoError(t, err)
return resp
}
func toString(t *testing.T, data interface{}) string {
bytes, err := json.Marshal(data)
require.NoError(t, err)
return string(bytes)
}
func TestFilterGetMessages(t *testing.T) {
pubsubTopic := "/waku/2/test/proto"
contentTopic := "/waku/2/app/1"
// get nodes add connect them
generatedPubsubTopic, err := protocol.GetPubSubTopicFromContentTopic(contentTopic)
require.NoError(t, err)
node1, node2 := twoFilterConnectedNodes(t, pubsubTopic, generatedPubsubTopic)
defer func() {
node1.Stop()
node2.Stop()
}()
// set router and start filter service
router := chi.NewRouter()
service := NewFilterService(node2, router, 2, utils.Logger())
go service.Start(context.Background())
defer service.Stop()
{ // create subscription so that messages are cached
for _, pubsubTopic := range []string{"", pubsubTopic} {
requestID := hex.EncodeToString(protocol.GenerateRequestID())
rr := httptest.NewRecorder()
reqReader := strings.NewReader(toString(t, filterSubscriptionRequest{
RequestID: requestID,
PubsubTopic: pubsubTopic,
ContentFilters: []string{contentTopic},
}))
req, _ := http.NewRequest(http.MethodPost, filterV2Subscriptions, reqReader)
router.ServeHTTP(rr, req)
checkJSON(t, filterSubscriptionResponse{
RequestID: requestID,
StatusDesc: "OK",
}, getFilterResponse(t, rr.Body))
require.Equal(t, http.StatusOK, rr.Code)
}
}
// submit messages
messageByContentTopic := []*protocol.Envelope{
genMessage("", contentTopic),
genMessage("", contentTopic),
genMessage("", contentTopic),
}
messageByPubsubTopic := []*protocol.Envelope{
genMessage(pubsubTopic, contentTopic),
}
for _, envelope := range append(messageByContentTopic, messageByPubsubTopic...) {
node2.Broadcaster().Submit(envelope)
}
time.Sleep(1 * time.Second)
{ // with malformed contentTopic
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet,
fmt.Sprintf("%s/%s", filterv2Messages, url.QueryEscape("/waku/2/wrongtopic")),
nil,
)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
require.Equal(t, "bad content topic", rr.Body.String())
}
{ // with check if the cache is working properly
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet,
fmt.Sprintf("%s/%s", filterv2Messages, url.QueryEscape(contentTopic)),
nil,
)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
checkJSON(t, toMessage(messageByContentTopic[1:]), getMessageResponse(t, rr.Body))
}
{ // check if pubsubTopic is present in the url
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet,
fmt.Sprintf("%s//%s", filterv2Messages, url.QueryEscape(contentTopic)),
nil,
)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
require.Equal(t, "missing pubsubTopic", rr.Body.String())
}
{ // check messages by pubsub/contentTopic pair
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet,
fmt.Sprintf("%s/%s/%s", filterv2Messages, url.QueryEscape(pubsubTopic), url.QueryEscape(contentTopic)),
nil,
)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
checkJSON(t, toMessage(messageByPubsubTopic), getMessageResponse(t, rr.Body))
}
{ // check if pubsubTopic/contentTOpic is subscribed or not.
rr := httptest.NewRecorder()
notSubscibredPubsubTopic := "/waku/2/test2/proto"
req, _ := http.NewRequest(http.MethodGet,
fmt.Sprintf("%s/%s/%s", filterv2Messages, url.QueryEscape(notSubscibredPubsubTopic), url.QueryEscape(contentTopic)),
nil,
)
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusNotFound, rr.Code)
require.Equal(t,
fmt.Sprintf("not subscribed to pubsubTopic:%s contentTopic: %s", notSubscibredPubsubTopic, contentTopic),
rr.Body.String(),
)
}
}
func toMessage(envs []*protocol.Envelope) []*pb.WakuMessage {
msgs := make([]*pb.WakuMessage, len(envs))
for i, env := range envs {
msgs[i] = env.Message()
}
return msgs
}
func genMessage(pubsubTopic, contentTopic string) *protocol.Envelope {
if pubsubTopic == "" {
pubsubTopic, _ = protocol.GetPubSubTopicFromContentTopic(contentTopic)
}
return protocol.NewEnvelope(
&pb.WakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: contentTopic,
Timestamp: utils.GetUnixEpoch(),
},
0,
pubsubTopic,
)
}
-52
View File
@@ -1,52 +0,0 @@
package rest
import (
"context"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/waku-org/go-waku/waku/v2/node"
)
type HealthService struct {
node *node.WakuNode
mux *chi.Mux
}
const routeHealth = "/health"
func NewHealthService(node *node.WakuNode, m *chi.Mux) *HealthService {
h := &HealthService{
node: node,
mux: m,
}
m.Get(routeHealth, h.getHealth)
return h
}
type HealthResponse string
func (d *HealthService) getHealth(w http.ResponseWriter, r *http.Request) {
if d.node.RLNRelay() != nil {
isReady, err := d.node.RLNRelay().IsReady(r.Context())
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
writeResponse(w, HealthResponse("Health check timed out"), http.StatusInternalServerError)
} else {
writeResponse(w, HealthResponse(err.Error()), http.StatusInternalServerError)
}
return
}
if isReady {
writeResponse(w, HealthResponse("Node is healthy"), http.StatusOK)
} else {
writeResponse(w, HealthResponse("Node is not ready"), http.StatusInternalServerError)
}
} else {
writeResponse(w, HealthResponse("Non RLN healthcheck is not implemented"), http.StatusNotImplemented)
}
}
-41
View File
@@ -1,41 +0,0 @@
openapi: 3.0.3
info:
title: Waku V2 node Health REST API
version: 1.0.0
contact:
name: VAC Team
url: https://forum.vac.dev/
tags:
- name: health
description: Healt check REST API for WakuV2 node
paths:
/health:
get:
summary: Get node health status
description: Retrieve readiness of a Waku v2 node.
operationId: healthcheck
tags:
- health
responses:
'200':
description: Waku v2 node is up and running.
content:
text/plain:
schema:
type: string
example: Node is healty
'500':
description: Internal server error
content:
text/plain:
schema:
type: string
'503':
description: Node not initialized or having issues
content:
text/plain:
schema:
type: string
example: Node is not initialized
-212
View File
@@ -1,212 +0,0 @@
package rest
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/multiformats/go-multiaddr"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_store"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_store/pb"
)
type LegacyStoreService struct {
node *node.WakuNode
mux *chi.Mux
}
type LegacyStoreResponse struct {
Messages []LegacyStoreWakuMessage `json:"messages"`
Cursor *LegacyHistoryCursor `json:"cursor,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type LegacyHistoryCursor struct {
PubsubTopic string `json:"pubsubTopic"`
SenderTime string `json:"senderTime"`
StoreTime string `json:"storeTime"`
Digest []byte `json:"digest"`
}
type LegacyStoreWakuMessage struct {
Payload []byte `json:"payload"`
ContentTopic string `json:"contentTopic"`
Version *uint32 `json:"version,omitempty"`
Timestamp *int64 `json:"timestamp,omitempty"`
Meta []byte `json:"meta,omitempty"`
}
const routeLegacyStoreMessagesV1 = "/store/v1/messages"
func NewLegacyStoreService(node *node.WakuNode, m *chi.Mux) *LegacyStoreService {
s := &LegacyStoreService{
node: node,
mux: m,
}
m.Get(routeLegacyStoreMessagesV1, s.getV1Messages)
return s
}
func getLegacyStoreParams(r *http.Request) (*legacy_store.Query, []legacy_store.HistoryRequestOption, error) {
query := &legacy_store.Query{}
var options []legacy_store.HistoryRequestOption
var err error
peerAddrStr := r.URL.Query().Get("peerAddr")
var m multiaddr.Multiaddr
if peerAddrStr != "" {
m, err = multiaddr.NewMultiaddr(peerAddrStr)
if err != nil {
return nil, nil, err
}
options = append(options, legacy_store.WithPeerAddr(m))
} else {
// The user didn't specify a peer address and self-node is configured as a store node.
// In this case we assume that the user is willing to retrieve the messages stored by
// the local/self store node.
options = append(options, legacy_store.WithLocalQuery())
}
query.PubsubTopic = r.URL.Query().Get("pubsubTopic")
contentTopics := r.URL.Query().Get("contentTopics")
if contentTopics != "" {
query.ContentTopics = strings.Split(contentTopics, ",")
}
startTimeStr := r.URL.Query().Get("startTime")
if startTimeStr != "" {
startTime, err := strconv.ParseInt(startTimeStr, 10, 64)
if err != nil {
return nil, nil, err
}
query.StartTime = &startTime
}
endTimeStr := r.URL.Query().Get("endTime")
if endTimeStr != "" {
endTime, err := strconv.ParseInt(endTimeStr, 10, 64)
if err != nil {
return nil, nil, err
}
query.EndTime = &endTime
}
var cursor *pb.Index
senderTimeStr := r.URL.Query().Get("senderTime")
storeTimeStr := r.URL.Query().Get("storeTime")
digestStr := r.URL.Query().Get("digest")
if senderTimeStr != "" || storeTimeStr != "" || digestStr != "" {
cursor = &pb.Index{}
if senderTimeStr != "" {
cursor.SenderTime, err = strconv.ParseInt(senderTimeStr, 10, 64)
if err != nil {
return nil, nil, err
}
}
if storeTimeStr != "" {
cursor.ReceiverTime, err = strconv.ParseInt(storeTimeStr, 10, 64)
if err != nil {
return nil, nil, err
}
}
if digestStr != "" {
cursor.Digest, err = base64.URLEncoding.DecodeString(digestStr)
if err != nil {
return nil, nil, err
}
}
cursor.PubsubTopic = query.PubsubTopic
options = append(options, legacy_store.WithCursor(cursor))
}
pageSizeStr := r.URL.Query().Get("pageSize")
ascendingStr := r.URL.Query().Get("ascending")
if ascendingStr != "" || pageSizeStr != "" {
ascending := true
pageSize := uint64(legacy_store.DefaultPageSize)
if ascendingStr != "" {
ascending, err = strconv.ParseBool(ascendingStr)
if err != nil {
return nil, nil, err
}
}
if pageSizeStr != "" {
pageSize, err = strconv.ParseUint(pageSizeStr, 10, 64)
if err != nil {
return nil, nil, err
}
if pageSize > legacy_store.MaxPageSize {
pageSize = legacy_store.MaxPageSize
}
}
options = append(options, legacy_store.WithPaging(ascending, pageSize))
}
return query, options, nil
}
func writeLegacyStoreError(w http.ResponseWriter, code int, err error) {
writeResponse(w, LegacyStoreResponse{ErrorMessage: err.Error()}, code)
}
func toLegacyStoreResponse(result *legacy_store.Result) LegacyStoreResponse {
response := LegacyStoreResponse{}
cursor := result.Cursor()
if cursor != nil {
response.Cursor = &LegacyHistoryCursor{
PubsubTopic: cursor.PubsubTopic,
SenderTime: fmt.Sprintf("%d", cursor.SenderTime),
StoreTime: fmt.Sprintf("%d", cursor.ReceiverTime),
Digest: cursor.Digest,
}
}
for _, m := range result.Messages {
response.Messages = append(response.Messages, LegacyStoreWakuMessage{
Payload: m.Payload,
ContentTopic: m.ContentTopic,
Version: m.Version,
Timestamp: m.Timestamp,
Meta: m.Meta,
})
}
return response
}
func (d *LegacyStoreService) getV1Messages(w http.ResponseWriter, r *http.Request) {
query, options, err := getLegacyStoreParams(r)
if err != nil {
writeLegacyStoreError(w, http.StatusBadRequest, err)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
result, err := d.node.LegacyStore().Query(ctx, *query, options...)
if err != nil {
writeLegacyStoreError(w, http.StatusInternalServerError, err)
return
}
writeErrOrResponse(w, nil, toLegacyStoreResponse(result))
}
-84
View File
@@ -1,84 +0,0 @@
openapi: 3.0.3
info:
title: Waku V2 node REST API
version: 1.0.0
contact:
name: VAC Team
url: https://forum.vac.dev/
tags:
- name: lightpush
description: Lightpush REST API for WakuV2 node
paths:
/lightpush/v1/message:
post:
summary: Request a message relay from a LightPush service provider
description: Push a message to be relayed on a PubSub topic.
operationId: postMessagesToPubsubTopic
tags:
- lightpush
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/PushRequest'
responses:
'200':
description: OK
content:
text/plain:
schema:
type: string
'400':
description: Bad request.
content:
text/plain:
schema:
type: string
'500':
description: Internal server error
content:
text/plain:
schema:
type: string
'503':
description: Service not available
content:
text/plain:
schema:
type: string
components:
schemas:
PubsubTopic:
type: string
ContentTopic:
type: string
WakuMessage:
type: object
properties:
payload:
type: string
format: byte
contentTopic:
$ref: '#/components/schemas/ContentTopic'
version:
type: number
timestamp:
type: number
required:
- payload
- contentTopic
PushRequest:
type: object
properties:
pusbsubTopic:
$ref: '#/components/schemas/PubsubTopic'
message:
$ref: '#/components/schemas/WakuMessage'
required:
- message
-96
View File
@@ -1,96 +0,0 @@
package rest
import (
"encoding/json"
"errors"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/lightpush"
"go.uber.org/zap"
)
const routeLightPushV1Messages = "/lightpush/v1/message"
type LightpushService struct {
node *node.WakuNode
log *zap.Logger
}
func NewLightpushService(node *node.WakuNode, m *chi.Mux, log *zap.Logger) *LightpushService {
serv := &LightpushService{
node: node,
log: log.Named("lightpush"),
}
m.Post(routeLightPushV1Messages, serv.postMessagev1)
return serv
}
func (msg lightpushRequest) Check() error {
if msg.Message == nil {
return errors.New("waku message is required")
}
return nil
}
type lightpushRequest struct {
PubSubTopic string `json:"pubsubTopic"`
Message *RestWakuMessage `json:"message"`
}
// handled error codes are 200, 400, 500, 503
func (serv *LightpushService) postMessagev1(w http.ResponseWriter, req *http.Request) {
request := &lightpushRequest{}
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(request); err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
if err := request.Check(); err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
serv.log.Error("writing response", zap.Error(err))
return
}
if serv.node.Lightpush() == nil {
w.WriteHeader(http.StatusServiceUnavailable)
return
}
message, err := request.Message.ToProto()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
_, err = w.Write([]byte(err.Error()))
if err != nil {
serv.log.Error("writing response", zap.Error(err))
}
return
}
if err = message.Validate(); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
_, err = w.Write([]byte(err.Error()))
if err != nil {
serv.log.Error("writing response", zap.Error(err))
}
return
}
_, err = serv.node.Lightpush().Publish(req.Context(), message, lightpush.WithPubSubTopic(request.PubSubTopic))
if err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
_, err = w.Write([]byte(err.Error()))
if err != nil {
serv.log.Error("writing response", zap.Error(err))
}
} else {
writeErrOrResponse(w, err, true)
}
}
@@ -1,68 +0,0 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/tests"
"github.com/waku-org/go-waku/waku/v2/node"
wakupeerstore "github.com/waku-org/go-waku/waku/v2/peerstore"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/lightpush"
"github.com/waku-org/go-waku/waku/v2/utils"
)
// node2 connects to node1
func twoLightPushConnectedNodes(t *testing.T, pubSubTopic string) (*node.WakuNode, *node.WakuNode) {
node1 := createNode(t, node.WithLightPush(), node.WithWakuRelay())
node2 := createNode(t, node.WithLightPush(), node.WithWakuRelay())
_, err := node1.Relay().Subscribe(context.Background(), protocol.NewContentFilter(pubSubTopic))
require.NoError(t, err)
_, err = node2.Relay().Subscribe(context.Background(), protocol.NewContentFilter(pubSubTopic))
require.NoError(t, err)
node2.Host().Peerstore().AddAddr(node1.Host().ID(), tests.GetHostAddress(node1.Host()), peerstore.PermanentAddrTTL)
err = node2.Host().Peerstore().AddProtocols(node1.Host().ID(), lightpush.LightPushID_v20beta1)
require.NoError(t, err)
err = node2.Host().Peerstore().(*wakupeerstore.WakuPeerstoreImpl).SetPubSubTopics(node1.Host().ID(), []string{pubSubTopic})
require.NoError(t, err)
return node1, node2
}
func TestLightpushMessagev1(t *testing.T) {
pubSubTopic := "/waku/2/default-waku/proto"
node1, node2 := twoLightPushConnectedNodes(t, pubSubTopic)
defer func() {
node1.Stop()
node2.Stop()
}()
router := chi.NewRouter()
serv := NewLightpushService(node2, router, utils.Logger())
_ = serv
msg := lightpushRequest{
PubSubTopic: pubSubTopic,
Message: &RestWakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "abc",
Timestamp: utils.GetUnixEpoch(),
},
}
msgJSONBytes, err := json.Marshal(msg)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, routeLightPushV1Messages, bytes.NewReader(msgJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
}
-49
View File
@@ -1,49 +0,0 @@
package rest
import (
"errors"
"github.com/waku-org/go-waku/cmd/waku/server"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
)
type RestWakuMessage struct {
Payload server.Base64URLByte `json:"payload"`
ContentTopic string `json:"contentTopic"`
Version *uint32 `json:"version,omitempty"`
Timestamp *int64 `json:"timestamp,omitempty"`
Meta []byte `json:"meta,omitempty"`
Ephemeral *bool `json:"ephemeral"`
}
func (r *RestWakuMessage) FromProto(input *pb.WakuMessage) error {
if err := input.Validate(); err != nil {
return err
}
r.Payload = input.Payload
r.ContentTopic = input.ContentTopic
r.Timestamp = input.Timestamp
r.Version = input.Version
r.Meta = input.Meta
r.Ephemeral = input.Ephemeral
return nil
}
func (r *RestWakuMessage) ToProto() (*pb.WakuMessage, error) {
if r == nil {
return nil, errors.New("wakumessage is missing")
}
msg := &pb.WakuMessage{
Payload: r.Payload,
ContentTopic: r.ContentTopic,
Version: r.Version,
Timestamp: r.Timestamp,
Meta: r.Meta,
Ephemeral: r.Ephemeral,
}
return msg, nil
}
-316
View File
@@ -1,316 +0,0 @@
package rest
import (
"encoding/json"
"net/http"
"strings"
"github.com/go-chi/chi/v5"
"github.com/waku-org/go-waku/cmd/waku/server"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"go.uber.org/zap"
)
const routeRelayV1Subscriptions = "/relay/v1/subscriptions"
const routeRelayV1Messages = "/relay/v1/messages/{topic}"
const routeRelayV1AutoSubscriptions = "/relay/v1/auto/subscriptions"
const routeRelayV1AutoMessages = "/relay/v1/auto/messages"
// RelayService represents the REST service for WakuRelay
type RelayService struct {
node *node.WakuNode
log *zap.Logger
cacheCapacity uint
}
// NewRelayService returns an instance of RelayService
func NewRelayService(node *node.WakuNode, m *chi.Mux, cacheCapacity uint, log *zap.Logger) *RelayService {
s := &RelayService{
node: node,
log: log.Named("relay"),
cacheCapacity: cacheCapacity,
}
m.Post(routeRelayV1Subscriptions, s.postV1Subscriptions)
m.Delete(routeRelayV1Subscriptions, s.deleteV1Subscriptions)
m.Get(routeRelayV1Messages, s.getV1Messages)
m.Post(routeRelayV1Messages, s.postV1Message)
m.Post(routeRelayV1AutoSubscriptions, s.postV1AutoSubscriptions)
m.Delete(routeRelayV1AutoSubscriptions, s.deleteV1AutoSubscriptions)
m.Route(routeRelayV1AutoMessages, func(r chi.Router) {
r.Get("/{contentTopic}", s.getV1AutoMessages)
r.Post("/", s.postV1AutoMessage)
})
return s
}
func (r *RelayService) deleteV1Subscriptions(w http.ResponseWriter, req *http.Request) {
var topics []string
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&topics); err != nil {
r.log.Error("decoding request failure", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var err error
for _, topic := range topics {
err = r.node.Relay().Unsubscribe(req.Context(), protocol.NewContentFilter(topic))
if err != nil {
r.log.Error("unsubscribing from topic", zap.String("topic", strings.Replace(strings.Replace(topic, "\n", "", -1), "\r", "", -1)), zap.Error(err))
}
}
writeErrOrResponse(w, err, true)
}
func (r *RelayService) postV1Subscriptions(w http.ResponseWriter, req *http.Request) {
var topics []string
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&topics); err != nil {
r.log.Error("decoding request failure", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var err error
var successCnt int
var topicToSubscribe string
for _, topic := range topics {
if topic == "" {
topicToSubscribe = relay.DefaultWakuTopic
} else {
topicToSubscribe = topic
}
_, err = r.node.Relay().Subscribe(r.node.Relay().Context(), protocol.NewContentFilter(topicToSubscribe), relay.WithCacheSize(r.cacheCapacity))
if err != nil {
r.log.Error("subscribing to topic", zap.String("topic", strings.Replace(topicToSubscribe, "\n", "", -1)), zap.Error(err))
continue
}
successCnt++
}
// on partial subscribe failure
if successCnt > 0 && err != nil {
r.log.Error("partial subscribe failed", zap.Error(err))
// on partial failure
writeResponse(w, err, http.StatusOK)
return
}
writeErrOrResponse(w, err, true)
}
func (r *RelayService) getV1Messages(w http.ResponseWriter, req *http.Request) {
topic := topicFromPath(w, req, "topic", r.log)
if topic == "" {
r.log.Debug("topic is not specified, using default waku topic")
topic = relay.DefaultWakuTopic
}
//TODO: Update the API to also take a contentTopic since relay now supports filtering based on contentTopic as well.
sub, err := r.node.Relay().GetSubscriptionWithPubsubTopic(topic, "")
if err != nil {
w.WriteHeader(http.StatusNotFound)
_, err = w.Write([]byte(err.Error()))
r.log.Error("writing response", zap.Error(err))
return
}
var response []*RestWakuMessage
done := false
for {
if done || len(response) > int(r.cacheCapacity) {
break
}
select {
case envelope, open := <-sub.Ch:
if !open {
r.log.Error("consume channel is closed for subscription", zap.String("pubsubTopic", topic))
w.WriteHeader(http.StatusNotFound)
_, err = w.Write([]byte("consume channel is closed for subscription"))
if err != nil {
r.log.Error("writing response", zap.Error(err))
}
return
}
message := &RestWakuMessage{}
if err := message.FromProto(envelope.Message()); err != nil {
r.log.Error("converting protobuffer msg into rest msg", zap.Error(err))
} else {
response = append(response, message)
}
case <-req.Context().Done():
done = true
default:
done = true
}
}
writeErrOrResponse(w, nil, response)
}
func (r *RelayService) postV1Message(w http.ResponseWriter, req *http.Request) {
topic := topicFromPath(w, req, "topic", r.log)
if topic == "" {
r.log.Debug("topic is not specified, using default waku topic")
topic = relay.DefaultWakuTopic
}
var restMessage *RestWakuMessage
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&restMessage); err != nil {
r.log.Error("decoding request failure", zap.Error(err))
writeErrResponse(w, r.log, err, http.StatusBadRequest)
return
}
defer req.Body.Close()
message, err := restMessage.ToProto()
if err != nil {
r.log.Error("failed to convert message to proto", zap.Error(err))
writeErrResponse(w, r.log, err, http.StatusBadRequest)
return
}
if err := server.AppendRLNProof(r.node, message); err != nil {
r.log.Error("failed to append RLN proof for the message", zap.Error(err))
writeErrOrResponse(w, err, nil)
return
}
_, err = r.node.Relay().Publish(req.Context(), message, relay.WithPubSubTopic(strings.Replace(topic, "\n", "", -1)))
if err != nil {
r.log.Error("publishing message", zap.Error(err))
if err == pb.ErrMissingPayload || err == pb.ErrMissingContentTopic || err == pb.ErrInvalidMetaLength {
writeErrResponse(w, r.log, err, http.StatusBadRequest)
return
}
}
writeErrOrResponse(w, err, true)
}
func (r *RelayService) deleteV1AutoSubscriptions(w http.ResponseWriter, req *http.Request) {
var cTopics []string
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&cTopics); err != nil {
r.log.Error("decoding request failure", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
err := r.node.Relay().Unsubscribe(req.Context(), protocol.NewContentFilter("", cTopics...))
if err != nil {
r.log.Error("unsubscribing from topics", zap.Strings("contentTopics", cTopics), zap.Error(err))
}
writeErrOrResponse(w, err, true)
}
func (r *RelayService) postV1AutoSubscriptions(w http.ResponseWriter, req *http.Request) {
var cTopics []string
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&cTopics); err != nil {
r.log.Error("decoding request failure", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
var err error
_, err = r.node.Relay().Subscribe(r.node.Relay().Context(), protocol.NewContentFilter("", cTopics...), relay.WithCacheSize(r.cacheCapacity))
if err != nil {
r.log.Error("subscribing to topics", zap.Strings("contentTopics", cTopics), zap.Error(err))
}
r.log.Debug("subscribed to topics", zap.Strings("contentTopics", cTopics))
if err != nil {
r.log.Error("writing response", zap.Error(err))
writeErrResponse(w, r.log, err, http.StatusBadRequest)
} else {
writeErrOrResponse(w, err, true)
}
}
func (r *RelayService) getV1AutoMessages(w http.ResponseWriter, req *http.Request) {
cTopic := topicFromPath(w, req, "contentTopic", r.log)
sub, err := r.node.Relay().GetSubscription(cTopic)
if err != nil {
r.log.Error("writing response", zap.Error(err))
writeErrResponse(w, r.log, err, http.StatusNotFound)
return
}
var response []*RestWakuMessage
done := false
for {
if done || len(response) > int(r.cacheCapacity) {
break
}
select {
case envelope := <-sub.Ch:
message := &RestWakuMessage{}
if err := message.FromProto(envelope.Message()); err != nil {
r.log.Error("converting protobuffer msg into rest msg", zap.Error(err))
} else {
response = append(response, message)
}
case <-req.Context().Done():
done = true
default:
done = true
}
}
writeErrOrResponse(w, nil, response)
}
func (r *RelayService) postV1AutoMessage(w http.ResponseWriter, req *http.Request) {
var restMessage *RestWakuMessage
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&restMessage); err != nil {
r.log.Error("decoding request failure", zap.Error(err))
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
message, err := restMessage.ToProto()
if err != nil {
writeErrOrResponse(w, err, nil)
return
}
if err = server.AppendRLNProof(r.node, message); err != nil {
writeErrOrResponse(w, err, nil)
return
}
_, err = r.node.Relay().Publish(req.Context(), message)
if err != nil {
r.log.Error("publishing message", zap.Error(err))
if err == pb.ErrMissingPayload || err == pb.ErrMissingContentTopic || err == pb.ErrInvalidMetaLength {
writeErrResponse(w, r.log, err, http.StatusBadRequest)
return
}
writeErrResponse(w, r.log, err, http.StatusBadRequest)
} else {
w.WriteHeader(http.StatusOK)
}
}
-303
View File
@@ -1,303 +0,0 @@
package rest
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"
"github.com/go-chi/chi/v5"
"github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/require"
"github.com/waku-org/go-waku/tests"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/utils"
"google.golang.org/protobuf/proto"
)
func makeRelayService(t *testing.T, mux *chi.Mux) *RelayService {
options := node.WithWakuRelayAndMinPeers(0)
n, err := node.New(options)
require.NoError(t, err)
err = n.Start(context.Background())
require.NoError(t, err)
return NewRelayService(n, mux, 3, utils.Logger())
}
func TestPostV1Message(t *testing.T) {
router := chi.NewRouter()
testTopic := "test"
r := makeRelayService(t, router)
msg := &RestWakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "abc",
Timestamp: utils.GetUnixEpoch(),
}
msgJSONBytes, err := json.Marshal(msg)
require.NoError(t, err)
_, err = r.node.Relay().Subscribe(context.Background(), protocol.NewContentFilter(testTopic))
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/relay/v1/messages/"+testTopic, bytes.NewReader(msgJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
}
func TestRelaySubscription(t *testing.T) {
router := chi.NewRouter()
r := makeRelayService(t, router)
// Wait for node to start
time.Sleep(500 * time.Millisecond)
topics := []string{"test"}
topicsJSONBytes, err := json.Marshal(topics)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, routeRelayV1Subscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
// Test max messages in subscription
now := *utils.GetUnixEpoch()
_, err = r.node.Relay().Publish(context.Background(),
tests.CreateWakuMessage("test", proto.Int64(now+1)), relay.WithPubSubTopic("test"))
require.NoError(t, err)
_, err = r.node.Relay().Publish(context.Background(),
tests.CreateWakuMessage("test", proto.Int64(now+2)), relay.WithPubSubTopic("test"))
require.NoError(t, err)
_, err = r.node.Relay().Publish(context.Background(),
tests.CreateWakuMessage("test", proto.Int64(now+3)), relay.WithPubSubTopic("test"))
require.NoError(t, err)
// Wait for the messages to be processed
time.Sleep(5 * time.Millisecond)
// Test deletion
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodDelete, routeRelayV1Subscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
}
func TestRelayGetV1Messages(t *testing.T) {
router := chi.NewRouter()
router1 := chi.NewRouter()
serviceA := makeRelayService(t, router)
serviceB := makeRelayService(t, router1)
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", serviceB.node.Host().ID().String()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range serviceB.node.Host().Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
err = serviceA.node.DialPeerWithMultiAddress(context.Background(), addr)
require.NoError(t, err)
// Wait for the dial to complete
time.Sleep(1 * time.Second)
topics := []string{"test"}
topicsJSONBytes, err := json.Marshal(topics)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, routeRelayV1Subscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Wait for the subscription to be started
time.Sleep(1 * time.Second)
ephemeral := true
msg := &RestWakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "test",
Timestamp: utils.GetUnixEpoch(),
Ephemeral: &ephemeral,
}
msgJsonBytes, err := json.Marshal(msg)
require.NoError(t, err)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, "/relay/v1/messages/test", bytes.NewReader(msgJsonBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Wait for the message to be received
time.Sleep(1 * time.Second)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, "/relay/v1/messages/test", bytes.NewReader([]byte{}))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
var messages []*pb.WakuMessage
err = json.Unmarshal(rr.Body.Bytes(), &messages)
require.NoError(t, err)
require.Len(t, messages, 1)
require.Equal(t, *messages[0].Ephemeral, true)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, "/relay/v1/messages/test", bytes.NewReader([]byte{}))
router1.ServeHTTP(rr, req)
require.Equal(t, http.StatusNotFound, rr.Code)
}
func TestPostAutoV1Message(t *testing.T) {
router := chi.NewRouter()
_ = makeRelayService(t, router)
msg := &RestWakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: "/toychat/1/huilong/proto",
Timestamp: utils.GetUnixEpoch(),
}
msgJSONBytes, err := json.Marshal(msg)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, routeRelayV1AutoMessages, bytes.NewReader(msgJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
}
func TestRelayAutoSubUnsub(t *testing.T) {
router := chi.NewRouter()
r := makeRelayService(t, router)
// Wait for node to start
time.Sleep(500 * time.Millisecond)
cTopic1 := "/toychat/1/huilong/proto"
cTopics := []string{cTopic1}
topicsJSONBytes, err := json.Marshal(cTopics)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, routeRelayV1AutoSubscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
// Test publishing messages after subscription
now := *utils.GetUnixEpoch()
_, err = r.node.Relay().Publish(context.Background(),
tests.CreateWakuMessage(cTopic1, proto.Int64(now+1)))
require.NoError(t, err)
// Wait for the messages to be processed
time.Sleep(5 * time.Millisecond)
// Test deletion
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodDelete, routeRelayV1AutoSubscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
cTopics = append(cTopics, "test")
topicsJSONBytes, err = json.Marshal(cTopics)
require.NoError(t, err)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, routeRelayV1AutoSubscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusBadRequest, rr.Code)
}
func TestRelayGetV1AutoMessages(t *testing.T) {
router := chi.NewRouter()
router1 := chi.NewRouter()
serviceA := makeRelayService(t, router)
serviceB := makeRelayService(t, router1)
hostInfo, err := multiaddr.NewMultiaddr(fmt.Sprintf("/p2p/%s", serviceB.node.Host().ID().String()))
require.NoError(t, err)
var addr multiaddr.Multiaddr
for _, a := range serviceB.node.Host().Addrs() {
addr = a.Encapsulate(hostInfo)
break
}
err = serviceA.node.DialPeerWithMultiAddress(context.Background(), addr)
require.NoError(t, err)
// Wait for the dial to complete
time.Sleep(1 * time.Second)
cTopic1 := "/toychat/1/huilong/proto"
cTopics := []string{cTopic1}
topicsJSONBytes, err := json.Marshal(cTopics)
require.NoError(t, err)
rr := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, routeRelayV1AutoSubscriptions, bytes.NewReader(topicsJSONBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
require.Equal(t, "true", rr.Body.String())
// Wait for the subscription to be started
time.Sleep(1 * time.Second)
msg := &RestWakuMessage{
Payload: []byte{1, 2, 3},
ContentTopic: cTopic1,
Timestamp: utils.GetUnixEpoch(),
}
msgJsonBytes, err := json.Marshal(msg)
require.NoError(t, err)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodPost, routeRelayV1AutoMessages, bytes.NewReader(msgJsonBytes))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
// Wait for the message to be received
time.Sleep(1 * time.Second)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", routeRelayV1AutoMessages, url.QueryEscape(cTopic1)), bytes.NewReader([]byte{}))
router.ServeHTTP(rr, req)
require.Equal(t, http.StatusOK, rr.Code)
var messages []*pb.WakuMessage
err = json.Unmarshal(rr.Body.Bytes(), &messages)
require.NoError(t, err)
require.Len(t, messages, 1)
rr = httptest.NewRecorder()
req, _ = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/%s", routeRelayV1AutoMessages, url.QueryEscape(cTopic1)), bytes.NewReader([]byte{}))
router1.ServeHTTP(rr, req)
require.Equal(t, http.StatusNotFound, rr.Code)
}
-189
View File
@@ -1,189 +0,0 @@
package rest
import (
"context"
"encoding/base64"
"errors"
"net/http"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/multiformats/go-multiaddr"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_store"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/store"
storepb "github.com/waku-org/go-waku/waku/v2/protocol/store/pb"
"google.golang.org/protobuf/proto"
)
type StoreQueryService struct {
node *node.WakuNode
mux *chi.Mux
}
const routeStoreMessagesV1 = "/store/v3/messages"
func NewStoreQueryService(node *node.WakuNode, m *chi.Mux) *StoreQueryService {
s := &StoreQueryService{
node: node,
mux: m,
}
m.Get(routeStoreMessagesV1, s.getV3Messages)
return s
}
func getStoreParams(r *http.Request) (store.Criteria, []store.RequestOption, error) {
var options []store.RequestOption
var err error
peerAddrStr := r.URL.Query().Get("peerAddr")
var m multiaddr.Multiaddr
if peerAddrStr != "" {
m, err = multiaddr.NewMultiaddr(peerAddrStr)
if err != nil {
return nil, nil, err
}
options = append(options, store.WithPeerAddr(m))
}
includeData := false
includeDataStr := r.URL.Query().Get("includeData")
if includeDataStr != "" {
includeData, err = strconv.ParseBool(includeDataStr)
if err != nil {
return nil, nil, errors.New("invalid value for includeData. Use true|false")
}
}
options = append(options, store.IncludeData(includeData))
pubsubTopic := r.URL.Query().Get("pubsubTopic")
contentTopics := r.URL.Query().Get("contentTopics")
var contentTopicsArr []string
if contentTopics != "" {
contentTopicsArr = strings.Split(contentTopics, ",")
}
hashesStr := r.URL.Query().Get("hashes")
var hashes []pb.MessageHash
if hashesStr != "" {
hashesStrArr := strings.Split(hashesStr, ",")
for _, hashStr := range hashesStrArr {
hash, err := base64.URLEncoding.DecodeString(hashStr)
if err != nil {
return nil, nil, err
}
hashes = append(hashes, pb.ToMessageHash(hash))
}
}
isMsgHashCriteria := false
if len(hashes) != 0 {
isMsgHashCriteria = true
if pubsubTopic != "" || len(contentTopics) != 0 {
return nil, nil, errors.New("cant use content filters while specifying message hashes")
}
} else {
if pubsubTopic == "" || len(contentTopicsArr) == 0 {
return nil, nil, errors.New("pubsubTopic and contentTopics are required")
}
}
startTimeStr := r.URL.Query().Get("startTime")
var startTime *int64
if startTimeStr != "" {
startTimeValue, err := strconv.ParseInt(startTimeStr, 10, 64)
if err != nil {
return nil, nil, err
}
startTime = &startTimeValue
}
endTimeStr := r.URL.Query().Get("endTime")
var endTime *int64
if endTimeStr != "" {
endTimeValue, err := strconv.ParseInt(endTimeStr, 10, 64)
if err != nil {
return nil, nil, err
}
endTime = &endTimeValue
}
var cursor []byte
cursorStr := r.URL.Query().Get("cursor")
if cursorStr != "" {
cursor, err = base64.URLEncoding.DecodeString(cursorStr)
if err != nil {
return nil, nil, err
}
options = append(options, store.WithCursor(cursor))
}
pageSizeStr := r.URL.Query().Get("pageSize")
ascendingStr := r.URL.Query().Get("ascending")
if ascendingStr != "" || pageSizeStr != "" {
ascending := true
pageSize := uint64(legacy_store.DefaultPageSize)
if ascendingStr != "" {
ascending, err = strconv.ParseBool(ascendingStr)
if err != nil {
return nil, nil, err
}
}
if pageSizeStr != "" {
pageSize, err = strconv.ParseUint(pageSizeStr, 10, 64)
if err != nil {
return nil, nil, err
}
if pageSize > legacy_store.MaxPageSize {
pageSize = legacy_store.MaxPageSize
}
}
options = append(options, store.WithPaging(ascending, pageSize))
}
var query store.Criteria
if isMsgHashCriteria {
query = store.MessageHashCriteria{
MessageHashes: hashes,
}
} else {
query = store.FilterCriteria{
ContentFilter: protocol.NewContentFilter(pubsubTopic, contentTopicsArr...),
TimeStart: startTime,
TimeEnd: endTime,
}
}
return query, options, nil
}
func writeStoreError(w http.ResponseWriter, code int, err error) {
writeResponse(w, &storepb.StoreQueryResponse{StatusCode: proto.Uint32(uint32(code)), StatusDesc: proto.String(err.Error())}, code)
}
func (d *StoreQueryService) getV3Messages(w http.ResponseWriter, r *http.Request) {
query, options, err := getStoreParams(r)
if err != nil {
writeStoreError(w, http.StatusBadRequest, err)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
result, err := d.node.Store().Request(ctx, query, options...)
if err != nil {
writeLegacyStoreError(w, http.StatusInternalServerError, err)
return
}
writeErrOrResponse(w, nil, result.Response())
}
-135
View File
@@ -1,135 +0,0 @@
# /store/v3/messages:
get:
summary: Gets message history
description: >
Retrieves Waku message history. The returned history
can be potentially filtered by optional request parameters.
operationId: getMessageHistory
tags:
- store
parameters:
- name: peerAddr
in: query
schema:
type: string
required: true
description: >
P2P fully qualified peer multiaddress
in the format `(ip4|ip6)/tcp/p2p/$peerId` and URL-encoded.
example: '%2Fip4%2F127.0.0.1%2Ftcp%2F60001%2Fp2p%2F16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN'
- name: includeData
in: query
schema:
type: string
description: >
Boolean indicating if the query should return messages (data) or hashes only.
A value of 'false' returns hashes only.
A value of 'true' returns hashes AND messages.
Default value is 'false'
example: 'true'
- name: pubsubTopic
in: query
schema:
type: string
description: >
The pubsub topic on which a WakuMessage is published.
If left empty, no filtering is applied.
It is also intended for pagination purposes.
It should be a URL-encoded string.
example: 'my%20pubsub%20topic'
- name: contentTopics
in: query
schema: string
description: >
Comma-separated list of content topics. When specified,
only WakuMessages that are linked to any of the given
content topics will be delivered in the get response.
It should be a URL-encoded-comma-separated string.
example: 'my%20first%20content%20topic%2Cmy%20second%20content%20topic%2Cmy%20third%20content%20topic'
- name: startTime
in: query
schema:
type: string
description: >
The inclusive lower bound on the timestamp of
queried WakuMessages. This field holds the
Unix epoch time in nanoseconds as a 64-bits
integer value.
example: '1680590945000000000'
- name: endTime
in: query
schema:
type: string
description: >
The inclusive upper bound on the timestamp of
queried WakuMessages. This field holds the
Unix epoch time in nanoseconds as a 64-bits
integer value.
example: '1680590945000000000'
- name: hashes
in: query
schema:
type: string
description: >
Comma-separated list of message hashes.
URL-base64-encoded string computed as a hash of messages.
Used to find messages by hash.
example: 'Gc4ACThW5t2QQO82huq3WnDv%2FapPPJpD%2FwJfxDxAnR0%3D'
- name: cursor
in: query
schema:
type: string
description: >
Cursor field intended for pagination purposes.
URL-base64-encoded string computed as a hash of a message.
It could be empty for retrieving the first page,
and will be returned from the GET response so that
it can be part of the next page request.
example: 'Gc4ACThW5t2QQO82huq3WnDv%2FapPPJpD%2FwJfxDxAnR0%3D'
- name: pageSize
in: query
schema:
type: string
description: >
Number of messages to retrieve per page
example: '5'
- name: ascending
in: query
schema:
type: string
description: >
"true" for paging forward, "false" for paging backward.
If not specified or if specified with an invalid value, the default is "true".
example: "true"
responses:
'200':
description: Waku message history.
content:
application/json:
schema:
$ref: '#/components/schemas/StoreQueryResponse'
'400':
description: Bad request error.
content:
text/plain:
type: string
'412':
description: Precondition failed.
content:
text/plain:
type: string
'500':
description: Internal server error.
content:
text/plain:
type: string
-83
View File
@@ -1,83 +0,0 @@
package rest
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"github.com/go-chi/chi/v5"
"go.uber.org/zap"
)
// The functions writes error response in plain text format with specified statusCode
func writeErrResponse(w http.ResponseWriter, log *zap.Logger, err error, statusCode int) {
w.WriteHeader(statusCode)
_, err = w.Write([]byte(err.Error()))
if err != nil {
log.Error("error while writing response", zap.Error(err))
}
}
// This function writes error or response in json format with statusCode as 500 in case of error
func writeErrOrResponse(w http.ResponseWriter, err error, value interface{}) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
jsonResponse, err := json.Marshal(value)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = w.Write(jsonResponse)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
// This function writes a response in json format
func writeResponse(w http.ResponseWriter, value interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
jsonResponse, err := json.Marshal(value)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// w.Write implicitly writes a 200 status code
// and only once we can write 2xx-5xx status code
// so any statusCode apart from 1xx being written to the header, will be ignored.
w.WriteHeader(code)
_, _ = w.Write(jsonResponse)
}
func topicFromPath(w http.ResponseWriter, req *http.Request, field string, logger *zap.Logger) string {
topic := chi.URLParam(req, field)
if topic == "" {
errMissing := fmt.Errorf("missing %s", field)
writeGetMessageErr(w, errMissing, http.StatusBadRequest, logger)
return ""
}
topic, err := url.QueryUnescape(topic)
if err != nil {
errInvalid := fmt.Errorf("invalid %s format", field)
writeGetMessageErr(w, errInvalid, http.StatusBadRequest, logger)
return ""
}
return topic
}
func writeGetMessageErr(w http.ResponseWriter, err error, code int, logger *zap.Logger) {
// write status before the body
w.WriteHeader(code)
logger.Error("get message", zap.Error(err))
if _, err := w.Write([]byte(err.Error())); err != nil {
logger.Error("writing response", zap.Error(err))
}
}
-104
View File
@@ -1,104 +0,0 @@
package rest
import (
"context"
"fmt"
"net/http"
"sync"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/waku-org/go-waku/waku/v2/node"
"go.uber.org/zap"
)
type WakuRest struct {
node *node.WakuNode
server *http.Server
log *zap.Logger
relayService *RelayService
filterService *FilterService
}
type RestConfig struct {
Address string
Port uint
EnablePProf bool
EnableAdmin bool
RelayCacheCapacity uint
FilterCacheCapacity uint
}
func NewWakuRest(node *node.WakuNode, config RestConfig, log *zap.Logger) *WakuRest {
wrpc := new(WakuRest)
wrpc.log = log.Named("rest")
mux := chi.NewRouter()
mux.Use(middleware.Logger)
mux.Use(middleware.NoCache)
mux.Use(func(h http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
h.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
})
if config.EnablePProf {
mux.Mount("/debug", middleware.Profiler())
}
_ = NewDebugService(node, mux)
_ = NewHealthService(node, mux)
_ = NewStoreQueryService(node, mux)
_ = NewLegacyStoreService(node, mux)
_ = NewLightpushService(node, mux, log)
listenAddr := fmt.Sprintf("%s:%d", config.Address, config.Port)
server := &http.Server{
Addr: listenAddr,
Handler: mux,
}
wrpc.node = node
wrpc.server = server
if node.Relay() != nil {
relayService := NewRelayService(node, mux, config.RelayCacheCapacity, log)
wrpc.relayService = relayService
}
if config.EnableAdmin {
_ = NewAdminService(node, mux, wrpc.log)
}
if node.FilterLightnode() != nil {
filterService := NewFilterService(node, mux, int(config.FilterCacheCapacity), log)
server.RegisterOnShutdown(func() {
filterService.Stop()
})
wrpc.filterService = filterService
}
return wrpc
}
func (r *WakuRest) Start(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
if r.node.FilterLightnode() != nil {
go r.filterService.Start(ctx)
}
go func() {
_ = r.server.ListenAndServe()
}()
r.log.Info("server started", zap.String("addr", r.server.Addr))
}
func (r *WakuRest) Stop(ctx context.Context) error {
r.log.Info("shutting down server")
return r.server.Shutdown(ctx)
}
-23
View File
@@ -1,23 +0,0 @@
//go:build !gowaku_no_rln
// +build !gowaku_no_rln
package server
import (
"fmt"
"github.com/waku-org/go-waku/waku/v2/node"
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
"github.com/waku-org/go-waku/waku/v2/protocol/rln"
)
func AppendRLNProof(node *node.WakuNode, msg *pb.WakuMessage) error {
_, rlnEnabled := node.RLNRelay().(*rln.WakuRLNRelay)
if rlnEnabled {
err := node.RLNRelay().AppendRLNProof(msg, node.Timesource().Now())
if err != nil {
return fmt.Errorf("could not append rln proof: %w", err)
}
}
return nil
}
-50
View File
@@ -1,50 +0,0 @@
package server
import (
"encoding/base64"
"strings"
"github.com/libp2p/go-libp2p/core/protocol"
"github.com/waku-org/go-waku/waku/v2/protocol/filter"
"github.com/waku-org/go-waku/waku/v2/protocol/legacy_store"
"github.com/waku-org/go-waku/waku/v2/protocol/lightpush"
"github.com/waku-org/go-waku/waku/v2/protocol/relay"
"github.com/waku-org/go-waku/waku/v2/protocol/store"
)
func IsWakuProtocol(protocol protocol.ID) bool {
return protocol == filter.FilterPushID_v20beta1 ||
protocol == filter.FilterSubscribeID_v20beta1 ||
protocol == relay.WakuRelayID_v200 ||
protocol == lightpush.LightPushID_v20beta1 ||
protocol == legacy_store.StoreID_v20beta4 ||
protocol == store.StoreQueryID_v300
}
type Base64URLByte []byte
// UnmarshalText is used by json.Unmarshal to decode both url-safe and standard
// base64 encoded strings with and without padding
func (h *Base64URLByte) UnmarshalText(b []byte) error {
inputValue := ""
if b != nil {
inputValue = string(b)
}
enc := base64.StdEncoding
if strings.ContainsAny(inputValue, "-_") {
enc = base64.URLEncoding
}
if len(inputValue)%4 != 0 {
enc = enc.WithPadding(base64.NoPadding)
}
decodedBytes, err := enc.DecodeString(inputValue)
if err != nil {
return err
}
*h = decodedBytes
return nil
}
-33
View File
@@ -1,33 +0,0 @@
{
pkgs ? import <nixpkgs> { },
self ? ./.,
subPkgs ? "cmd/waku",
ldflags ? [],
output ? null,
commit ? builtins.substring 0 7 (self.rev or "dirty"),
version ? builtins.readFile ./VERSION,
}:
pkgs.buildGo121Module {
name = "go-waku";
src = self;
subPackages = subPkgs;
tags = ["gowaku_no_rln"];
ldflags = [
"-X github.com/waku-org/go-waku/waku/v2/node.GitCommit=${commit}"
"-X github.com/waku-org/go-waku/waku/v2/node.Version=${version}"
] ++ ldflags;
doCheck = false;
# Otherwise library would be just called bin/c.
postInstall = if builtins.isString output then ''
mv $out/bin/* $out/bin/${output}
'' else "";
# FIXME: This needs to be manually changed when updating modules.
vendorHash = "sha256-TrKlv3UHhFl+1HviEYFTmOpF+UiVdL6h53IkJXBFsRo=";
# Fix for 'nix run' trying to execute 'go-waku'.
meta = { mainProgram = "waku"; };
}
-30
View File
@@ -1,30 +0,0 @@
# BUILD IMAGE --------------------------------------------------------
FROM golang:1.20 as builder
WORKDIR /app
COPY . .
# Build the final node binary
RUN make -j$(nproc) build
# ACTUAL IMAGE -------------------------------------------------------
FROM debian:12.1-slim
LABEL maintainer="romanzac@status.im"
LABEL source="https://github.com/waku-org/go-waku"
LABEL description="go-waku: Waku V2 node - test"
# 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
COPY --from=builder /app/build/waku /usr/bin/waku
ENTRYPOINT ["/usr/bin/waku"]
# By default just show help if called without arguments
CMD ["--help"]
+2 -2
View File
@@ -30,7 +30,7 @@ import (
)
func main() {
discoveryURL := "enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im"
discoveryURL := "enrtree://AOGECG2SPND25EEFMAJ5WF3KSGJNSGV356DSTL2YVLLZWIV6SAYBM@test.waku.nodes.status.im"
nodes, err := dnsdisc.RetrieveNodes(context.Background(), discoveryURL)
if err != nil {
panic(err)
@@ -40,4 +40,4 @@ func main() {
}
```
`dnsdisc.RetrieveNodes` can also accept a `WithNameserver(nameserver string)` option which can be used to specify the nameserver to use to retrieve the TXT record from the domain name
`dnsdisc.RetrieveNodes` can also accept a `WithNameserver(nameserver string)` option which can be used to specify the nameserver to use to retrieve the TXT record from the domain name
+1 -1
View File
@@ -38,7 +38,7 @@ One of these options must be specified when instantiating a node supporting the
```go
...
peerAddr, err := multiaddr.NewMultiaddr("/dns4/node-01.do-ams3.waku.test.status.im/tcp/30303/p2p/16Uiu2HAkykgaECHswi3YKJ5dMLbq2kPVCo89fcyTd38UcQD6ej5W")
peerAddr, err := multiaddr.NewMultiaddr("/dns4/node-01.do-ams3.wakuv2.test.statusim.net/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ")
if err != nil {
panic(err)
}
+11 -6
View File
@@ -65,17 +65,22 @@ if err != nil {
```
To send a message, it needs to be wrapped into a [`WakuMessage`](https://rfc.vac.dev/spec/14/) protobuffer.
The payload of the message is not limited to strings. Any kind of data that can be serialized
To send a message, it needs to be wrapped into a [`WakuMessage`](https://rfc.vac.dev/spec/14/) protobuffer. The payload of the message is not limited to strings. Any kind of data that can be serialized
into a `[]byte` can be sent as long as it does not exceed the maximum length a message can have (~1MB)
`wakuNode.Lightpush().Publish(ctx, msg, opts...)` is used to publish a message. This function will return a message id on success, or an error if the message could not be published.
The following functions can be used to publish a message:
- `wakuNode.Lightpush().Publish(ctx, msg, opts...)` - to send a message to the default waku pubsub topic
- `wakuNode.Lightpush().PublishToTopic(ctx, msg, topic, opts...)` - to send a message to a custom pubsub topic
If no options are specified, go-waku will automatically choose the peer used to broadcast the message via Lightpush and publish the message to a pubsub topic derived from the content topic of the message. This behaviour can be controlled via options:
Both of these functions will return a message id on success, or an error if the message could not be published.
If no options are specified, go-waku will automatically choose the peer used to broadcast the message via Lightpush. This behaviour can be controlled via options:
### Options
- `lightpush.WithPubSubTopic(topic)` - broadcast the message using a custom pubsub topic
- `lightpush.WithDefaultPubsubTopic()` - broadcast the message to the default pubsub topic
- `lightpush.WithPeer(peerID)` - use an specific peer ID (which should be part of the node peerstore) to broadcast the message with
- `lightpush.WithAutomaticPeerSelection(host)` - automatically select a peer that supports lightpush protocol from the peerstore to broadcast the message with
- `lightpush.WithFastestPeerSelection(ctx)` - automatically select a peer based on its ping reply time

Some files were not shown because too many files have changed in this diff Show More