Compare commits

..

51 Commits

Author SHA1 Message Date
Tommy Verrall 34b8a0706d Update package.json 2023-07-25 16:49:45 +02:00
pierre 275417c186 fix workflows (try) 2023-07-25 16:05:01 +02:00
pierre 40a92e07d6 fix workflows 2023-07-25 15:47:24 +02:00
benedetta davico e83a12bd91 Update CHANGELOG.md 2023-07-25 15:36:59 +02:00
Tommy Verrall 381162554e Update nym-connect-publish-ubuntu.yml (#3706)
* Update nym-connect-publish-ubuntu.yml

* Update nym-connect-publish-ubuntu.yml

* Update nym-connect-publish-macos.yml

* Update nym-connect-publish-windows10.yml
2023-07-25 14:40:46 +02:00
Tommy Verrall 7a740c06fd Update nym-connect-publish-macos.yml (#3705)
* Update nym-connect-publish-macos.yml

install wasm and build

* Update nym-connect-publish-macos.yml

use the correct download
2023-07-25 13:45:32 +02:00
Tommy Verrall e65285ac7b bump versions for NC 2023-07-25 10:52:17 +02:00
mx 715a3bd687 Merge pull request #3696 from nymtech/feature/sdk-surb-example
Feature/sdk surb example
2023-07-24 07:16:03 +00:00
mfahampshire 858f1ac13c fixed clippy warning 2023-07-23 12:00:39 +02:00
pierre 3ee1328626 add alephium to supported wallets 2023-07-21 16:15:39 +02:00
pierre 36ac825b43 build(nc-desktop): sentry dsn as env var 2023-07-21 16:00:39 +02:00
mfahampshire 165f189115 ran fmt 2023-07-21 14:11:22 +02:00
mfahampshire b5fcfbe2fe added reply with surbs example to rust sdk examples dir 2023-07-21 14:04:33 +02:00
mfahampshire 7a38f1f469 added rust sdk surb example 2023-07-21 13:59:52 +02:00
pierre 5faca46235 ci: fix connect-desktop-ci workflow 2023-07-21 13:20:18 +02:00
Pierre Dommerc d780ac55b1 feat(nc-desktop): add sentry to backend (#3652) 2023-07-21 12:48:56 +02:00
Pierre Dommerc c5f7d066b0 refactor(nc-desktop): add privacy level user settings (#3664) 2023-07-21 12:48:30 +02:00
mx 8cc90be8c6 Merge pull request #3685 from nymtech/feature/network-requester-updates
Feature/network requester updates
2023-07-19 12:07:21 +00:00
mfahampshire aae96e7537 included url 2023-07-19 13:56:47 +02:00
mfahampshire 39b521bc1f updated NR guide with list explainer + info on comments in local allow list 2023-07-19 13:51:55 +02:00
mx 7339695ce8 Merge pull request #3677 from nymtech/feature/v1-1-24-docs
Feature/v1 1 25 docs (outdated branch name due to lack of release)
2023-07-19 11:43:11 +00:00
mfahampshire 0e1c9853aa version update 2023-07-19 13:16:11 +02:00
mfahampshire 76b9c669d7 * added serinko + alexia to book authors
* version bumps for next release
2023-07-19 12:45:30 +02:00
mfahampshire 553cfd098b updated sdk documentation with surb example 2023-07-19 12:41:29 +02:00
farbanas edeb8369df Merge branch 'master' into develop 2023-07-18 14:22:46 +02:00
farbanas 22b2405aa2 Merge branch 'release/v1.1.24' 2023-07-18 13:43:47 +02:00
farbanas 63254ecffe update versions and changelogs for the release 2023-07-18 13:43:30 +02:00
farbanas 407d280019 fixes to GH action 2023-07-18 11:23:15 +02:00
farbanas 1fafc126fb debugging 2023-07-18 11:06:45 +02:00
farbanas 9a51135d22 debugging 2023-07-18 10:43:27 +02:00
Tommy Verrall 1b2790da80 Merge pull request #3676 from nymtech/bugfix/#3630
[wallet] bugfix: don't send funds for pledge decrease simulation
2023-07-17 12:08:32 +02:00
Jędrzej Stuczyński f8943eebce preemptively resolving future clippy issue 2023-07-17 11:01:45 +01:00
Jędrzej Stuczyński d7b53cba40 don't send funds for pledge decrease simulation 2023-07-17 11:01:45 +01:00
mx 89d2f0ac12 Merge pull request #3665 from nymtech/dev-portal/communityupdates
Dev portal/communityupdates
2023-07-17 09:49:52 +00:00
wigy 110b4d384e fix: typo in Rust SDK docs (#3655) 2023-07-17 10:29:28 +01:00
Lorexia 4631c72c6b Update quickstart overview, delete project comments in community-applications-and-guides 2023-07-13 10:36:27 +02:00
Lorexia 12aa5f1f4f Add Minibolt to community projects list 2023-07-12 18:37:06 +02:00
Lorexia 825f25800a Add Nymster email info, update Nostr-Nym link, update deployed apps text 2023-07-12 14:44:51 +02:00
Lorexia d9b4d8fde6 Add preprocessors for build, update NIsNymUp issue, update DarkFi picture 2023-07-12 11:05:42 +02:00
Lorexia a98613d83c Update community application list, merge community and guides pages, update SUMMARY file, update overview file 2023-07-11 19:57:21 +02:00
mfahampshire 40e1243f3c Merge branch 'feature/v1-1-24-docs' of github.com:nymtech/nym into feature/v1-1-24-docs 2023-07-11 15:52:59 +02:00
mfahampshire 50d2ca0a12 updated validator docs: upgrade to 0.32.0 instructions 2023-07-11 15:52:35 +02:00
mfahampshire 34de42fe7a updated validator docs: upgrade to 0.32.0 instructions 2023-07-11 15:51:02 +02:00
serinko 6e2eaf29e7 edited NC-Matrix user manual
- changed setup for Mac
- fixed typo
2023-07-10 15:32:23 +02:00
mx 32d9baaf02 Merge pull request #3614 from twofaktor/twofaktor-requester-no-incoming-connection
[UPDATE] Fix + update network-requester-setup doc
2023-07-10 08:30:37 +00:00
mx c6b193eb4f Merge pull request #3657 from nymtech/serinko-dev-portal-patch
update NC-Matrix user manual
2023-07-10 08:17:17 +00:00
mfahampshire 0179f7648c version bumps 2023-07-10 09:56:22 +02:00
mfahampshire 45c04d63e2 removed command information from mix node + gateway guide 2023-07-10 09:44:33 +02:00
serinko 92b9edf0da edited NC-Matrix user manual
- changed setup for Mac
- fixed typo
2023-07-04 14:52:05 +00:00
⚡️2FakTor⚡️ 0f8ac1506b Update network-requester-setup.md 2023-06-28 16:07:32 +02:00
⚡️2FakTor⚡️ efdf27d1e9 Update network-requester-setup.md 2023-06-28 13:59:51 +02:00
216 changed files with 1566 additions and 17288 deletions
@@ -109,7 +109,6 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_service_provider_directory.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_name_service.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ephemera.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
+6 -1
View File
@@ -23,7 +23,12 @@ jobs:
node-version: 18
- name: Install Yarn
run: npm install -g yarn
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install project dependencies
run: cd ../.. && yarn --network-timeout 100000
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -31,10 +31,20 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: wasm32-unknown-unknown
- name: Install wasm-pack
run: |
export WASM_PACK_VERSION="v0.12.1"
curl -LO https://github.com/rustwasm/wasm-pack/releases/download/${WASM_PACK_VERSION}/wasm-pack-${WASM_PACK_VERSION}-x86_64-apple-darwin.tar.gz
tar xvzf wasm-pack-${WASM_PACK_VERSION}-x86_64-apple-darwin.tar.gz -C $HOME/.cargo/bin --strip-components=1
rm wasm-pack-${WASM_PACK_VERSION}-x86_64-apple-darwin.tar.gz
- name: Install the Apple developer certificate for code signing
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -78,6 +88,8 @@ jobs:
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SENTRY_DSN_RUST: ${{ secrets.SENTRY_DSN_RUST }}
SENTRY_DSN_JS: ${{ secrets.SENTRY_DSN_JS }}
run: yarn && yarn build
- name: Upload Artifact
@@ -103,9 +115,10 @@ jobs:
- id: release-info
name: Prepare release info
run: |
semver="${${{ github.ref_name }}##nym-connect-}" && semver="${semver##v}"
echo "version=$semver" >> "$GITHUB_OUTPUT"
echo "filename=nym-connect_$version_x64.dmg" >> "$GITHUB_OUTPUT"
ref=${{ github.ref_name }}
semver="${ref##nym-connect-}" && semver="${semver##v}"
echo "version=${semver}" >> "$GITHUB_OUTPUT"
echo "filename=nym-connect_${version}_x64_en-US.msi" >> "$GITHUB_OUTPUT"
echo "file_hash=${{ hashFiles('nym-connect/desktop/target/release/bundle/dmg/nym-connect_*_x64.dmg') }}" >> "$GITHUB_OUTPUT"
push-release-data:
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -61,6 +61,8 @@ jobs:
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SENTRY_DSN_RUST: ${{ secrets.SENTRY_DSN_RUST }}
SENTRY_DSN_JS: ${{ secrets.SENTRY_DSN_JS }}
- name: Upload Artifact
uses: actions/upload-artifact@v3
@@ -80,11 +82,11 @@ jobs:
- id: release-info
name: Prepare release info
run: |
semver="${${{ github.ref_name }}##nym-connect-}" && semver="${semver##v}"
echo "version=$semver" >> "$GITHUB_OUTPUT"
echo "filename=nym-connect_$version_amd64.AppImage" >> "$GITHUB_OUTPUT"
ref=${{ github.ref_name }}
semver="${ref##nym-connect-}" && semver="${semver##v}"
echo "version=${semver}" >> "$GITHUB_OUTPUT"
echo "filename=nym-connect_${version}_amd64.AppImage" >> "$GITHUB_OUTPUT"
echo "file_hash=${{ hashFiles('nym-connect/desktop/target/release/bundle/appimage/nym-connect_*_amd64.AppImage') }}" >> "$GITHUB_OUTPUT"
push-release-data:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/push-release-data.yml
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -79,6 +79,8 @@ jobs:
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SENTRY_DSN_RUST: ${{ secrets.SENTRY_DSN_RUST }}
SENTRY_DSN_JS: ${{ secrets.SENTRY_DSN_JS }}
run: yarn build
- name: Upload Artifact
@@ -99,9 +101,10 @@ jobs:
- id: release-info
name: Prepare release info
run: |
semver="${${{ github.ref_name }}##nym-connect-}" && semver="${semver##v}"
echo "version=$semver" >> "$GITHUB_OUTPUT"
echo "filename=nym-connect_$version_x64_en-US.msi" >> "$GITHUB_OUTPUT"
ref=${{ github.ref_name }}
semver="${ref##nym-connect-}" && semver="${semver##v}"
echo "version=${semver}" >> "$GITHUB_OUTPUT"
echo "filename=nym-connect_${version}_x64_en-US.msi" >> "$GITHUB_OUTPUT"
echo "file_hash=${{ hashFiles('nym-connect/desktop/target/release/bundle/msi/nym-connect_*_x64_en-US.msi') }}" >> "$GITHUB_OUTPUT"
push-release-data:
@@ -1,104 +0,0 @@
name: Publish Nym Connect - desktop (MacOS)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install the Apple developer certificate for code signing
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# import certificate and provisioning profile from secrets
echo -n "$APPLE_CERTIFICATE" | base64 --decode --output $CERTIFICATE_PATH
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Install app dependencies and build it
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
run: yarn && yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s_1.0.0_x64.dmg
path: nym-connect/desktop/target/release/bundle/dmg/nym-connect-s_1*_x64.dmg
retention-days: 30
- name: Clean up keychain
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/dmg/*.dmg
nym-connect/desktop/target/release/bundle/macos/*.app.tar.gz*
@@ -1,85 +0,0 @@
name: Publish NymConnect S - desktop (Ubuntu)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [custom-runner-linux]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v2
- name: Tauri dependencies
run: >
sudo apt-get update &&
sudo apt-get install -y webkit2gtk-4.0 libayatana-appindicator3-dev
continue-on-error: true
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
run: yarn
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Build app
run: yarn build
env:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s.AppImage.tar.gz
path: nym-connect/desktop/target/release/bundle/appimage/nym-connect-s_1*_amd64.AppImage
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/appimage/*.AppImage
nym-connect/desktop/target/release/bundle/appimage/*.AppImage.tar.gz*
@@ -1,104 +0,0 @@
name: Publish Nym Connect - desktop (Windows 10)
on:
workflow_dispatch:
inputs:
release_tag:
required: true
description: Release tag (nym-connect-s-v*)
type: string
default: 'nym-connect-s-v1.0.0'
push:
tags:
- nym-connect-s-v*
defaults:
run:
working-directory: nym-connect/desktop
jobs:
publish-tauri:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-connect-s-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
platform: [windows10]
runs-on: ${{ matrix.platform }}
steps:
- name: Clean up first
continue-on-error: true
working-directory: .
run: |
cd ..
del /s /q /A:H nym
rmdir /s /q nym
- uses: actions/checkout@v3
- name: Import signing certificate
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
Remove-Item -path certificate -include tempCert.txt
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
- name: Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
- name: Install app dependencies
shell: bash
run: yarn --network-timeout 100000
- name: Prepare for build
env:
RELEASE_TAG: ${{ github.ref_name || inputs.release_tag }}
run: ./scripts/pre_medium_build.sh
- name: Build and sign it
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
NYM_CONNECT_ENABLE_MEDIUM: 1
run: yarn build
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-connect-s_1.0.0_x64_en-US.msi
path: nym-connect/desktop/target/release/bundle/msi/nym-connect-s_1*_x64_en-US.msi
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
if: github.event_name == 'release'
with:
files: |
nym-connect/desktop/target/release/bundle/msi/*.msi
nym-connect/desktop/target/release/bundle/msi/*.msi.zip*
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -99,12 +99,14 @@ jobs:
files: |
nym-wallet/target/release/bundle/dmg/*.dmg
nym-wallet/target/release/bundle/macos/*.app.tar.gz*
- id: release-info
name: Prepare release info
run: |
semver="${${{ github.ref_name }}##nym-wallet-}" && semver="${semver##v}"
echo "version=$semver" >> "$GITHUB_OUTPUT"
echo "filename=nym-wallet_$version_x64.dmg" >> "$GITHUB_OUTPUT"
ref=${{ github.ref_name }}
semver="${ref##nym-wallet-}" && semver="${semver##v}"
echo "version=${semver}" >> "$GITHUB_OUTPUT"
echo "filename=nym-wallet_${version}_x64.dmg" >> "$GITHUB_OUTPUT"
echo "file_hash=${{ hashFiles('nym-wallet/target/release/bundle/dmg/nym-wallet_*_x64.dmg') }}" >> "$GITHUB_OUTPUT"
push-release-data:
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -61,6 +61,13 @@ jobs:
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: nym-wallet_1.0.0_amd64.AppImage.tar.gz
path: nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
@@ -73,9 +80,10 @@ jobs:
- id: release-info
name: Prepare release info
run: |
semver="${${{ github.ref_name }}##nym-wallet-}" && semver="${semver##v}"
echo "version=$semver" >> "$GITHUB_OUTPUT"
echo "filename=nym-wallet_$version_amd64.AppImage" >> "$GITHUB_OUTPUT"
ref=${{ github.ref_name }}
semver="${ref##nym-wallet-}" && semver="${semver##v}"
echo "version=${semver}" >> "$GITHUB_OUTPUT"
echo "filename=nym-wallet_${version}_amd64.AppImage" >> "$GITHUB_OUTPUT"
echo "file_hash=${{ hashFiles('nym-wallet/target/release/bundle/appimage/nym-wallet_*_amd64.AppImage') }}" >> "$GITHUB_OUTPUT"
push-release-data:
@@ -19,7 +19,7 @@ jobs:
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
@@ -96,12 +96,14 @@ jobs:
files: |
nym-wallet/target/release/bundle/msi/*.msi
nym-wallet/target/release/bundle/msi/*.msi.zip*
- id: release-info
name: Prepare release info
run: |
semver="${${{ github.ref_name }}##nym-wallet-}" && semver="${semver##v}"
echo "version=$semver" >> "$GITHUB_OUTPUT"
echo "filename=nym-wallet_$version_x64_en-US.msi" >> "$GITHUB_OUTPUT"
ref=${{ github.ref_name }}
semver="${ref##nym-wallet-}" && semver="${semver##v}"
echo "version=${semver}" >> "$GITHUB_OUTPUT"
echo "filename=nym-wallet_${version}_x64_en-US.msi" >> "$GITHUB_OUTPUT"
echo "file_hash=${{ hashFiles('nym-wallet/target/release/bundle/msi/nym-wallet_*_x64_en-US.msi') }}" >> "$GITHUB_OUTPUT"
push-release-data:
Generated
+31 -2788
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -34,7 +34,6 @@ members = [
"common/cosmwasm-smart-contracts/coconut-bandwidth-contract",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/ephemera",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
-12
View File
@@ -2562,17 +2562,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-ephemera-common"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"cw-utils",
"nym-contracts-common",
"schemars",
"serde",
]
[[package]]
name = "nym-gateway-client"
version = "0.1.0"
@@ -2976,7 +2965,6 @@ dependencies = [
"nym-coconut-interface",
"nym-config",
"nym-contracts-common",
"nym-ephemera-common",
"nym-group-contract-common",
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
@@ -13,7 +13,6 @@ colored = "2.0"
nym-coconut-dkg-common = { path = "../../cosmwasm-smart-contracts/coconut-dkg" }
nym-contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" }
nym-ephemera-common = { path = "../../cosmwasm-smart-contracts/ephemera" }
nym-mixnet-contract-common = { path = "../../cosmwasm-smart-contracts/mixnet-contract" }
nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-contract" }
nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
@@ -17,7 +17,7 @@ pub use nym_mixnet_contract_common::{
use url::Url;
#[cfg(feature = "nyxd-client")]
use crate::nyxd::traits::{DkgQueryClient, EphemeraQueryClient, MixnetQueryClient};
use crate::nyxd::traits::{DkgQueryClient, MixnetQueryClient};
#[cfg(feature = "nyxd-client")]
use crate::nyxd::{self, CosmWasmClient, NyxdClient, QueryNyxdClient, SigningNyxdClient};
#[cfg(feature = "nyxd-client")]
@@ -29,8 +29,6 @@ use nym_coconut_dkg_common::{types::EpochId, verification_key::ContractVKShare};
#[cfg(feature = "nyxd-client")]
use nym_coconut_interface::Base58;
#[cfg(feature = "nyxd-client")]
use nym_ephemera_common::types::JsonPeerInfo;
#[cfg(feature = "nyxd-client")]
use nym_mixnet_contract_common::{
families::{Family, FamilyHead},
mixnode::MixNodeBond,
@@ -571,26 +569,6 @@ impl<C> Client<C> {
Ok(events)
}
pub async fn get_all_ephemera_peers(&self) -> Result<Vec<JsonPeerInfo>, ValidatorClientError>
where
C: CosmWasmClient + Sync + Send,
{
let mut peers = Vec::new();
let mut start_after = None;
loop {
let mut paged_response = self.get_peers_paged(start_after.take(), None).await?;
peers.append(&mut paged_response.peers);
if let Some(start_after_res) = paged_response.start_next_after {
start_after = Some(start_after_res.to_string())
} else {
break;
}
}
Ok(peers)
}
}
// validator-api wrappers
@@ -67,7 +67,6 @@ pub struct Config {
pub(crate) group_contract_address: Option<AccountId>,
pub(crate) multisig_contract_address: Option<AccountId>,
pub(crate) coconut_dkg_contract_address: Option<AccountId>,
pub(crate) ephemera_contract_address: Option<AccountId>,
pub(crate) service_provider_contract_address: Option<AccountId>,
pub(crate) name_service_contract_address: Option<AccountId>,
// TODO: add this in later commits
@@ -134,10 +133,6 @@ impl Config {
details.contracts.coconut_dkg_contract_address.as_ref(),
prefix,
)?,
ephemera_contract_address: Self::parse_optional_account(
details.contracts.ephemera_contract_address.as_ref(),
prefix,
)?,
service_provider_contract_address: Self::parse_optional_account(
details
.contracts
@@ -268,10 +263,6 @@ impl<C> NyxdClient<C> {
self.config.service_provider_contract_address = Some(address);
}
pub fn set_ephemera_contract_address(&mut self, address: AccountId) {
self.config.ephemera_contract_address = Some(address);
}
// TODO: this should get changed into Result<&AccountId, NyxdError> (or Option<&AccountId> in future commits
// note: what unwrap is doing here is just moving a failure that would have normally
// occurred in `connect` when attempting to parse an empty address,
@@ -330,14 +321,6 @@ impl<C> NyxdClient<C> {
self.config.coconut_dkg_contract_address.as_ref().unwrap()
}
// TODO: this should get changed into Result<&AccountId, NyxdError> (or Option<&AccountId> in future commits
// note: what unwrap is doing here is just moving a failure that would have normally
// occurred in `connect` when attempting to parse an empty address,
// so it's not introducing new source of failure (just moves it)
pub fn ephemera_contract_address(&self) -> &AccountId {
self.config.ephemera_contract_address.as_ref().unwrap()
}
// The service provider directory contract is optional, so we return an Option not a Result
pub fn service_provider_contract_address(&self) -> Option<&AccountId> {
self.config.service_provider_contract_address.as_ref()
@@ -1,53 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::error::NyxdError;
use crate::nyxd::{CosmWasmClient, NyxdClient};
use async_trait::async_trait;
use nym_ephemera_common::msg::QueryMsg as EphemeraQueryMsg;
use nym_ephemera_common::peers::PagedPeerResponse;
use serde::Deserialize;
#[async_trait]
pub trait EphemeraQueryClient {
async fn query_ephemera_contract<T>(&self, query: EphemeraQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_peers_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedPeerResponse, NyxdError> {
let request = EphemeraQueryMsg::GetPeers { start_after, limit };
self.query_ephemera_contract(request).await
}
}
#[async_trait]
impl<C> EphemeraQueryClient for NyxdClient<C>
where
C: CosmWasmClient + Send + Sync,
{
async fn query_ephemera_contract<T>(&self, query: EphemeraQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
self.client
.query_contract_smart(self.ephemera_contract_address(), &query)
.await
}
}
#[async_trait]
impl<C> EphemeraQueryClient for crate::Client<C>
where
C: CosmWasmClient + Sync + Send,
{
async fn query_ephemera_contract<T>(&self, query: EphemeraQueryMsg) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
self.nyxd.query_ephemera_contract(query).await
}
}
@@ -1,43 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Fee, NyxdClient, SigningCosmWasmClient};
use async_trait::async_trait;
use nym_ephemera_common::msg::ExecuteMsg as EphemeraExecuteMsg;
use nym_ephemera_common::types::JsonPeerInfo;
#[async_trait]
pub trait EphemeraSigningClient {
async fn register_as_peer(
&self,
peer_info: JsonPeerInfo,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError>;
}
#[async_trait]
impl<C> EphemeraSigningClient for NyxdClient<C>
where
C: SigningCosmWasmClient + Send + Sync,
{
async fn register_as_peer(
&self,
peer_info: JsonPeerInfo,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = EphemeraExecuteMsg::RegisterPeer { peer_info };
self.client
.execute(
self.address(),
self.ephemera_contract_address(),
&req,
fee.unwrap_or_default(),
format!("registering {} as an ephemera peer", self.address()),
vec![],
)
.await
}
}
@@ -5,8 +5,6 @@
mod coconut_bandwidth_query_client;
mod dkg_query_client;
mod ephemera_query_client;
mod ephemera_signing_client;
mod group_query_client;
mod mixnet_query_client;
mod multisig_query_client;
@@ -26,7 +24,6 @@ mod name_service_signing_client;
pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient;
pub use dkg_query_client::DkgQueryClient;
pub use ephemera_query_client::EphemeraQueryClient;
pub use group_query_client::GroupQueryClient;
pub use mixnet_query_client::MixnetQueryClient;
pub use multisig_query_client::MultisigQueryClient;
@@ -36,7 +33,6 @@ pub use vesting_query_client::VestingQueryClient;
pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient;
pub use dkg_signing_client::DkgSigningClient;
pub use ephemera_signing_client::EphemeraSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient;
pub use name_service_signing_client::NameServiceSigningClient;
-1
View File
@@ -17,7 +17,6 @@ pub mod helpers;
pub mod legacy_helpers;
pub const NYM_DIR: &str = ".nym";
pub const DEFAULT_NYM_APIS_DIR: &str = "nym-api";
pub const DEFAULT_CONFIG_DIR: &str = "config";
pub const DEFAULT_DATA_DIR: &str = "data";
pub const DEFAULT_CONFIG_FILENAME: &str = "config.toml";
@@ -1,14 +0,0 @@
[package]
name = "nym-ephemera-common"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = { workspace = true }
cw-utils = { workspace = true }
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common" }
@@ -1,3 +0,0 @@
pub mod msg;
pub mod peers;
pub mod types;
@@ -1,31 +0,0 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::types::JsonPeerInfo;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct InstantiateMsg {
pub group_addr: String,
pub mix_denom: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
RegisterPeer { peer_info: JsonPeerInfo },
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetPeers {
limit: Option<u32>,
start_after: Option<String>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
@@ -1,24 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::types::JsonPeerInfo;
use cosmwasm_std::Addr;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct PagedPeerResponse {
pub peers: Vec<JsonPeerInfo>,
pub per_page: usize,
pub start_next_after: Option<Addr>,
}
impl PagedPeerResponse {
pub fn new(peers: Vec<JsonPeerInfo>, per_page: usize, start_next_after: Option<Addr>) -> Self {
PagedPeerResponse {
peers,
per_page,
start_next_after,
}
}
}
@@ -1,30 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema)]
pub struct JsonPeerInfo {
/// The cosmos address of the peer, used in interacting with the chain.
pub cosmos_address: Addr,
/// The TCP/IP address of the peer.
/// Expected formats:
/// 1. `<IP>:<PORT>`
/// 2. `/ip4/<IP>/tcp/<PORT>` - this is the format used by libp2p multiaddr
pub ip_address: String,
///Serialized public key.
pub public_key: String,
}
impl JsonPeerInfo {
#[must_use]
pub fn new(cosmos_address: Addr, ip_address: String, public_key: String) -> Self {
Self {
cosmos_address,
ip_address,
public_key,
}
}
}
+1 -2
View File
@@ -27,7 +27,7 @@ thiserror = "1.0.37"
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
# internal
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0", default-features = false }
nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0" }
nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
[dev-dependencies]
@@ -38,4 +38,3 @@ serde = ["serde_crate", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serd
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
symmetric = ["aes", "ctr", "cipher", "generic-array"]
sphinx-only = ["nym-sphinx-types/sphinx-only"]
-11
View File
@@ -30,7 +30,6 @@ pub struct NymContracts {
pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>,
pub coconut_dkg_contract_address: Option<String>,
pub ephemera_contract_address: Option<String>,
pub service_provider_directory_contract_address: Option<String>,
pub name_service_contract_address: Option<String>,
}
@@ -130,9 +129,6 @@ impl NymNetworkDetails {
.with_coconut_dkg_contract(Some(
var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"),
))
.with_ephemera_contract(Some(
var(var_names::EPHEMERA_CONTRACT_ADDRESS).expect("ephemera contract not set"),
))
.with_service_provider_directory_contract(get_optional_env(
var_names::SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS,
))
@@ -166,7 +162,6 @@ impl NymNetworkDetails {
coconut_dkg_contract_address: parse_optional_str(
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
),
ephemera_contract_address: parse_optional_str(mainnet::EPHEMERA_CONTRACT_ADDRESS),
service_provider_directory_contract_address: None,
name_service_contract_address: None,
},
@@ -251,12 +246,6 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_ephemera_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
self.contracts.ephemera_contract_address = contract.map(Into::into);
self
}
#[must_use]
pub fn with_service_provider_directory_contract<S: Into<String>>(
mut self,
-9
View File
@@ -20,7 +20,6 @@ pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str =
pub(crate) const GROUP_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const EPHEMERA_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/";
@@ -88,10 +87,6 @@ pub fn export_to_env() {
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
COCONUT_DKG_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::EPHEMERA_CONTRACT_ADDRESS,
EPHEMERA_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::REWARDING_VALIDATOR_ADDRESS,
REWARDING_VALIDATOR_ADDRESS,
@@ -137,10 +132,6 @@ pub fn export_to_env_if_not_set() {
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
COCONUT_DKG_CONTRACT_ADDRESS,
);
set_var_conditionally_to_default(
var_names::EPHEMERA_CONTRACT_ADDRESS,
EPHEMERA_CONTRACT_ADDRESS,
);
set_var_conditionally_to_default(
var_names::REWARDING_VALIDATOR_ADDRESS,
REWARDING_VALIDATOR_ADDRESS,
-1
View File
@@ -17,7 +17,6 @@ pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
pub const EPHEMERA_CONTRACT_ADDRESS: &str = "EPHEMERA_CONTRACT_ADDRESS";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS";
pub const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str =
+1 -5
View File
@@ -9,9 +9,5 @@ repository = { workspace = true }
[dependencies]
sphinx-packet = { version = "0.1.0" }
nym-outfox = { path = "../../../nym-outfox", optional = true }
nym-outfox = { path = "../../../nym-outfox" }
thiserror = "1"
[features]
default = ["nym-outfox"]
sphinx-only = []
-11
View File
@@ -1,13 +1,11 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(not(feature = "sphinx-only"))]
pub use nym_outfox::{
constants::MIN_PACKET_SIZE, constants::MIX_PARAMS_LEN, constants::OUTFOX_PACKET_OVERHEAD,
error::OutfoxError,
};
// re-exporting types and constants available in sphinx
#[cfg(not(feature = "sphinx-only"))]
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
pub use sphinx_packet::{
constants::{
@@ -32,7 +30,6 @@ pub enum NymPacketError {
Sphinx(#[from] sphinx_packet::Error),
#[error("Outfox error: {0}")]
#[cfg(not(feature = "sphinx-only"))]
Outfox(#[from] nym_outfox::error::OutfoxError),
#[error("{0}")]
@@ -42,13 +39,11 @@ pub enum NymPacketError {
#[allow(clippy::large_enum_variant)]
pub enum NymPacket {
Sphinx(SphinxPacket),
#[cfg(not(feature = "sphinx-only"))]
Outfox(OutfoxPacket),
}
pub enum NymProcessedPacket {
Sphinx(ProcessedPacket),
#[cfg(not(feature = "sphinx-only"))]
Outfox(OutfoxProcessedPacket),
}
@@ -59,7 +54,6 @@ impl fmt::Debug for NymPacket {
.debug_struct("NymPacket::Sphinx")
.field("len", &packet.len())
.finish(),
#[cfg(not(feature = "sphinx-only"))]
NymPacket::Outfox(packet) => f
.debug_struct("NymPacket::Outfox")
.field("len", &packet.len())
@@ -86,7 +80,6 @@ impl NymPacket {
Ok(NymPacket::Sphinx(SphinxPacket::from_bytes(bytes)?))
}
#[cfg(not(feature = "sphinx-only"))]
pub fn outfox_build<M: AsRef<[u8]>>(
payload: M,
route: &[Node],
@@ -101,7 +94,6 @@ impl NymPacket {
)?))
}
#[cfg(not(feature = "sphinx-only"))]
pub fn outfox_from_bytes(bytes: &[u8]) -> Result<NymPacket, NymPacketError> {
Ok(NymPacket::Outfox(OutfoxPacket::try_from(bytes)?))
}
@@ -109,7 +101,6 @@ impl NymPacket {
pub fn len(&self) -> usize {
match self {
NymPacket::Sphinx(packet) => packet.len(),
#[cfg(not(feature = "sphinx-only"))]
NymPacket::Outfox(packet) => packet.len(),
}
}
@@ -121,7 +112,6 @@ impl NymPacket {
pub fn to_bytes(&self) -> Result<Vec<u8>, NymPacketError> {
match self {
NymPacket::Sphinx(packet) => Ok(packet.to_bytes()),
#[cfg(not(feature = "sphinx-only"))]
NymPacket::Outfox(packet) => Ok(packet.to_bytes()?),
}
}
@@ -134,7 +124,6 @@ impl NymPacket {
NymPacket::Sphinx(packet) => {
Ok(NymProcessedPacket::Sphinx(packet.process(node_secret_key)?))
}
#[cfg(not(feature = "sphinx-only"))]
NymPacket::Outfox(mut packet) => {
let next_address = packet.decode_next_layer(node_secret_key)?;
Ok(NymProcessedPacket::Outfox(OutfoxProcessedPacket::new(
+214 -33
View File
@@ -2,6 +2,16 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array 0.14.6",
]
[[package]]
name = "aes"
version = "0.7.5"
@@ -9,7 +19,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8"
dependencies = [
"cfg-if",
"cipher",
"cipher 0.3.0",
"cpufeatures",
"ctr",
"opaque-debug 0.3.0",
@@ -38,6 +48,12 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545"
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -86,6 +102,20 @@ dependencies = [
"opaque-debug 0.2.3",
]
[[package]]
name = "blake3"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"digest 0.10.7",
]
[[package]]
name = "block-buffer"
version = "0.9.0"
@@ -159,6 +189,30 @@ dependencies = [
"keystream",
]
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher 0.4.4",
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher 0.4.4",
"poly1305",
"zeroize",
]
[[package]]
name = "cipher"
version = "0.3.0"
@@ -168,6 +222,17 @@ dependencies = [
"generic-array 0.14.6",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
name = "coconut-test"
version = "0.1.0"
@@ -199,6 +264,12 @@ version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913"
[[package]]
name = "constant_time_eq"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "cosmwasm-crypto"
version = "1.2.5"
@@ -284,6 +355,49 @@ dependencies = [
"libc",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [
"cfg-if",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-deque"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.2"
@@ -309,6 +423,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array 0.14.6",
"rand_core 0.6.4",
"typenum",
]
@@ -338,7 +453,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea"
dependencies = [
"cipher",
"cipher 0.3.0",
]
[[package]]
@@ -887,8 +1002,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1003,6 +1120,15 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array 0.14.6",
]
[[package]]
name = "instant"
version = "0.1.12"
@@ -1149,6 +1275,15 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "mixnet-vesting-integration-tests"
version = "0.1.0"
@@ -1175,6 +1310,16 @@ dependencies = [
"libm",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "nym-coconut-bandwidth"
version = "0.1.0"
@@ -1258,37 +1403,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-ephemera"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"cosmwasm-storage",
"cw-controllers",
"cw-multi-test",
"cw-storage-plus",
"cw4",
"cw4-group",
"lazy_static",
"nym-ephemera-common",
"nym-group-contract-common",
"rusty-fork",
"schemars",
"serde",
"thiserror",
]
[[package]]
name = "nym-ephemera-common"
version = "0.1.0"
dependencies = [
"cosmwasm-std",
"cw-utils",
"nym-contracts-common",
"schemars",
"serde",
]
[[package]]
name = "nym-group-contract-common"
version = "0.1.0"
@@ -1393,6 +1507,23 @@ dependencies = [
"thiserror",
]
[[package]]
name = "nym-outfox"
version = "0.1.0"
dependencies = [
"blake3",
"chacha20",
"chacha20poly1305",
"curve25519-dalek",
"getrandom 0.2.10",
"log",
"rand 0.7.3",
"rayon",
"sphinx-packet",
"thiserror",
"zeroize",
]
[[package]]
name = "nym-pemstore"
version = "0.3.0"
@@ -1440,6 +1571,7 @@ dependencies = [
name = "nym-sphinx-types"
version = "0.2.0"
dependencies = [
"nym-outfox",
"sphinx-packet",
"thiserror",
]
@@ -1541,6 +1673,17 @@ version = "0.3.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug 0.3.0",
"universal-hash",
]
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@@ -1699,6 +1842,28 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rayon"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
@@ -1831,6 +1996,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sec1"
version = "0.3.0"
@@ -2162,6 +2333,16 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle 2.4.1",
]
[[package]]
name = "url"
version = "2.3.1"
-1
View File
@@ -3,7 +3,6 @@ members = [
"coconut-bandwidth",
"coconut-dkg",
"coconut-test",
"ephemera",
"mixnet",
"mixnet-vesting-integration-tests",
"multisig/cw3-flex-multisig",
-29
View File
@@ -1,29 +0,0 @@
[package]
name = "nym-ephemera"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
nym-ephemera-common = { path = "../../common/cosmwasm-smart-contracts/ephemera" }
cosmwasm-std = { workspace = true }
cosmwasm-storage = { workspace = true }
cw-storage-plus = { workspace = true }
cw-controllers = { workspace = true }
cw4 = { workspace = true }
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = "1.0.23"
[dev-dependencies]
cw-multi-test = { workspace = true }
cw4-group = { path = "../multisig/cw4-group" }
nym-group-contract-common = { path = "../../common/cosmwasm-smart-contracts/group-contract" }
lazy_static = "1.4"
rusty-fork = "0.3"
-97
View File
@@ -1,97 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ContractError;
use crate::peers::queries::query_peers_paged;
use crate::peers::transactions::try_register_peer;
use crate::state::{State, STATE};
use cosmwasm_std::{
entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response,
};
use cw4::Cw4Contract;
use nym_ephemera_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
/// Instantiate the contract.
///
/// `deps` contains Storage, API and Querier
/// `env` contains block, message and contract info
/// `msg` is the contract initialization message, sort of like a constructor call.
#[entry_point]
pub fn instantiate(
deps: DepsMut<'_>,
_env: Env,
_info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, ContractError> {
let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
ContractError::InvalidGroup {
addr: msg.group_addr.clone(),
}
})?);
let state = State {
group_addr,
mix_denom: msg.mix_denom,
};
STATE.save(deps.storage, &state)?;
Ok(Response::default())
}
/// Handle an incoming message
#[entry_point]
pub fn execute(
deps: DepsMut<'_>,
_env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::RegisterPeer { peer_info } => try_register_peer(deps, info, peer_info),
}
}
#[entry_point]
pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
let response = match msg {
QueryMsg::GetPeers { limit, start_after } => {
to_binary(&query_peers_paged(deps, start_after, limit)?)?
}
};
Ok(response)
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::Addr;
#[test]
fn initialize_contract() {
let mut deps = mock_dependencies();
let env = mock_env();
let init_msg = InstantiateMsg {
group_addr: "group_addr".to_string(),
mix_denom: "uatom".to_string(),
};
let sender = mock_info("sender", &[]);
let res = instantiate(deps.as_mut(), env, sender, init_msg);
assert!(res.is_ok());
let expected_state = State {
group_addr: Cw4Contract::new(Addr::unchecked("group_addr")),
mix_denom: "uatom".to_string(),
};
let state = STATE.load(deps.as_ref().storage).unwrap();
assert_eq!(state, expected_state);
}
}
-21
View File
@@ -1,21 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::StdError;
use thiserror::Error;
/// Custom errors for contract failure conditions.
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error(transparent)]
Std(#[from] StdError),
#[error("Group contract invalid address '{addr}'")]
InvalidGroup { addr: String },
#[error("This potential ephemera peer is not in the ephemera group")]
Unauthorized,
#[error("This sender is already registered")]
AlreadyRegistered,
}
-8
View File
@@ -1,8 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod error;
mod peers;
mod state;
mod support;
-6
View File
@@ -1,6 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod queries;
pub mod storage;
pub mod transactions;
-164
View File
@@ -1,164 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::peers::storage::{PEERS, PEERS_PAGE_DEFAULT_LIMIT, PEERS_PAGE_MAX_LIMIT};
use cosmwasm_std::{Deps, Order, StdResult};
use cw_storage_plus::Bound;
use nym_ephemera_common::peers::PagedPeerResponse;
pub fn query_peers_paged(
deps: Deps<'_>,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<PagedPeerResponse> {
let limit = limit
.unwrap_or(PEERS_PAGE_DEFAULT_LIMIT)
.min(PEERS_PAGE_MAX_LIMIT) as usize;
let addr = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?;
let start = addr.map(Bound::exclusive);
let peers = PEERS
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|res| res.map(|item| item.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = peers
.last()
.map(|peer_info| peer_info.cosmos_address.clone());
Ok(PagedPeerResponse::new(peers, limit, start_next_after))
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::peers::storage::{PEERS_PAGE_DEFAULT_LIMIT, PEERS_PAGE_MAX_LIMIT};
use crate::support::tests::fixtures::peer_fixture;
use crate::support::tests::helpers::init_contract;
use cosmwasm_std::DepsMut;
fn fill_peers(deps: DepsMut<'_>, size: usize) {
for n in 0..size {
let peer = peer_fixture(&format!("peer{}", n));
PEERS
.save(deps.storage, peer.cosmos_address.clone(), &peer)
.unwrap();
}
}
fn remove_peers(deps: DepsMut<'_>, size: usize) {
for n in 0..size {
let peer = peer_fixture(&format!("peer{}", n));
PEERS.remove(deps.storage, peer.cosmos_address);
}
}
#[test]
fn peers_empty_on_init() {
let deps = init_contract();
let page1 = query_peers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(0, page1.peers.len() as u32);
}
#[test]
fn peers_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_peers(deps.as_mut(), 1000);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.peers.len() as u32);
remove_peers(deps.as_mut(), 1000);
}
#[test]
fn peers_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
fill_peers(deps.as_mut(), 1000);
// query without explicitly setting a limit
let page1 = query_peers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(PEERS_PAGE_DEFAULT_LIMIT, page1.peers.len() as u32);
remove_peers(deps.as_mut(), 1000);
}
#[test]
fn peers_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * PEERS_PAGE_MAX_LIMIT;
fill_peers(deps.as_mut(), 1000);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
// we default to a decent sized upper bound instead
let expected_limit = PEERS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.peers.len() as u32);
remove_peers(deps.as_mut(), 1000);
}
#[test]
fn peers_pagination_works() {
let mut deps = init_contract();
let per_page = 2;
fill_peers(deps.as_mut(), 1);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
// page should have 1 result on it
assert_eq!(1, page1.peers.len());
remove_peers(deps.as_mut(), 1);
fill_peers(deps.as_mut(), 2);
// page1 should have 2 results on it
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.peers.len());
remove_peers(deps.as_mut(), 2);
fill_peers(deps.as_mut(), 3);
// page1 still has 2 results
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.peers.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_peers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.peers.len());
remove_peers(deps.as_mut(), 3);
fill_peers(deps.as_mut(), 4);
let page1 = query_peers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_peers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.peers.len());
remove_peers(deps.as_mut(), 4);
}
}
-11
View File
@@ -1,11 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_storage_plus::Map;
use nym_ephemera_common::types::JsonPeerInfo;
pub(crate) const PEERS_PAGE_MAX_LIMIT: u32 = 75;
pub(crate) const PEERS_PAGE_DEFAULT_LIMIT: u32 = 50;
pub(crate) const PEERS: Map<'_, Addr, JsonPeerInfo> = Map::new("prs");
@@ -1,63 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ContractError;
use crate::peers::storage::PEERS;
use crate::state::STATE;
use cosmwasm_std::{DepsMut, MessageInfo, Response};
use nym_ephemera_common::types::JsonPeerInfo;
pub fn try_register_peer(
deps: DepsMut<'_>,
info: MessageInfo,
peer_info: JsonPeerInfo,
) -> Result<Response, ContractError> {
if PEERS.may_load(deps.storage, info.sender.clone())?.is_none() {
if STATE
.load(deps.storage)?
.group_addr
.is_voting_member(&deps.querier, &info.sender, None)?
.is_some()
{
PEERS.save(deps.storage, info.sender, &peer_info)?;
Ok(Default::default())
} else {
Err(ContractError::Unauthorized {})
}
} else {
Err(ContractError::AlreadyRegistered)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::support::tests::fixtures::peer_fixture;
use crate::support::tests::helpers;
use crate::support::tests::helpers::GROUP_MEMBERS;
use cosmwasm_std::testing::mock_info;
use cw4::Member;
#[test]
fn peer_registration() {
let mut deps = helpers::init_contract();
let peer_info = peer_fixture("owner");
let info = mock_info("owner", &[]);
let ret = try_register_peer(deps.as_mut(), info.clone(), peer_info.clone()).unwrap_err();
assert_eq!(ret, ContractError::Unauthorized);
GROUP_MEMBERS.lock().unwrap().push((
Member {
addr: "owner".to_string(),
weight: 10,
},
1,
));
try_register_peer(deps.as_mut(), info.clone(), peer_info.clone()).unwrap();
let ret = try_register_peer(deps.as_mut(), info, peer_info).unwrap_err();
assert_eq!(ret, ContractError::AlreadyRegistered);
}
}
-16
View File
@@ -1,16 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw4::Cw4Contract;
use cw_storage_plus::Item;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
// unique items
pub const STATE: Item<State> = Item::new("state");
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct State {
pub mix_denom: String,
pub group_addr: Cw4Contract,
}
-5
View File
@@ -1,5 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(test)]
pub mod tests;
@@ -1,15 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use nym_ephemera_common::types::JsonPeerInfo;
pub const TEST_MIX_DENOM: &str = "unym";
pub fn peer_fixture(cosmos_address: &str) -> JsonPeerInfo {
JsonPeerInfo {
cosmos_address: Addr::unchecked(cosmos_address),
ip_address: "127.0.0.1".to_string(),
public_key: "random_key".to_string(),
}
}
@@ -1,74 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::instantiate;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
use cosmwasm_std::{
from_binary, to_binary, ContractResult, Empty, MemoryStorage, OwnedDeps, QuerierResult,
SystemResult, WasmQuery,
};
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
use lazy_static::lazy_static;
use nym_ephemera_common::msg::InstantiateMsg;
use std::sync::Mutex;
use super::fixtures::TEST_MIX_DENOM;
pub const ADMIN_ADDRESS: &str = "admin address";
pub const GROUP_CONTRACT: &str = "group contract address";
lazy_static! {
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
}
fn querier_handler(query: &WasmQuery) -> QuerierResult {
let bin = match query {
WasmQuery::Smart { contract_addr, msg } => {
if contract_addr != GROUP_CONTRACT {
panic!("Not supported");
}
match from_binary(msg) {
Ok(Cw4QueryMsg::Member { addr, at_height }) => {
let weight = GROUP_MEMBERS.lock().unwrap().iter().find_map(|(m, h)| {
if m.addr == addr {
if let Some(height) = at_height {
if height != *h {
return None;
}
}
Some(m.weight)
} else {
None
}
});
to_binary(&MemberResponse { weight }).unwrap()
}
Ok(Cw4QueryMsg::ListMembers { .. }) => {
let members = GROUP_MEMBERS
.lock()
.unwrap()
.iter()
.map(|m| m.0.clone())
.collect();
to_binary(&MemberListResponse { members }).unwrap()
}
_ => panic!("Not supported"),
}
}
_ => panic!("Not supported"),
};
SystemResult::Ok(ContractResult::Ok(bin))
}
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
deps.querier.update_wasm(querier_handler);
let msg = InstantiateMsg {
group_addr: GROUP_CONTRACT.to_string(),
mix_denom: TEST_MIX_DENOM.to_string(),
};
let env = mock_env();
let info = mock_info(ADMIN_ADDRESS, &[]);
instantiate(deps.as_mut(), env, info, msg).unwrap();
deps
}
@@ -1,5 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod fixtures;
pub mod helpers;
@@ -22,7 +22,7 @@ nym-mixnet-contract = { path = "../mixnet" }
nym-vesting-contract = { path = "../vesting" }
# other local dependencies
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "sphinx-only"] }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
# external dependencies
rand_chacha = "0.2"
+1 -1
View File
@@ -42,7 +42,7 @@ semver = { version = "1.0.16", default-features = false }
[dev-dependencies]
cosmwasm-schema = { workspace = true }
rand_chacha = "0.2"
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "sphinx-only"] }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
[build-dependencies]
vergen = { version = "=7.4.3", default-features = false, features = ["build", "git", "rustc"] }
@@ -25,6 +25,6 @@ vergen = { version = "=7.4.3", default-features = false, features = ["build", "g
[dev-dependencies]
anyhow = "1.0.40"
cw-multi-test = { workspace = true }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "sphinx-only"] }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
rand_chacha = "0.2"
rstest = "0.17.0"
+4 -4
View File
@@ -1,6 +1,6 @@
[book]
title = "Nym Developer Portal"
authors = ["Max Hampshire"]
authors = ["Max Hampshire, Serinko, Alexia Lorenza Martinel"]
language = "en"
multilingual = false
src = "src"
@@ -47,10 +47,10 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
# variables preprocessor: import variables into files
# https://gitlab.com/tglman/mdbook-variables/
[preprocessor.variables.variables]
# code prerequisites versions
minimum_rust_version = "1.66"
# TODO remove this in place of develop in next release
platform_release_version = "v1.1.21"
# vars for links: TODO think on how to streamline updating
platform_release_version = "v1.1.25"
wallet_release_version = "v1.2.7"
[preprocessor.last-changed]
command = "mdbook-last-changed"
+1 -2
View File
@@ -40,8 +40,7 @@
# Community Resources
- [Nym DevRel AMAs](community-resources/ama.md)
- [Community Applications](community-resources/community-applications.md)
- [Community Guides](community-resources/community-guides.md)
- [Community Applications and Guides](community-resources/community-applications-and-guides.md)
- [Change Service Grantee Information](info-request.md)
- [Rewards FAQ](community-resources/rewards-faq.md)
---
@@ -0,0 +1,116 @@
# Community Applications
We love seeing our developer community create applications using Nym. If you would like to share your application with the community, please submit a pull request to the `main` branch of the `nymtech/dev-portal` [repository](https://github.com/nymtech/dev-portal).
## <img src='../images/profile_picture/pastenym_ntv_pp.png' style="float: right; width: 75px; height: 75px;">Pastenym
>A [pastebin](https://pastebin.com) inspired project, offer a solution for sharing text with Nym products to offer full anonymity, even on metadata level.
* [Github](https://github.com/notrustverify/pastenym)
* [Deployed App](https://pastenym.ch)
## <img src='../images/profile_picture/pineappleproxy_pp.png' style="float: right; width: 75px; height: 75px;">Nostr-Nym
> [Nostr-nym](https://github.com/notrustverify/nostr-nym) offer a solution to use [Nostr](https://nostr.how/en/what-is-nostr) protocol by giving the possibility to run a relay on mixnet. By using a nostr client compatible with the mixnet, users can protect their privacy to be able to use Nostr has the want, without being observed.
* [Github](https://github.com/notrustverify/nostr-nym)
* [Deployed App](https://nostrnym.pnproxy.org/)
* [Documentation](https://blog.notrustverify.ch/nostr-relay-on-nym)
## <img src='../images/profile_picture/ethereum_rpc_spook_pp.png' style="float: right; width: 75px; height: 75px;"> Spook
> Ethereum RPC request mixer uses the Nym network mixing service to anonymize RPC requests to the Ethereum network without revealing sensitive data and metadata.
* [Github](https://github.com/EdenBlockVC/spook)
## <img src='../images/profile_picture/ethereum_transaction_broadcaster_root_pp.png' style="float: right; width: 75px; height: 75px;"> Ethereum Transaction Broadcaster
> Ethereum Transaction Broadcaster that uses the Nym Mixnet to provide privacy and anonymity for transactions on the Ethereum network command-line interface.
* [Github](https://github.com/noot/nym-ethtx)
## <img src='../images/profile_picture/nymdrive_saleel_pp.png' style="float: right; width: 75px; height: 75px;">NymDrive
> An open-source, decentralized, E2E encrypted, privacy friendly alternative to Google Drive/Dropbox, allowing for file encryption and decryption using the Nym Mixnet.
* [Github](https://github.com/saleel/nymdrive)
* [Demo](https://www.youtube.com/watch?v=5Rx73nw8NYI)
* [Presentation](https://docs.google.com/presentation/d/1MpvIK32Mx9VKLVfMTcvbeyrsKHHUsTvDQ-3n31dR0NE/edit#slide=id.p)
## <img src='../images/profile_picture/nym_dashboard_pp.svg' style="float: right; width: 75px; height: 75px;">Nym Dashboard
> Developed by No Trust Verify, this dashboard is a great tool to get information about the mixnet, gateways and mixnodes.
* [Deployed App](https://status.notrustverify.ch/d/CW3L7dVVk/nym-mixnet?orgId=1)
## <img src='../images/profile_picture/pastenym_ntv_pp.png' style="float: right; width: 75px; height: 75px;">Is Nym Up
> Explore whether we're up through IsNymUp, a tool that helps check the heath of the Nym network as well as some mixnet related statistics!
* [Deployed App](https://isnymup.com/)
## <img src='../images/profile_picture/darkfi_over_nym_pp.png' style="float: right; width: 75px; height: 75px;">DarkFi over Nym
> DarkFi leverages Nym's mixnet as a pluggable transport for IRCD, their p2p IRC variant. Users can anonymously connect to peers over the network, ensuring secure and private communication within the DarkFi ecosystem.
* [Github](https://github.com/darkrenaissance/darkfi)
* [Documentation](https://darkrenaissance.github.io/darkfi/clients/nym_outbound.html)
## <img src='../images/profile_picture/nymstr_email_pp.png' style="float: right; width: 75px; height: 75px;">Nymstr email
> Experience secure and private email communication with ease using Nymstr email, which enables seamless transmission of emails over a SOCKS5 proxy and our NYM mixnet!
* [Github](https://github.com/dial0ut/nymstr-email)
## <img src='../images/profile_picture/minibolt_pp.png' style="float: right; width: 75px; height: 75px;">Minibolt
> Anonymize your p2p inventory messages and mempool for your Bitcoin & Lightning full nodes on consumer PCs!
* [Github](https://github.com/minibolt-guide/minibolt)
* [Documentation](https://v2.minibolt.info/bonus-guides/system/nym-mixnet#proxying-bitcoin-core)
<br/> <br/>
# Community Guides
We aren't the only ones writing documentation: the Nym developer community is also a great source of guides and resources, some of which we've included here.
## <img src='../images/profile_picture/pastenym_ntv_pp.png' style="float: right; width: 75px; height: 75px;"> No Trust Verify
>No Trust Verify is a project that aims to build open-source, privacy-enhancing technologies that make it easier to use the Internet securely and anonymously. Their focus is on providing tools and services that make it simple for developers to create decentralized applications (dApps) that respect users' privacy.
* [Awesome Nym list](https://notrustverify.github.io/awesome-nym/) ([GitHub](https://github.com/notrustverify/awesome-nym))
* A lot of guides can be found on the [NTV Blog](https://blog.notrustverify.ch/)
## <img src='../images/profile_picture/pineappleproxy_pp.png' style="float: right; width: 75px; height: 75px;">The Way of the NYMJA
by Pineapple Proxy🍍
>Born out of a study group from Nym's Shipyard Academy, Pineapple Proxy has emerged as a cluster of motivated and skilled individuals who see the new internet taking shape. With vibecare at the heart of their approach, this zesty collective is on a mission to make privacy convenient for everyone via content, new tools, events, and novel experiences. They believe in collective intelligence, empathy, and collaboration as the means by which privacy will become a meaningful reality.
* [Website](https://pnproxy.org/welcome.html) ([GitHub](https://github.com/Pineapple-Proxy-DAO/web))
@@ -1,61 +0,0 @@
# Community Applications
We love seeing our developer community create applications using Nym. If you would like to share your application with the community, please submit a pull request to the `main` branch of the `nymtech/dev-portal` [repository](https://github.com/nymtech/dev-portal).
<!--Pastenym *start-->
## <img src='../images/profile_picture/pastenym_ntv_pp.png' style="float: right; width: 75px; height: 75px;">Pastenym
>A [pastebin](https://pastebin.com) inspired project, offer a solution for sharing text with Nym products to offer full anonymity, even on metadata level.
* [Github](https://github.com/notrustverify/pastenym)
* [Deployed Website App](https://pastenym.ch)
<!--Pastenym *end-->
<!--Pineapple *start-->
## <img src='../images/profile_picture/pineappleproxy_pp.png' style="float: right; width: 75px; height: 75px;">Nostr-Nym
> [Nostr-nym](https://github.com/notrustverify/nostr-nym) offer a solution to use [Nostr](https://nostr.how/) protocol by giving the possibility to run a relay on mixnet. By using a nostr client compatible with the mixnet, users can protect their privacy to be able to use Nostr has the want, without being observed.
* [Github](https://github.com/notrustverify/nostr-nym)
* Deployed Website App coming soon
<!--Pineapple *end-->
<!--Ethereum RPC Provider *start-->
## <img src='../images/profile_picture/ethereum_rpc_spook_pp.png' style="float: right; width: 75px; height: 75px;"> Spook
> Ethereum RPC request mixer uses the Nym network mixing service to anonymize RPC requests to the Ethereum network without revealing sensitive data and metadata.
* [Github](https://github.com/EdenBlockVC/spook)
<!--Ethereum RPC Provider *end-->
<!--Ethereum Transaction Broadcaster *start-->
## <img src='../images/profile_picture/ethereum_transaction_broadcaster_root_pp.png' style="float: right; width: 75px; height: 75px;"> Ethereum Transaction Broadcaster
> Ethereum Transaction Broadcaster that uses the Nym Mixnet to provide privacy and anonymity for transactions on the Ethereum network command-line interface.
* [Github](https://github.com/noot/nym-ethtx)
<!--Ethereum Transaction Broadcaster *end-->
<!--NymDrive *start-->
## <img src='../images/profile_picture/nymdrive_saleel_pp.png' style="float: right; width: 75px; height: 75px;">NymDrive
> An open-source, decentralized, E2E encrypted, privacy friendly alternative to Google Drive/Dropbox, allowing for file encryption and decryption using the Nym Mixnet.
* [Github](https://github.com/saleel/nymdrive)
* [Demo](https://www.youtube.com/watch?v=5Rx73nw8NYI)
* [Presentation](https://docs.google.com/presentation/d/1MpvIK32Mx9VKLVfMTcvbeyrsKHHUsTvDQ-3n31dR0NE/edit#slide=id.p)
<!--NymDrive *end-->
@@ -1,17 +0,0 @@
# Community Guides
We aren't the only ones writing documentation: the Nym developer community is also a great source of guides and resources, some of which we've included here.
## <img src='../images/profile_picture/pastenym_ntv_pp.png' style="float: right; width: 75px; height: 75px;"> No Trust Verify
>No Trust Verify is a project that aims to build open-source, privacy-enhancing technologies that make it easier to use the Internet securely and anonymously. Their focus is on providing tools and services that make it simple for developers to create decentralized applications (dApps) that respect users' privacy.
* [Awesome Nym list](https://notrustverify.github.io/awesome-nym/) ([GitHub](https://github.com/notrustverify/awesome-nym))
* A lot of guides can be found on the [NTV Blog](https://blog.notrustverify.ch/)
## <img src='../images/profile_picture/pineappleproxy_pp.png' style="float: right; width: 75px; height: 75px;">The Way of the NYMJA
by Pineapple Proxy🍍
>Born out of a study group from Nym's Shipyard Academy, Pineapple Proxy has emerged as a cluster of motivated and skilled individuals who see the new internet taking shape. With vibecare at the heart of their approach, this zesty collective is on a mission to make privacy convenient for everyone via content, new tools, events, and novel experiences. They believe in collective intelligence, empathy, and collaboration as the means by which privacy will become a meaningful reality.
* [Website](https://pnproxy.org/welcome.html) ([GitHub](https://github.com/Pineapple-Proxy-DAO/web))
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<g id="Layer_1_1_">
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="30%" x2="175.5" y2="99%">
<stop offset="0" style="stop-color:#F05A28"/>
<stop offset="1" style="stop-color:#FBCA0A"/>
</linearGradient>
<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

@@ -36,7 +36,7 @@ In order to ensure uptime and reliability, it is recommended that you run some p
* If you're running a purely P2P application, then just integrating clients and having some method of sharing addresses should be enough to route your traffic through the mixnet.
* If you're wanting to place the mixnet between your users' application instances and a server-based backend, you can use the [network requester](https://nymtech.net/docs/nodes/network-requester-setup.html) service provider binary to proxy these requests to your application backend, with the mixnet 'between' the user and your service, in order to prevent metadata leakage being broadcast to the internet.
* If you're wanting to route RPC requests through the mixnet to a blockchain, you will need to look into setting up some sort of service that does the transaction broadcasting for you. You can find examples of such projects on the [community applications](../community-resources/community-applications.md) page.
* If you're wanting to route RPC requests through the mixnet to a blockchain, you will need to look into setting up some sort of service that does the transaction broadcasting for you. You can find examples of such projects on the [community applications](../community-resources/community-applications-and-guides.md) page.
## Example application traffic flow
### Initialization
@@ -2,9 +2,10 @@
There are multiple options to quickly connect to Nym and see the network in action without the need for any code changes to your application. At most, these involve running Nym as a second process alongside an existing application in order to send traffic through the mixnet.
Demo application:
Demo applications:
* a browser-based 'hello world' [chat application](https://chat-demo.nymtech.net). Either open in two browser windows and send messages to yourself, or share with a friend and send messages to each other through the mixnet!
* a Coconut-scheme based [Credential Library](https://coco-demo.nymtech.net/). This is a WASM implementation of our Coconut libraries which generate raw Coconut credentials. Test it to create and re-randomize your own credentials!
<br>
Proxy traffic with the Nym Socks5 client:
* set up a plug-and-play connection with the [NymConnect](./nymconnect-gui.md) GUI for proxying Telegram, Electrum, Keybase or Blockstream Green traffic through the mixnet (~2 minutes).
* [Download and run](./socks-proxy.md) the Nym Socks5 client via the CLI, for other desktop applications with SOCKS5 connection options (~30 minutes).
@@ -3,7 +3,7 @@
Chat applications became an essential part of human communication. Matrix chat has end to end encryption on protocol level and Element app users can sort their communication into spaces and rooms. Now the Matrix communities can rely on network privacy as NymConnect supports Matrix chat protocol.
Currently there is no option in Matrix's Element client to set a socks5 proxy. In order to use Element via NymConnect users have to start it from the command-line. The setup is simple, for convenience a a keyboard shortcut setting can be easily done.
Currently there is no option in Matrix's Element client to set a Socks5 proxy. In order to use Element via NymConnect users have to start it from the command-line. The setup is simple, for convenience a keyboard shortcut setting or terminal alias can be easily done.
## Setup & Run
@@ -35,10 +35,46 @@ Alternatively you can add a keybinding via the CLI, using whatever config files
### Create an alias
If you prefer to simply shorten the length of the command (or all your keybindings are already taken) then you can simply create an alias for this long-winded command (this example aliases that command to the single word `element`, but you can replace it with whatever you like):
**Linux**
```sh
alias element="element-desktop --proxy-server=socks5://127.0.0.1:1080"
```
To make this alias persist, then add this to your `.bashrc` or `.zshrc` file (usually located in your `$HOME` directory) and `source` that file.
To make this alias persist, then add this to your `.bashrc` or `.zshrc` file (usually located in your `$HOME` directory) and `source` that file. This can be done by appending the alias command directly to the shell config file with one command.
Now you can run Element throught the mixnet with a single-word command.
For `bash` enter:
```sh
alias element="element-desktop --proxy-server=socks5://127.0.0.1:1080" >> ~/.bashrc
```
For `zsh` enter:
```sh
alias element="element-desktop --proxy-server=socks5://127.0.0.1:1080" >> ~/.zshrc
```
You can add the alias manually by opening your `$HOME` directory, enable hidden files (press `ctrl` + `h`) and open `.bashrc` or `.zshrc` file (based on your terminal setup) in a text editor, paste the string `alias element="element-desktop --proxy-server=socks5://127.0.0.1:1080"` to the the end, save and exit. Start a new terminal and run `element`.
**Mac**
```sh
alias element="open -a Element --args --proxy-server=socks5://127.0.0.1:1080"
```
To make this alias persist, then add this to your `.zshrc` (or `.bashrc`/`.profile`) file (usually located in your `$HOME` directory) and `source` that file. This can be done by appending the alias command directly to the shell config file with one command.
For `zsh` enter:
```sh
alias element="open -a Element --args --proxy-server=socks5://127.0.0.1:1080" >> ~/.zshrc
```
For `.bashrc` or `.profile` just change the end of the command.
You can add the alias manually by opening your `$HOME` directory, enable hidden files (in Finder press `Shift` + `Command` + `.`) and open `.zshrc` file (or `.bashrc`/`.profile`) in a text editor, paste the string `alias element="open -a Element --args --proxy-server=socks5://127.0.0.1:1080"` to the the end, save and exit. Start a new terminal and run `element`.
**Now you can run Element through the Nym Mixnet with a single-word command.**
@@ -12,7 +12,7 @@ Simply fill in the fields in your browser and click `Send`. In your browser you
<img src="../../images/tutorial_image_5.png"/>
This small project can be used as a template to start conceptualizing and developing more complex PEApps. Stay tuned for more soon, and if you're searching for inspiration check out the [community apps](../../community-resources/community-applications.md) list!
This small project can be used as a template to start conceptualizing and developing more complex PEApps. Stay tuned for more soon, and if you're searching for inspiration check out the [community apps](../../community-resources/community-applications-and-guides.md) list!
@@ -62,7 +62,8 @@ h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before {
display: inline-block;
/* display "none" in order to avoid default artifacts on the community-applications-and-guides page */
display: none;
content: "»";
margin-left: -30px;
width: 30px;
+3 -3
View File
@@ -1,6 +1,6 @@
[book]
title = "Nym Docs"
authors = ["Max Hampshire"]
authors = ["Max Hampshire, Serinko, Alexia Lorenza Martinel"]
description = "Nym technical documentation"
language = "en"
multilingual = false # for the moment - ideally work on chinese, brazillian, spanish next
@@ -49,8 +49,8 @@ assets_version = "2.0.0" # do not edit: managed by `mdbook-admonish install`
[preprocessor.variables.variables]
minimum_rust_version = "1.66"
# vars for links: TODO think on how to streamline updating
platform_release_version = "v1.1.22"
wallet_release_version = "v1.2.5"
platform_release_version = "v1.1.25"
wallet_release_version = "v1.2.7"
[preprocessor.last-changed]
command = "mdbook-last-changed"
@@ -153,8 +153,6 @@ Follow these steps to upgrade your binary and update its config file:
* re-run `init` with the same values as you used initially. **This will just update the config file, it will not overwrite existing keys**.
* restart your gateway process with the new binary.
> Do **not** use the `upgrade` command: there is a known error with the command that will be fixed in a subsequent release.
#### Step 2: updating your node information in the smart contract
Follow these steps to update the information about your node which is publically avaliable from the [Nym API](https://validator.nymtech.net/api/swagger/index.html) and information displayed on the [mixnet explorer](https://explorer.nymtech.net).
@@ -191,8 +191,6 @@ Follow these steps to upgrade your mix node binary and update its config file:
* re-run `init` with the same values as you used initially. **This will just update the config file, it will not overwrite existing keys**.
* restart your mix node process with the new binary.
> Do **not** use the `upgrade` command: there is a known error with the command that will be fixed in a subsequent release.
#### Step 2: updating your node information in the smart contract
Follow these steps to update the information about your mix node which is publically avaliable from the [Nym API](https://validator.nymtech.net/api/swagger/index.html) and information displayed on the [mixnet explorer](https://explorer.nymtech.net).
@@ -8,7 +8,6 @@
<!-- cmdrun ../../../../target/release/nym-network-requester --version | grep "Build Version" | cut -b 21-26 -->
```
## Network Requester Whitelist
If you have access to a server, you can run the network requester, which allows Nym users to send outbound requests from their local machine through the mixnet to a server, which then makes the request on their behalf, shielding them (and their metadata) from clearnet, untrusted and unknown infrastructure, such as email or message client servers.
@@ -65,6 +64,34 @@ p2pify.com
2001:67c:4e8::/48
2001:b28:f23c::/48
2a0a:f280::/32
# nym matrix server
nymtech.chat
# generic matrix server backends
vector.im
matrix.org
# monero desktop - mainnet
212.83.175.67
212.83.172.165
176.9.0.187
88.198.163.90
95.217.25.101
136.244.105.131
104.238.221.81
66.85.74.134
88.99.173.38
51.79.173.165
# monero desktop - stagenet
162.210.173.150
176.9.0.187
88.99.173.38
51.79.173.165
# alephium
alephium.org
```
## Network Requester Directory
@@ -193,25 +220,32 @@ sudo ufw enable
sudo ufw status
```
Finally open your requester's p2p port, as well as ports for ssh and incoming traffic connections:
Finally open your requester's ssh port to incoming administration connections:
```
sudo ufw allow 22,9000/tcp
sudo ufw allow 22/tcp
# check the status of the firewall
sudo ufw status
```
For more information about your requester's port configuration, check the [requester port reference table](./network-requester-setup.md#requester-port-reference) below.
## Using your network requester
The next thing to do is use your requester, share its address with friends (or whoever you want to help privacy-enhance their app traffic). Is this safe to do? If it was an open proxy, this would be unsafe, because any Nym user could make network requests to any system on the internet.
To make things a bit less stressful for administrators, the Network Requester drops all incoming requests by default. In order for it to make requests, you need to add specific domains to the `allowed.list` file at `$HOME/.nym/service-providers/network-requester/allowed.list`.
### Global vs local allow lists
Your Network Requester will check for a domain against 2 lists before allowing traffic through for a particular domain or IP.
* The first list is the default list on the [nymtech.net server](https://nymtech.net/.wellknown/network-requester/standard-allowed-list.txt). Your Requester will not check against this list every time, but instead will keep a record of accepted domains in memory.
* The second is the local `allowed.list` file.
### Supporting custom domains with your network requester
It is easy to add new domains and services to your network requester - simply find out which endpoints (both URLs and raw IP addresses are supported) you need to whitelist, and then add these endpoints to your `allowed.list`.
> In order to keep things more organised, you can now use comments in the `allow.list` like the example at the top of this page.
How to go about this? Have a look in your nym-network-requester config directory:
```
@@ -249,7 +283,3 @@ This command should return the following:
### Requester port reference
All network-requester-specific port configuration can be found in `$HOME/.nym/service-providers/network-requester/<YOUR_ID>/config/config.toml`. If you do edit any port configs, remember to restart your client and requester processes.
| Default port | Use |
|--------------|---------------------------|
| 9000 | Listen for Client traffic |
@@ -593,18 +593,19 @@ nyxd tx slashing unjail
### Upgrading your validator
Upgrading from `v0.26.0` -> `v0.31.1` process is fairly simple. Grab the v0.31.1 release tarball from the [`nyxd` releases page](https://github.com/nymtech/nyxd/releases), and untar it. Inside are two files:
Upgrading from `v0.31.1` -> `v0.32.0` process is fairly simple. Grab the `v0.32.0` release tarball from the [`nyxd` releases page](https://github.com/nymtech/nyxd/releases), and untar it. Inside are two files:
- the new validator (`nyxd`) v0.31.1
- the new validator (`nyxd`) v0.32.0
- the new wasmvm (it depends on your platform, but most common filename is `libwasmvm.x86_64.so`)
Before the upgrade height, copy `libwasmvm.x86_64.so` to the default LD_LIBRARY_PATH on your system (on Ubuntu 20.04 this is `/lib/x86_64-linux-gnu/`):
Wait for the upgrade height to be reached and the chain to halt awaiting upgrade, then:
Then just swap in your new `nyxd` binary and restart once the halt height is reached.
* copy `libwasmvm.x86_64.so` to the default LD_LIBRARY_PATH on your system (on Ubuntu 20.04 this is `/lib/x86_64-linux-gnu/`) replacing your existing file with the same name.
* swap in your new `nyxd` binary and restart.
You can also use something like [Cosmovisor](https://github.com/cosmos/cosmos-sdk/tree/main/tools/cosmovisor) - grab the relevant information from the current upgrade proposal [here](https://nym.explorers.guru/proposal/8).
You can also use something like [Cosmovisor](https://github.com/cosmos/cosmos-sdk/tree/main/tools/cosmovisor) - grab the relevant information from the current upgrade proposal [here](https://nym.explorers.guru/proposal/9).
Note: Cosmovisor will swap the `nyxd` binary, but you'll need to already have the `libwasmvm.x86_64.so` in place. Luckily, the name of the wasmvm in v0.26.1 was `libwasmvm.so`, and the new name is `libwasmvm.x86_64.so`, so you can have it already sitting there without disrupting service.
Note: Cosmovisor will swap the `nyxd` binary, but you'll need to already have the `libwasmvm.x86_64.so` in place.
#### Common reasons for your validator being jailed
+8 -3
View File
@@ -56,14 +56,19 @@ If you're integrating mixnet functionality into an existing app and want to inte
### Anonymous replies with SURBs
Both functions used to send messages through the mixnet (`send_str` and `send_bytes`) send a pre-determined number of SURBs along with their messages by default.
The number of SURBs is set [here](https://github.com/nymtech/nym/blob/release/{{platform_release_version}}/sdk/rust/nym-sdk/src/mixnet/client.rs#L35):
The number of SURBs is set [here](https://github.com/nymtech/nym/blob/release/{{platform_release_version}}/sdk/rust/nym-sdk/src/mixnet/client.rs#L34):
```rust,noplayground
{{#include ../../../../sdk/rust/nym-sdk/src/mixnet/client.rs:30}}
{{#include ../../../../sdk/rust/nym-sdk/src/mixnet/client.rs:34}}
```
You can read more about how SURBs function under the hood [here](../architecture/traffic-flow.md#private-replies-using-surbs).
In order to reply to an incoming message using SURBs, you can construct a `recipient` from the `sender_tag` sent along with the message you wish to reply to:
```rust,noplayground
{{#include ../../../../sdk/rust/nym-sdk/examples/surb-reply.rs}}
```
### Importing and using a custom network topology
If you want to send traffic through a sub-set of nodes (for instance, ones you control, or a small test setup) when developing, debugging, or peforming research, you will need to import these nodes as a custom network topology, instead of grabbing it from the [`Mainnet Nym-API`](https://validator.nymtech.net/api/swagger/index.html) (`examples/custom_topology_provider.rs`).
@@ -102,4 +107,4 @@ The following code shows how you can use the SDK to create and use a [credential
{{#include ../../../../sdk/rust/nym-sdk/examples/bandwidth.rs}}
```
You can read more about Coconut credentials (also referred to as `zk-Nym`) [here](../cococnut.md).
You can read more about Coconut credentials (also referred to as `zk-Nym`) [here](../coconut.md).
-1
View File
@@ -16,7 +16,6 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1lp5zex6685kd22agzskhqsylpnssxnweyuvsz4edr4p
GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m
MULTISIG_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m
COCONUT_DKG_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p
EPHEMERA_CONTRACT_ADDRESS=n1vwtgazgpancsfel04y7syc95ausmat47cjtldqzkdmx6phyrwx2qvkv32p
REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
NYXD="http://127.0.0.1:26657"
-1
View File
@@ -16,7 +16,6 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m
MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
NYXD="https://rpc.nymtech.net"
-1
View File
@@ -16,7 +16,6 @@ COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19d2nwj7fdhxqmyvgy8lf3ad49a6vmww4shryhrkj2mq
GROUP_CONTRACT_ADDRESS=n1fqquzw4mk0pkamgr2ywt2v7h2j9nuyjjn4gvpy8zlpp6xn0uyuzqfm28l5
MULTISIG_CONTRACT_ADDRESS=n1gaq3666chd5348apj8cka8t2mckv7azp9espyr7wgpxyuzur5d0sazpysy
COCONUT_DKG_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs
EPHEMERA_CONTRACT_ADDRESS=n18yadscxw8v35dds7ksv3j0svmjh3h6e7tmxpadk96mvgz27zygkshuf4vs
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1qsn2655eflc0nx2uwqtwyv5kad5dwm4c0gn72yr4q4de5r3jaz2slvqjgt
NAME_SERVICE_CONTRACT_ADDRESS=n1cm2u5vfjd3zalfw0p65xyh4tcrw3hjlm0960gzhewga449h4mgas77mjkl
+11 -14
View File
@@ -9,19 +9,16 @@ MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n10qt8wg0n7z740ssvf3urmvgtjhxpyp74hxqvqt7z226gykuus7eq5u9pvq
VESTING_CONTRACT_ADDRESS=n1vguuxez2h5ekltfj9gjd62fs5k4rl2zy5hfrncasykzw08rezpfstk9xtk
MIXNET_CONTRACT_ADDRESS=n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8
VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1x22q8lfhz7qcvtzs0dakhgx2th64l79kepjujhhxk5x804taeqlqcywn96
GROUP_CONTRACT_ADDRESS=n1g4xlpqy29m50j5y69reguae328tc9y83l4299pf2wmjn0xczq5js3704ql
MULTISIG_CONTRACT_ADDRESS=n1p54qvfde6mpnqvz3dnpa78x2qyyr5k4sgw9qr97mxjgklc5gze9sv6t964
COCONUT_DKG_CONTRACT_ADDRESS=n1xqkp8x4gqwjnhemtemc5dqhwll6w6rrgpywvhka7sh8vz8swul9sp3lv3w
EPHEMERA_CONTRACT_ADDRESS=n1tdxl4uv6fg3ke52n0umhv96648ttmwtg0u6uujmv5cuvjktg6qvq4mlzeg
REWARDING_VALIDATOR_ADDRESS=n1547eraje66h9mf99fm50mea04n9mrzsc8c23r5
NAME=n1uz24lsnwxvhep8m3gjec7ev86twlhlqrf5rphlgn3rda3zu048ssjqr5w9
SERVICE_PROVICER=n1nhdr07kmjns2x8dnp53tdk4qxreze8zdxj6xucyvkdj9tta73rjqa96wps
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
NYXD="https://qa-validator.qa.nymte.ch/"
NYM_API="https://qa-nym-api.qa.nymte.ch/api"
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw
GROUP_CONTRACT_ADDRESS=n1rw8fw2mpcpzzq3jpa4e52ufawnmj5a4u68p35umvgskewuw0nlzsaa5w4m
MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs
COCONUT_DKG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs
REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
NYXD="https://qa-validator.nymtech.net"
NYM_API="https://qa-validator-api.nymtech.net/api"
DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600"
DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600"
-1
View File
@@ -17,7 +17,6 @@ BANDWIDTH_CLAIM_CONTRACT_ADDRESS="nymt1rhmk9udessnv3r8f3eh2s03f45svnjaczpmcqz"
MULTISIG_CONTRACT_ADDRESS="nymt142dkm8xe9f0ytyarp7ww4kvclva65705jphxsk9exn3nqdsm8jkqnp06ac"
COCONUT_BANDWIDTH_CONTRACT_ADDRESS="nymt1ty0frysegskh6ndm3v96z5xdq66qzcu0aw7xcxlgp54jg0mjwlgqplc6v0"
COCONUT_DKG_CONTRACT_ADDRESS="nymt1gwk6muhmzeuxje7df7rjvqwl2vex0kj4t2hwuzmyx5k62kfusu5qk4k5z4"
EPHEMERA_CONTRACT_ADDRESS="nymt1gwk6muhmzeuxje7df7rjvqwl2vex0kj4t2hwuzmyx5k62kfusu5qk4k5z4"
GROUP_CONTRACT_ADDRESS="nymt14ry36mwauycz08v8ndcujghxz4hmua5epxcn0mamlr3suqe0l2qsqx5ya2"
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
-65
View File
@@ -1,65 +0,0 @@
[package]
name = "ephemera"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "ephemera"
path = "bin/main.rs"
[dependencies]
## internal
nym-task = { path = "../common/task" }
actix-web = "4"
anyhow = { version = "1.0.66", features = ["backtrace"] }
array-bytes = "6.0.0"
async-trait = "0.1.59"
asynchronous-codec = "0.6.1"
blake2 = "0.10.6"
bs58 = "0.4.0"
bytes = "1.3.0"
cfg-if = "1.0.0"
chrono = { version = "0.4.24", default-features = false, features = ["clock"] }
clap = { version = "4.0.32", features = ["derive"] }
config = { version = "0.13", default-features = false, features = ["toml"] }
digest = "0.10.6"
dirs = "5.0.0"
futures = "0.3.18"
futures-util = "0.3.25"
lazy_static = "1.4.0"
libp2p = { version = "0.51.3", default-features = false, features = ["dns", "gossipsub", "kad", "macros", "noise", "request-response", "serde", "tcp", "tokio", "yamux"] }
libp2p-identity = "0.1.0"
log = "0.4.14"
lru = "0.10.0"
nym-config = { path = "../common/config" }
nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" }
pretty_env_logger = "0.4"
refinery = { version = "0.8.7", features = ["rusqlite"], optional = true }
reqwest = { version = "0.11.6", features = ["json"] }
rocksdb = { version = "0.20.1", optional = true }
rusqlite = { version = "0.27.0", features = ["bundled"], optional = true }
serde = { version = "1.0", features = ["derive"] }
serde_derive = "1.0.149"
serde_json = "1.0.91"
thiserror = "1.0.37"
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
tokio-tungstenite = "0.18.0"
tokio-util = { version = "0.7.4", features = ["full"] }
toml = "0.7.0"
unsigned-varint = "0.7.1"
utoipa = { version = "3.0.1", features = ["actix_extras"] }
utoipa-swagger-ui = { version = "3.0.2", features = ["actix-web"] }
uuid = { version = "1.2.2", features = ["v4"] }
# Temporary fix to https://github.com/bluejekyll/trust-dns/issues/1946
enum-as-inner = "=0.5.1"
[dev-dependencies]
assert_matches = "1.5.0"
rand = "0.8.5"
[features]
default = ["sqlite_storage"]
rocksdb_storage = ["rocksdb"]
sqlite_storage = ["rusqlite", "refinery"]
-142
View File
@@ -1,142 +0,0 @@
# Ephemera - reliable broadcast protocol implementation
Ephemera does reliable broadcast for blocks.
## Short Overview
All Ephemera nodes accept messages submitted by clients. Node then gossips these to other nodes in the cluster. After certain interval,
a node collects messages and produces a block. Then it does reliable broadcast for the block with other nodes in the cluster.
Ephemera doesn't have the concept of (decentralised) leader at the moment. It's up to an _Application_ to decide which block to use.
For example in case of Nym-Api, it is the first block submitted to a "Smart Contract".
At the same time, the purpose of blocks is to reach consensus about which messages are included. It's just that Ephemera doesn't make the final decision,
instead it leaves that to an _Application_.
## Main concepts
- **Node** - a single instance of Ephemera.
- **Cluster** - a set of nodes participating in reliable broadcast.
- **EphemeraMessage** - a message submitted by a client.
- **Block** - a set of messages collected by a node.
- **Application(ABCI)** - a trait which Ephemera users implement to accept messages and blocks.
- check_tx
- check_block
- accept_block
## How to run
[README](../scripts/README.md)
## HTTP API
See [Rust](src/api/http/mod.rs)
### Endpoints
**NODE**
- `/ephemera/node/health`
- `/ephemera/node/config`
**BLOCKS**
- `/ephemera/broadcast/block/{hash}`
- `/ephemera/broadcast/block/height/{height}`
- `/ephemera/broadcast/blocks/last`
- `/ephemera/broadcast/block/certificates/{hash}`
- `/ephemera/broadcast/block/broadcast_info/{hash}`
**GROUP**
- `/ephemera/broadcast/group/info`
**MESSAGES**
- `/ephemera/broadcast/submit_message`
**DHT**
- `/ephemera/dht/query/{key}`
- `/ephemera/dht/store`
## Rust API
Almost identical to HTTP API.
See [Rust](src/api/mod.rs)
## Application(Ephemera ABCI)
Cosmos style ABCI application hook
- `check_tx`
- `check_block`
- `deliver_block`
See [Rust](src/api/application.rs)
## Examples
### Ephemera HTTP and WS external interfaces example/tests
See [README.md](../examples/http-ws-sync/README.md)
### Nym Api simulation
See [README.md](../examples/nym-api/README.md)
### http API example/tests
See [README.md](../examples/cluster-http-api/README.md)
### Membership over HTTP API example/tests
See [README.md](../examples/members_provider_http/README.md)
## About reliable broadcast and consensus
In blockchain technology blocks have two main purposes:
1. To maintain chain of blocks, so that the validity of each block can be cryptographically verified by the previous blocks
2. As a unit of consensus, each block contains a set of transactions/messages/actions that are agreed upon by
the network. This set of transactions is chosen from the global set of all possible transactions that are pending.
We call the set of transactions in a block consensus because the set of nodes trying to achieve global shared state
agreed on this particular set of transactions.
Ephemera is not a blockchain. But it uses blocks to agree on the set of transactions in a block.
But at the same time it doesn't behave like a blockchain consensus algorithm.
We may say that it allows each application that uses Ephemera to "propose" something what can be
afterwards to be used to achieve consensus.
### In Summary
1. Ephemera provides functionality to reach agreement on a single value between a set of nodes.
2. Ephemera also provides the concept of a block, which application can take advantage of to reach consensus externally.
### Reliable broadcast, consensus and blocks
In distributed systems(including byzantine), we try to solve the problem of reaching to a commons state between a set of nodes.
One way to define this problem is using the following properties:
1.
1) Agreement: All nodes agree on the same value.(TODO clarify)
2) Consensus: All nodes agree on the same value.(TODO clarify)
2. Validity: All nodes agree on a value that is valid.
3. Termination: All nodes eventually agree on a value.
Reliable broadcast ensures the properties of 1.1 and 1.2. It's left to a particular consensus algorithm to ensure the termination property.
One important feature of consensus in blockchain is that it guarantees total ordering of transactions.
Reliable broadcast with blocks helps to ensure this total ordering.
### Ephemera specific properties
Because Ephemera doesn't use the idea of leader, we can say that it solves consensus partially.
It allows each instance to create a block. And then it's up to an application to decide which block to use.
Also, as it doesn't implement a full consensus algorithm, it doesn't ensure the termination.
There's no algorithm in place what tries to reach a consensus about a single block globally and sequentially
in time.
When a block contains a single message, then it's semantically equivalent to a reliable broadcast.
But when a block contains multiple messages, then it can be part of a consensus process. Except that in Ephemera each node
can create a block. To achieve consensus in a more traditional sense, it needs an application help if more strict
consensus is required.
For example, Nym-Api allows each node to create a block but uses external coordinator(a smart contract)
to decide which block to use.
-12
View File
@@ -1,12 +0,0 @@
use clap::Parser;
use ephemera::cli::Cli;
use ephemera::logging;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
logging::init();
Cli::parse().execute().await?;
Ok(())
}
-24
View File
@@ -1,24 +0,0 @@
CREATE TABLE IF NOT EXISTS blocks (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
block_hash TEXT NOT NULL UNIQUE,
height TEXT NOT NULL UNIQUE,
block BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS block_certificates (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
block_hash TEXT NOT NULL UNIQUE,
certificates BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS block_broadcast_group (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
block_hash TEXT NOT NULL UNIQUE,
members BLOB NOT NULL
);
CREATE TABLE IF NOT EXISTS block_merkle_tree (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
block_hash TEXT NOT NULL UNIQUE,
merkle_tree BLOB NOT NULL
);
-104
View File
@@ -1,104 +0,0 @@
use log::trace;
use thiserror::Error;
use crate::api::types::{ApiBlock, ApiEphemeraMessage};
#[derive(Debug, Clone, PartialEq)]
pub enum RemoveMessages {
/// Remove all messages from the mempool
All,
/// Remove only inclued messages from the mempool
Selected(Vec<ApiEphemeraMessage>),
}
#[derive(Debug, Clone, PartialEq)]
pub enum CheckBlockResult {
/// Accept the block
Accept,
/// Reject the block with a reason.
Reject,
/// Reject the block and remove messages from the mempool
RejectAndRemoveMessages(RemoveMessages),
}
#[derive(Debug, Clone, PartialEq)]
pub struct CheckBlockResponse {
pub accept: bool,
pub reason: Option<String>,
}
#[derive(Error, Debug)]
pub enum Error {
//Just a placeholder for now
#[error("ApplicationError: {0}")]
Application(#[from] anyhow::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// Cosmos style ABCI application hook
///
/// These functions should be relatively fast, as they are called synchronously by Ephemera main loop.
pub trait Application {
/// It's called when receiving a new message from network before adding it to the mempool.
/// It's up to the application to decide whether the message is valid or not.
/// Basic check could be for example signature verification.
///
/// # Arguments
/// * `message` - message to be checked
///
/// # Returns
/// * `true` - if the message is valid
/// * `false` - if the message is invalid
///
/// # Errors
/// * `Error::General` - if there was an error during validation
fn check_tx(&self, message: ApiEphemeraMessage) -> Result<bool>;
/// Ephemera produces new blocks with configured interval.
/// Application can decide whether to accept the block or not.
/// For example, if the block doesn't contain any transactions, it can be rejected.
///
/// # Arguments
/// * `block` - block to be checked
///
/// # Returns
/// * `CheckBlockResult::Accept` - if the block is valid
/// * `CheckBlockResult::Reject` - if the block is invalid
/// * `CheckBlockResult::RejectAndRemoveMessages` - if the block is invalid and some messages should be removed from the mempool
///
/// # Errors
/// * `Error::General` - if there was an error during validation
fn check_block(&self, block: &ApiBlock) -> Result<CheckBlockResult>;
/// Deliver Block is called after block is confirmed by Ephemera and persisted to the storage.
///
/// # Arguments
/// * `block` - block to be delivered
///
/// # Errors
/// * `Error::General` - if there was an error during validation
fn deliver_block(&self, block: ApiBlock) -> Result<()>;
}
/// Dummy application which doesn't do any validation.
/// Might be useful for testing.
#[derive(Default)]
pub struct Dummy;
impl Application for Dummy {
fn check_tx(&self, tx: ApiEphemeraMessage) -> Result<bool> {
trace!("check_tx: {tx:?}");
Ok(true)
}
fn check_block(&self, block: &ApiBlock) -> Result<CheckBlockResult> {
trace!("accept_block: {block:?}");
Ok(CheckBlockResult::Accept)
}
fn deliver_block(&self, block: ApiBlock) -> Result<()> {
trace!("deliver_block: {block:?}");
Ok(())
}
}
-497
View File
@@ -1,497 +0,0 @@
use std::time::Duration;
use thiserror::Error;
use crate::api::types::{ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiHealth};
use crate::ephemera_api::{
ApiBlock, ApiCertificate, ApiDhtQueryRequest, ApiDhtQueryResponse, ApiDhtStoreRequest,
ApiEphemeraConfig, ApiEphemeraMessage, ApiVerifyMessageInBlock,
};
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Internal(#[from] reqwest::Error),
#[error("Unexpected response: {status} {body}")]
UnexpectedResponse {
status: reqwest::StatusCode,
body: String,
},
}
pub type Result<T> = std::result::Result<T, Error>;
/// Client to interact with the node over HTTP api.
pub struct Client {
pub(crate) client: reqwest::Client,
pub(crate) url: String,
}
impl Client {
/// Create a http new client.
///
/// # Arguments
/// * `url` - The url of the node api endpoint.
#[must_use]
pub fn new(url: String) -> Self {
let client = reqwest::Client::new();
Self { client, url }
}
/// Create a new client.
///
/// # Arguments
/// * `url` - The url of the node api endpoint.
/// * `timeout_sec` - Request timeout in seconds.
///
/// # Panics
/// If the client cannot be created.
#[must_use]
pub fn new_with_timeout(url: String, timeout_sec: u64) -> Self {
let client = reqwest::ClientBuilder::new()
.timeout(Duration::from_secs(timeout_sec))
.build()
.unwrap();
Self { client, url }
}
/// Get the health of the node.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{Client, ApiHealth};
///
/// #[tokio::main]
///async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000".to_string());
/// let health = client.health().await.unwrap();
/// Ok(())
/// }
/// ```
///
/// # Returns
/// * [`ApiHealth`] - The health of the node.
///
/// # Errors
/// If the request fails.
pub async fn health(&self) -> Result<ApiHealth> {
self.query("ephemera/node/health").await
}
/// Get the block by hash.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{ApiBlock, Client};
///
/// #[tokio::main]
///async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000".to_string());
/// let block = client.get_block_by_hash("9D2LaY17rbnxfgKUbvcsJ5cB2BRHEd8fPJwsBnDHNGBX").await?;
/// Ok(())
/// }
/// ```
///
/// # Returns
/// * Option<[`ApiBlock`]> - The block.
///
/// # Errors
/// If the request fails.
pub async fn get_block_by_hash(&self, hash: &str) -> Result<Option<ApiBlock>> {
let url = format!("ephemera/broadcast/block/{hash}",);
self.query_optional(&url).await
}
/// Get the block certificates by hash.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{ApiCertificate, Client};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000".to_string());
/// let certificates = client.get_block_certificates("9D2LaY17rbnxfgKUbvcsJ5cB2BRHEd8fPJwsBnDHNGBX").await?;
/// Ok(())
/// }
/// ```
///
/// # Arguments
/// * `hash` - The hash of the block.
///
/// # Returns
/// * Option<Vec<[`ApiCertificate`]>> - The block certificates.
///
/// # Errors
/// If the request fails.
pub async fn get_block_certificates(&self, hash: &str) -> Result<Option<Vec<ApiCertificate>>> {
let url = format!("ephemera/broadcast/block/certificates/{hash}",);
self.query_optional(&url).await
}
/// Get the block by height.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{ApiBlock, Client};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let block = client.get_block_by_height(1).await?;
/// Ok(())
/// }
/// ```
///
/// # Arguments
/// * `height` - The height of the block.
///
/// # Returns
/// * Option<[`ApiBlock`]> - The block.
///
/// # Errors
/// If the request fails.
pub async fn get_block_by_height(&self, height: u64) -> Result<Option<ApiBlock>> {
let url = format!("ephemera/broadcast/block/height/{height}",);
self.query_optional(&url).await
}
/// Get the last block.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{ApiBlock, Client};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let block = client.get_last_block().await?;
/// Ok(())
/// }
/// ```
///
/// # Returns
/// * [`ApiBlock`] - The last block.
///
/// # Errors
/// If the request fails.
pub async fn get_last_block(&self) -> Result<ApiBlock> {
self.query("ephemera/broadcast/blocks/last").await
}
/// Get the node configuration.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{ApiEphemeraConfig, Client};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let config = client.get_ephemera_config().await?;
/// Ok(())
/// }
/// ```
///
/// # Returns
/// * [`ApiEphemeraConfig`] - The node configuration.
///
/// # Errors
/// If the request fails.
pub async fn get_ephemera_config(&self) -> Result<ApiEphemeraConfig> {
self.query("ephemera/node/config").await
}
/// Submit a message to the node.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::{ApiEphemeraMessage, Client};
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let message = unimplemented!("See how to create a ApiEphemeraMessage");
/// client.submit_message(message).await?;
/// Ok(())
/// }
///
/// ```
///
/// # Arguments
/// * `message` - The message to submit.
///
/// # Errors
/// If the request fails.
pub async fn submit_message(&self, message: ApiEphemeraMessage) -> Result<()> {
let url = format!("{}/{}", self.url, "ephemera/broadcast/submit_message");
let response = self.client.post(&url).json(&message).send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(Error::UnexpectedResponse {
status: response.status(),
body: response.text().await?,
})
}
}
///Store Key Value pair in the DHT.
///
/// # Example
///```no_run
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use ephemera::ephemera_api::Client;
/// let client = Client::new("http://localhost:7000/".to_string());
/// let request = unimplemented!("See how to create a ApiDhtStoreRequest");
/// client.store_in_dht(request).await?;
/// Ok(())
/// }
/// ```
///
/// # Arguments
/// * `request` - Key Value pair to store.
///
/// # Errors
/// If the request fails.
pub async fn store_in_dht(&self, request: ApiDhtStoreRequest) -> Result<()> {
let url = format!("{}/{}", self.url, "ephemera/dht/store");
let response = self.client.post(&url).json(&request).send().await?;
if response.status().is_success() {
Ok(())
} else {
Err(Error::UnexpectedResponse {
status: response.status(),
body: response.text().await?,
})
}
}
///Store Key Value pair in the DHT.
///
/// # Example
///```no_run
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use ephemera::ephemera_api::Client;
/// let client = Client::new("http://localhost:7000/".to_string());
/// let key = &[1, 2, 3];
/// let value = &[4, 5, 6];
/// client.store_in_dht_key_value(key, value).await?;
/// Ok(())
/// }
/// ```
/// # Arguments
/// * `key` - Key to use to store the value.
/// * `value` - Value to store.
///
/// # Errors
/// If the request fails.
pub async fn store_in_dht_key_value(&self, key: &[u8], value: &[u8]) -> Result<()> {
let request = ApiDhtStoreRequest::new(key, value);
self.store_in_dht(request).await
}
/// Query the DHT for a given key.
///
/// # Example
///```no_run
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use ephemera::ephemera_api::{ApiDhtQueryRequest, Client};
/// let client = Client::new("http://localhost:7000/".to_string());
/// let request = ApiDhtQueryRequest::new(&[1, 2, 3]);
/// let response = client.query_dht(request).await?;
/// Ok(())
/// }
/// ```
///
/// # Arguments
/// * `request` - Key to query.
///
/// # Returns
/// * Option<[`ApiDhtQueryResponse`]> - The value stored in the DHT for the given key.
///
/// # Errors
/// If the request fails.
pub async fn query_dht(
&self,
request: ApiDhtQueryRequest,
) -> Result<Option<ApiDhtQueryResponse>> {
let url = format!("ephemera/dht/query/{}", request.key_encoded());
self.query_optional(&url).await
}
/// Query the DHT for a given key.
///
/// # Example
/// ```no_run
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use ephemera::ephemera_api::Client;
/// let client = Client::new("http://localhost:7000/".to_string());
/// let key = &[1, 2, 3];
/// let response = client.query_dht_key(key).await?;
/// Ok(())
/// }
/// ```
///
/// # Arguments
/// * `key` - Key to query.
///
/// # Returns
/// * Option<[`ApiDhtQueryResponse`]> - The value stored in the DHT for the given key.
///
/// # Errors
/// If the request fails.
pub async fn query_dht_key(&self, key: &[u8]) -> Result<Option<ApiDhtQueryResponse>> {
let request = ApiDhtQueryRequest::new(key);
self.query_dht(request).await
}
/// Get broadcast group info.
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::Client;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let info = client.broadcast_info().await?;
/// Ok(())
/// }
/// ```
///
/// # Returns
/// * [`ApiBroadcastInfo`] - The broadcast group info.
///
/// # Errors
/// If the request fails.
pub async fn broadcast_info(&self) -> Result<ApiBroadcastInfo> {
self.query("ephemera/broadcast/group/info").await
}
/// Get block broadcast info
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::Client;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let info = client.get_block_broadcast_info("hash").await?;
/// Ok(())
/// }
///```
///
/// # Arguments
/// * `hash` - Hash of the block to query.
///
/// # Returns
/// * Some([`ApiBlockBroadcastInfo`]) - The block broadcast info.
/// * None - If the block is not found.
///
/// # Errors
/// If the request fails.
pub async fn get_block_broadcast_info(
&self,
hash: &str,
) -> Result<Option<ApiBlockBroadcastInfo>> {
let url = format!("ephemera/broadcast/block/broadcast_info/{hash}",);
self.query_optional(&url).await
}
/// Verifies if given message is in block identified by block hash
///
/// # Example
/// ```no_run
/// use ephemera::ephemera_api::Client;
///
/// #[tokio::main]
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = Client::new("http://localhost:7000/".to_string());
/// let is_in_block = client.verify_message_in_block("block_hash", "message_hash", 0).await?;
/// Ok(())
/// }
/// ```
///
/// # Arguments
/// * `block_hash` - Hash of the block to query.
/// * `message_hash` - Hash of the message to query.
/// * `message_index` - Index of the message in the block.
///
/// # Returns
/// * bool - True if the message is in the block, false otherwise.
///
/// # Errors
/// If the request fails.
pub async fn verify_message_in_block(
&self,
block_hash: &str,
message_hash: &str,
message_index: usize,
) -> Result<bool> {
let request = ApiVerifyMessageInBlock::new(
block_hash.to_string(),
message_hash.to_string(),
message_index,
);
let url = format!("{}/{}", self.url, "ephemera/messages/verify");
let response = self.client.post(&url).json(&request).send().await?;
if response.status().is_success() {
let body = response.json::<bool>().await?;
Ok(body)
} else {
Err(Error::UnexpectedResponse {
status: response.status(),
body: response.text().await?,
})
}
}
async fn query_optional<T: for<'de> serde::Deserialize<'de>>(
&self,
path: &str,
) -> Result<Option<T>> {
let url = format!("{}/{}", self.url, path);
match self.client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
let body = response.json::<T>().await?;
Ok(Some(body))
} else if response.status() == reqwest::StatusCode::NOT_FOUND {
Ok(None)
} else {
return Err(Error::UnexpectedResponse {
status: response.status(),
body: response.text().await?,
});
}
}
Err(err) => Err(err.into()),
}
}
async fn query<T: for<'de> serde::Deserialize<'de>>(&self, path: &str) -> Result<T> {
let url = format!("{}/{}", self.url, path);
match self.client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
let body = response.json::<T>().await?;
Ok(body)
} else {
Err(Error::UnexpectedResponse {
status: response.status(),
body: response.text().await?,
})
}
}
Err(err) => Err(err.into()),
}
}
}
-88
View File
@@ -1,88 +0,0 @@
use actix_web::{dev::Server, http::KeepAlive, web::Data, App, HttpServer};
use log::info;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
use crate::api::CommandExecutor;
use crate::core::builder::NodeInfo;
pub(crate) mod client;
pub(crate) mod query;
pub(crate) mod submit;
/// Starts the HTTP server.
pub(crate) fn init(node_info: &NodeInfo, api: CommandExecutor) -> anyhow::Result<Server> {
print_startup_messages(node_info);
let server = HttpServer::new(move || {
App::new()
.app_data(Data::new(api.clone()))
.service(query::health)
.service(query::block_by_hash)
.service(query::block_certificates)
.service(query::block_by_height)
.service(query::block_broadcast_group)
.service(query::last_block)
.service(query::node_config)
.service(query::query_dht)
.service(query::broadcast_info)
.service(submit::submit_message)
.service(submit::store_in_dht)
.service(submit::verify_message_in_block)
.service(swagger_ui())
})
.keep_alive(KeepAlive::Os)
.bind((node_info.ip.as_str(), node_info.initial_config.http.port))?
.run();
Ok(server)
}
/// Builds the Swagger UI.
///
/// Note that all routes you want Swagger docs for must be in the `paths` annotation.
fn swagger_ui() -> SwaggerUi {
use crate::api::types;
#[derive(OpenApi)]
#[openapi(
paths(
query::health,
query::block_by_hash,
query::block_certificates,
query::block_by_height,
query::last_block,
query::block_broadcast_group,
query::node_config,
query::query_dht,
query::broadcast_info,
submit::submit_message,
submit::store_in_dht,
submit::verify_message_in_block
),
components(schemas(
types::ApiBlock,
types::ApiEphemeraMessage,
types::ApiCertificate,
types::ApiSignature,
types::ApiPublicKey,
types::ApiHealth,
types::HealthStatus,
types::ApiEphemeraConfig,
types::ApiDhtStoreRequest,
types::ApiDhtQueryRequest,
types::ApiDhtQueryResponse,
types::ApiBroadcastInfo,
types::ApiVerifyMessageInBlock,
))
)]
struct ApiDoc;
SwaggerUi::new("/swagger-ui/{_:.*}").url("/api-doc/openapi.json", ApiDoc::openapi())
}
/// Prints messages saying which ports HTTP is running on, and some helpful pointers
/// `OpenAPI` and `Swagger UI` endpoints.
fn print_startup_messages(info: &NodeInfo) {
let http_root = info.api_address_http();
info!("Server running on {}", http_root);
info!("Swagger UI: {}/swagger-ui/", http_root);
info!("OpenAPI spec is at: {}/api-doc/openapi.json", http_root);
}
-182
View File
@@ -1,182 +0,0 @@
use actix_web::{get, web, HttpResponse, Responder};
use log::error;
use crate::{
api::{types::ApiHealth, types::HealthStatus::Healthy, CommandExecutor},
ephemera_api::{ApiDhtQueryRequest, ApiDhtQueryResponse},
};
#[utoipa::path(
responses(
(status = 200, description = "Endpoint to check if the server is running")),
)]
#[get("/ephemera/node/health")]
#[allow(clippy::unused_async)]
pub(crate) async fn health() -> impl Responder {
HttpResponse::Ok().json(ApiHealth { status: Healthy })
}
#[utoipa::path(
responses(
(status = 200, description = "Get current broadcast group"),
(status = 500, description = "Server failed to process request")),
)]
#[get("/ephemera/broadcast/group/info")]
pub(crate) async fn broadcast_info(api: web::Data<CommandExecutor>) -> impl Responder {
match api.get_broadcast_info().await {
Ok(group) => HttpResponse::Ok().json(group),
Err(err) => {
error!("Failed to get current broadcast group: {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "GET block by hash"),
(status = 404, description = "Block not found"),
(status = 500, description = "Server failed to process request")),
params(("hash", description = "Block hash")),
)]
#[get("/ephemera/broadcast/block/{hash}")]
pub(crate) async fn block_by_hash(
hash: web::Path<String>,
api: web::Data<CommandExecutor>,
) -> impl Responder {
match api.get_block_by_id(hash.into_inner()).await {
Ok(Some(block)) => HttpResponse::Ok().json(block),
Ok(_) => HttpResponse::NotFound().json("Block not found"),
Err(err) => {
error!("Failed to get block by hash: {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "Get block signatures"),
(status = 404, description = "Certificates not found"),
(status = 500, description = "Server failed to process request")),
params(("hash", description = "Block hash")),
)]
#[get("/ephemera/broadcast/block/certificates/{hash}")]
pub(crate) async fn block_certificates(
hash: web::Path<String>,
api: web::Data<CommandExecutor>,
) -> impl Responder {
let id = hash.into_inner();
match api.get_block_certificates(id.clone()).await {
Ok(Some(signatures)) => HttpResponse::Ok().json(signatures),
Ok(_) => HttpResponse::NotFound().json("Certificates not found"),
Err(err) => {
error!("Failed to get signatures {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "Get block by height"),
(status = 404, description = "Block not found"),
(status = 500, description = "Server failed to process request")),
params(("height", description = "Block height")),
)]
#[get("/ephemera/broadcast/block/height/{height}")]
pub(crate) async fn block_by_height(
height: web::Path<u64>,
api: web::Data<CommandExecutor>,
) -> impl Responder {
match api.get_block_by_height(height.into_inner()).await {
Ok(Some(block)) => HttpResponse::Ok().json(block),
Ok(_) => HttpResponse::NotFound().json("Block not found"),
Err(err) => {
error!("Failed to get block {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "Get last block"),
(status = 500, description = "Server failed to process request")),
)]
//Need to use plural(blocks), otherwise overlaps with block_by_id route
#[get("/ephemera/broadcast/blocks/last")]
pub(crate) async fn last_block(api: web::Data<CommandExecutor>) -> impl Responder {
match api.get_last_block().await {
Ok(block) => HttpResponse::Ok().json(block),
Err(err) => {
error!("Failed to get block {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "Get block broadcast group"),
(status = 404, description = "Block not found"),
(status = 500, description = "Server failed to process request")),
params(("hash", description = "Block hash")),
)]
#[get("/ephemera/broadcast/block/broadcast_info/{hash}")]
pub(crate) async fn block_broadcast_group(
hash: web::Path<String>,
api: web::Data<CommandExecutor>,
) -> impl Responder {
let hash = hash.into_inner();
match api.get_block_broadcast_info(hash).await {
Ok(Some(group)) => HttpResponse::Ok().json(group),
Ok(_) => HttpResponse::NotFound().json("Block not found"),
Err(err) => {
error!("Failed to get block broadcast group {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "Get node config"),
(status = 500, description = "Server failed to process request")),
)]
#[get("/ephemera/node/config")]
pub(crate) async fn node_config(api: web::Data<CommandExecutor>) -> impl Responder {
match api.get_node_config().await {
Ok(config) => HttpResponse::Ok().json(config),
Err(err) => {
error!("Failed to get node config {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
responses(
(status = 200, description = "Query dht"),
(status = 500, description = "Server failed to process request")),
params(("query", description = "Dht query")),
)]
#[get("/ephemera/dht/query/{key}")]
pub(crate) async fn query_dht(
api: web::Data<CommandExecutor>,
key: web::Path<String>,
) -> impl Responder {
let key = ApiDhtQueryRequest::parse_key(key.into_inner().as_str());
match api.query_dht(key).await {
Ok(Some((key, value))) => {
let response = ApiDhtQueryResponse::new(key, value);
HttpResponse::Ok().json(response)
}
Ok(_) => HttpResponse::NotFound().json("Not found"),
Err(err) => {
error!("Failed to query dht {err}",);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
-88
View File
@@ -1,88 +0,0 @@
use actix_web::{post, web, HttpResponse};
use log::{debug, error};
use crate::api::types::ApiVerifyMessageInBlock;
use crate::api::{
types::{ApiDhtStoreRequest, ApiEphemeraMessage},
ApiError, CommandExecutor,
};
#[utoipa::path(
request_body = ApiEphemeraMessage,
responses(
(status = 200, description = "Send a message to an Ephemera node which will be broadcast to the network"),
(status = 500, description = "Server failed to process request")),
params(("message", description = "Message to send"))
)]
#[post("/ephemera/broadcast/submit_message")]
pub(crate) async fn submit_message(
message: web::Json<ApiEphemeraMessage>,
api: web::Data<CommandExecutor>,
) -> HttpResponse {
match api.send_ephemera_message(message.into_inner()).await {
Ok(_) => HttpResponse::Ok().json("Message submitted"),
Err(err) => {
if let ApiError::DuplicateMessage = err {
debug!("Message already submitted {err:?}");
HttpResponse::BadRequest().json("Message already submitted")
} else {
error!("Error submitting message: {}", err);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
}
#[utoipa::path(
request_body = ApiDhtStoreRequest,
responses(
(status = 200, description = "Request to store a value in the DHT"),
(status = 500, description = "Server failed to process request")),
params(
("request", description = "Dht store request")
)
)]
#[post("/ephemera/dht/store")]
pub(crate) async fn store_in_dht(
request: web::Json<ApiDhtStoreRequest>,
api: web::Data<CommandExecutor>,
) -> HttpResponse {
let request = request.into_inner();
let key = request.key();
let value = request.value();
match api.store_in_dht(key, value).await {
Ok(_) => HttpResponse::Ok().json("Store request submitted"),
Err(err) => {
error!("Error storing in dht: {}", err);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
#[utoipa::path(
request_body = ApiVerifyMessageInBlock,
responses(
(status = 200, description = "Verifies if given message is in block identified by block hash.\
Returns true if message is in block, false otherwise. False can also mean that block or message \
does not exist in that block."),
(status = 500, description = "Server failed to process request")),
params(
("request", description = "Verify message request")
)
)]
#[post("/ephemera/messages/verify")]
pub(crate) async fn verify_message_in_block(
request: web::Json<ApiVerifyMessageInBlock>,
api: web::Data<CommandExecutor>,
) -> HttpResponse {
let request = request.into_inner();
match api.verify_message_in_block(request).await {
Ok(valid) => HttpResponse::Ok().json(valid),
Err(err) => {
error!("Error verifying message: {}", err);
HttpResponse::InternalServerError().json("Server failed to process request")
}
}
}
-309
View File
@@ -1,309 +0,0 @@
//! # Ephemera API
//!
//! This module contains all the types and functions available as part of Ephemera public API.
//!
//! This API is also available over HTTP.
use std::fmt::Display;
use log::{error, trace};
use tokio::sync::{
mpsc::{channel, Receiver, Sender},
oneshot,
};
use crate::api::types::{
ApiBlock, ApiBlockBroadcastInfo, ApiBroadcastInfo, ApiCertificate, ApiEphemeraConfig,
ApiEphemeraMessage, ApiError, ApiVerifyMessageInBlock,
};
pub(crate) mod application;
pub(crate) mod http;
pub(crate) mod types;
/// Kademlia DHT key
pub(crate) type DhtKey = Vec<u8>;
/// Kademlia DHT value
pub(crate) type DhtValue = Vec<u8>;
/// Kademlia DHT key/value pair
pub(crate) type DhtKV = (DhtKey, DhtValue);
pub(crate) type Result<T> = std::result::Result<T, ApiError>;
#[derive(Debug)]
pub(crate) enum ToEphemeraApiCmd {
SubmitEphemeraMessage(Box<ApiEphemeraMessage>, oneshot::Sender<Result<()>>),
QueryBlockByHeight(u64, oneshot::Sender<Result<Option<ApiBlock>>>),
QueryBlockByHash(String, oneshot::Sender<Result<Option<ApiBlock>>>),
QueryLastBlock(oneshot::Sender<Result<ApiBlock>>),
QueryBlockCertificates(String, oneshot::Sender<Result<Option<Vec<ApiCertificate>>>>),
QueryDht(DhtKey, oneshot::Sender<Result<Option<DhtKV>>>),
StoreInDht(DhtKey, DhtValue, oneshot::Sender<Result<()>>),
QueryEphemeraConfig(oneshot::Sender<Result<ApiEphemeraConfig>>),
QueryBroadcastGroup(oneshot::Sender<Result<ApiBroadcastInfo>>),
QueryBlockBroadcastInfo(
String,
oneshot::Sender<Result<Option<ApiBlockBroadcastInfo>>>,
),
VerifyMessageInBlock(String, String, usize, oneshot::Sender<Result<bool>>),
}
impl Display for ToEphemeraApiCmd {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToEphemeraApiCmd::SubmitEphemeraMessage(message, _) => {
write!(f, "SubmitEphemeraMessage({message})",)
}
ToEphemeraApiCmd::QueryBlockByHeight(height, _) => {
write!(f, "QueryBlockByHeight({height})",)
}
ToEphemeraApiCmd::QueryBlockByHash(hash, _) => write!(f, "QueryBlockByHash({hash})",),
ToEphemeraApiCmd::QueryLastBlock(_) => write!(f, "QueryLastBlock"),
ToEphemeraApiCmd::QueryBlockCertificates(id, _) => {
write!(f, "QueryBlockSignatures{id}")
}
ToEphemeraApiCmd::QueryDht(_, _) => {
write!(f, "QueryDht")
}
ToEphemeraApiCmd::StoreInDht(_, _, _) => {
write!(f, "StoreInDht")
}
ToEphemeraApiCmd::QueryEphemeraConfig(_) => {
write!(f, "EphemeraConfig")
}
ToEphemeraApiCmd::QueryBroadcastGroup(_) => {
write!(f, "BroadcastGroup")
}
ToEphemeraApiCmd::QueryBlockBroadcastInfo(hash, ..) => {
write!(f, "BlockBroadcastInfo({hash})")
}
ToEphemeraApiCmd::VerifyMessageInBlock(block_id, message_id, height, _) => {
write!(
f,
"VerifyMessageInBlock({block_id}, {message_id}, {height})",
)
}
}
}
}
pub(crate) struct ApiListener {
pub(crate) messages_rcv: Receiver<ToEphemeraApiCmd>,
}
impl ApiListener {
pub(crate) fn new(messages_rcv: Receiver<ToEphemeraApiCmd>) -> Self {
Self { messages_rcv }
}
}
#[derive(Clone)]
pub struct CommandExecutor {
pub(crate) commands_channel: Sender<ToEphemeraApiCmd>,
}
impl CommandExecutor {
pub(crate) fn new() -> (CommandExecutor, ApiListener) {
let (commands_channel, signed_messages_rcv) = channel(100);
let api_listener = ApiListener::new(signed_messages_rcv);
let api = CommandExecutor { commands_channel };
(api, api_listener)
}
/// Returns block with given id if it exists
///
/// # Arguments
/// * `block_id` - Block id
///
/// # Returns
/// * `ApiBlock` - Block
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn get_block_by_id(&self, block_id: String) -> Result<Option<ApiBlock>> {
trace!("get_block_by_id({:?})", block_id);
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockByHash(block_id, tx))
.await
}
/// Returns block with given height if it exists
///
/// # Arguments
/// * `height` - Block height
///
/// # Returns
/// * `ApiBlock` - Block
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn get_block_by_height(&self, height: u64) -> Result<Option<ApiBlock>> {
trace!("get_block_by_height({:?})", height);
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockByHeight(height, tx))
.await
}
/// Returns last block. Which has maximum height and is stored in database
///
/// # Returns
/// * `ApiBlock` - Last block
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn get_last_block(&self) -> Result<ApiBlock> {
trace!("get_last_block()");
self.send_and_wait_response(ToEphemeraApiCmd::QueryLastBlock)
.await
}
/// Returns signatures for given block id
///
/// # Arguments
/// * `block_hash` - Block id
///
/// # Returns
/// * `Vec<ApiCertificate>` - Certificates
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn get_block_certificates(
&self,
block_hash: String,
) -> Result<Option<Vec<ApiCertificate>>> {
trace!("get_block_certificates({block_hash:?})",);
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockCertificates(block_hash, tx))
.await
}
/// Queries DHT for given key
///
/// # Arguments
/// * `key` - DHT key
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
///
/// # Returns
/// * `Some((key, value))` - If key is found
/// * `None` - If key is not found
pub async fn query_dht(&self, key: DhtKey) -> Result<Option<(DhtKey, DhtValue)>> {
trace!("get_dht({key:?})");
//TODO: this needs timeout(somewhere around dht query functionality)
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryDht(key, tx))
.await
}
/// Stores given key-value pair in DHT
///
/// # Arguments
/// * `key` - DHT key
/// * `value` - DHT value
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn store_in_dht(&self, key: DhtKey, value: DhtValue) -> Result<()> {
trace!("store_in_dht({key:?}, {value:?})");
self.send_and_wait_response(|tx| ToEphemeraApiCmd::StoreInDht(key, value, tx))
.await
}
/// Returns node configuration
///
/// # Returns
/// * `ApiEphemeraConfig` - Node configuration
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn get_node_config(&self) -> Result<ApiEphemeraConfig> {
trace!("get_node_config()");
self.send_and_wait_response(ToEphemeraApiCmd::QueryEphemeraConfig)
.await
}
/// Returns broadcast group
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
///
/// # Return
/// * `ApiBroadcastInfo` - Broadcast group
pub async fn get_broadcast_info(&self) -> Result<ApiBroadcastInfo> {
trace!("get_broadcast_group()");
self.send_and_wait_response(ToEphemeraApiCmd::QueryBroadcastGroup)
.await
}
/// Returns block broadcast info.
///
/// # Arguments
/// * `block_hash` - Block hash
///
/// # Return
/// * `ApiBlockBroadcastInfo` - Block broadcast info
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn get_block_broadcast_info(
&self,
block_hash: String,
) -> Result<Option<ApiBlockBroadcastInfo>> {
trace!("get_broadcast_group()");
self.send_and_wait_response(|tx| ToEphemeraApiCmd::QueryBlockBroadcastInfo(block_hash, tx))
.await
}
/// Send a message to Ephemera which should then be included in mempool and broadcast to all peers
///
/// # Arguments
/// * `message` - Message to be sent
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn send_ephemera_message(&self, message: ApiEphemeraMessage) -> Result<()> {
trace!("send_ephemera_message({message})",);
self.send_and_wait_response(|tx| {
ToEphemeraApiCmd::SubmitEphemeraMessage(message.into(), tx)
})
.await
}
/// Verifies if given message is in block identified by block hash
/// Returns true if message is in block, false otherwise. False can also mean that block or message
/// does not exist.
///
/// # Arguments
/// * `request` - Message and block hash
///
/// # Errors
/// * `ApiError::InternalError` - If there is an internal error
pub async fn verify_message_in_block(&self, request: ApiVerifyMessageInBlock) -> Result<bool> {
trace!("verify_message_in_block({request})",);
let block_hash = request.block_hash;
let message_hash = request.message_hash;
let index = request.message_index;
self.send_and_wait_response(|tx| {
ToEphemeraApiCmd::VerifyMessageInBlock(block_hash, message_hash, index, tx)
})
.await
}
async fn send_and_wait_response<F, R>(&self, f: F) -> Result<R>
where
F: FnOnce(oneshot::Sender<Result<R>>) -> ToEphemeraApiCmd,
R: Send + 'static,
{
let (tx, rcv) = oneshot::channel();
let cmd = f(tx);
if let Err(e) = self.commands_channel.send(cmd).await {
error!("Failed to send command to Ephemera: {e:?}",);
return Err(ApiError::Internal(
"Failed to receive response from Ephemera".to_string(),
));
}
rcv.await.map_err(|e| {
error!("Failed to receive response from Ephemera: {e:?}",);
ApiError::Internal("Failed to receive response from Ephemera".to_string())
})?
}
}
-659
View File
@@ -1,659 +0,0 @@
//! This module contains all the types that are used in the API.
//!
//! - `ApiEphemeraMessage`
//! - `RawApiEphemeraMessage`
//! - `ApiBlock`
//! - `ApiCertificate`
//! - `Health`
//! - `ApiError`
//! - `ApiEphemeraConfig`
//! - `ApiDhtQueryRequest`
//! - `ApiDhtQueryResponse`
//! - `ApiDhtStoreRequest`
//! - `ApiBroadcastInfo`
//! - `ApiBlockBroadcastInfo`
//! - `ApiVerifyMessageInBlock`
use std::collections::HashSet;
use std::fmt::Display;
use array_bytes::{bytes2hex, hex2bytes};
use log::error;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use utoipa::ToSchema;
use crate::peer::{PeerId, ToPeerId};
use crate::utilities::codec::{Codec, DecodingError, EncodingError, EphemeraCodec};
use crate::{
block::types::{block::Block, block::BlockHeader, message::EphemeraMessage},
codec::{Decode, Encode},
crypto::{Keypair, PublicKey},
ephemera_api,
utilities::{
crypto::{Certificate, Signature},
time::EphemeraTime,
},
};
#[derive(Error, Debug)]
pub enum ApiError {
#[error("Application rejected ephemera message")]
ApplicationRejectedMessage,
#[error("Duplicate message")]
DuplicateMessage,
#[error("Invalid hash: {0}")]
InvalidHash(String),
#[error("ApplicationError: {0}")]
Application(#[from] ephemera_api::ApplicationError),
#[error("Internal error: {0}")]
Internal(String),
}
/// # Ephemera message.
///
/// A message submitted to an Ephemera node will be gossiped to other nodes.
/// And will be eventually included in a Ephemera block.
///
/// It needs to signed by the sender. The signature is included in the certificate.
///
/// The fields of the message what are signed:
/// - timestamp
/// - label
/// - data
///
/// Currently it's up provided [`ephemera_api::application::Application`] to verify the signature.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiEphemeraMessage {
/// The timestamp of the message.
pub timestamp: u64,
/// The label of the message. It can be used to identify the type of a message for example.
pub label: String,
/// The data of the message. It is application specific.
pub data: Vec<u8>,
/// The certificate of the message. All messages are required to be signed.
pub certificate: ApiCertificate,
}
impl ApiEphemeraMessage {
#[must_use]
pub fn new(raw_message: RawApiEphemeraMessage, certificate: ApiCertificate) -> Self {
Self {
timestamp: raw_message.timestamp,
label: raw_message.label,
data: raw_message.data,
certificate,
}
}
/// Generates the message hash.
///
/// # Errors
/// - If internal hash function fails.
pub fn hash(&self) -> anyhow::Result<String> {
let em = EphemeraMessage::from(self.clone());
let hash = em.hash_with_default_hasher()?.to_string();
Ok(hash)
}
}
/// `RawApiEphemeraMessage` contains the fields of the `ApiEphemeraMessage` that are signed.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct RawApiEphemeraMessage {
/// The timestamp of the message. It's initialized when the message is created.
/// It uses UTC time.
pub timestamp: u64,
/// The label of the message. It can be used to identify the type of a message without decoding full data.
pub label: String,
/// The data of the message. It is application specific.
pub data: Vec<u8>,
}
impl RawApiEphemeraMessage {
#[must_use]
pub fn new(label: String, data: Vec<u8>) -> Self {
Self {
timestamp: EphemeraTime::now(),
label,
data,
}
}
/// Signs the message with the given keypair.
///
/// # Signing example
///
/// ```
/// use ephemera::codec::Encode;
/// use ephemera::crypto::{EphemeraKeypair, EphemeraPublicKey, Keypair};
/// use ephemera::ephemera_api::{ApiEphemeraMessage, RawApiEphemeraMessage};
///
/// let keypair = Keypair::generate(None);
/// let raw_message = RawApiEphemeraMessage::new("test".to_string(), vec![]);
///
/// let signed_message:ApiEphemeraMessage = raw_message.sign(&keypair).unwrap();
///
/// assert_eq!(signed_message.certificate.public_key, keypair.public_key().into());
///
/// let bytes = raw_message.encode().unwrap();
/// assert!(keypair.public_key().verify(&bytes, &signed_message.certificate.signature.into()));
/// ```
///
/// # Errors
/// - If the message can't be encoded.
/// - If the message can't be signed.
pub fn sign(&self, keypair: &Keypair) -> anyhow::Result<ApiEphemeraMessage> {
let certificate = Certificate::prepare(keypair, &self)?;
let message = ApiEphemeraMessage::new(self.clone(), certificate.into());
Ok(message)
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ApiBlockHeader {
/// The timestamp of the block. It's initialized when the block is created.
/// It uses UTC time.
pub timestamp: u64,
/// The PeerId of the block producer instance.
pub creator: PeerId,
/// The height of the block.
pub height: u64,
/// The hash of the current block.
pub hash: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiBlock {
pub header: ApiBlockHeader,
pub messages: Vec<ApiEphemeraMessage>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct ApiRawBlock {
pub(crate) header: ApiBlockHeader,
pub(crate) messages: Vec<ApiEphemeraMessage>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiCertificate {
pub signature: ApiSignature,
pub public_key: ApiPublicKey,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiSignature(pub(crate) Signature);
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiPublicKey(pub(crate) PublicKey);
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiEphemeraConfig {
/// The address of the node. It's the address what Ephemera instance uses to communicate with other nodes.
pub protocol_address: String,
/// The HTTP API address of the node.
pub api_address: String,
/// The WebSocket address of the node.
pub websocket_address: String,
/// Node's public key.
///
/// # Converting to string and back example
/// ```
/// use ephemera::crypto::{EphemeraKeypair, Keypair, PublicKey};
///
/// let keypair = Keypair::generate(None);
/// let public_key = keypair.public_key().to_string();
///
/// let from_str = public_key.parse::<PublicKey>().unwrap();
///
/// assert_eq!(keypair.public_key(), from_str);
/// ```
pub public_key: String,
/// True if the node is a block producer. It's a configuration option.
pub block_producer: bool,
/// The interval of block creation in seconds. It's a configuration option.
pub block_creation_interval_sec: u64,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiDhtQueryRequest {
/// The key to query for in hex format.
key: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiDhtQueryResponse {
/// The key that was queried for in hex format.
key: String,
/// The value that was stored under the queried key in hex format.
value: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub enum HealthStatus {
Healthy,
Unhealthy,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiHealth {
pub(crate) status: HealthStatus,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiDhtStoreRequest {
/// The key to store the value under in hex format.
key: String,
/// The value to store in hex format.
value: String,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiBroadcastInfo {
/// The PeerId of the local node.
pub local_peer_id: PeerId,
/// The list of the current members of the network.
pub current_members: HashSet<PeerId>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiBlockBroadcastInfo {
pub local_peer_id: PeerId,
pub broadcast_group: Vec<PeerId>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ToSchema)]
pub struct ApiVerifyMessageInBlock {
pub block_hash: String,
pub message_hash: String,
pub message_index: usize,
}
impl ApiVerifyMessageInBlock {
#[must_use]
pub fn new(block_hash: String, message_hash: String, message_index: usize) -> Self {
Self {
block_hash,
message_hash,
message_index,
}
}
}
impl Display for ApiVerifyMessageInBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{{ block_hash: {}, message_hash: {} }}",
self.block_hash, self.message_hash
)
}
}
impl ApiBlockBroadcastInfo {
pub(crate) fn new(local_peer_id: PeerId, broadcast_group: Vec<PeerId>) -> Self {
Self {
local_peer_id,
broadcast_group,
}
}
}
impl ApiBroadcastInfo {
pub(crate) fn new(current_members: HashSet<PeerId>, local_peer_id: PeerId) -> Self {
Self {
local_peer_id,
current_members,
}
}
}
impl Display for ApiBroadcastInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let current_members = self.current_members.iter().map(ToString::to_string);
write!(
f,
"{{ local_peer_id: {}, current_members: {current_members:?} }}",
self.local_peer_id,
)
}
}
impl Display for ApiEphemeraMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ApiEphemeraMessage(timestamp: {}, label: {})",
self.timestamp, self.label,
)
}
}
impl From<ApiEphemeraMessage> for RawApiEphemeraMessage {
fn from(message: ApiEphemeraMessage) -> Self {
RawApiEphemeraMessage {
timestamp: message.timestamp,
label: message.label,
data: message.data,
}
}
}
impl From<ApiEphemeraMessage> for EphemeraMessage {
fn from(message: ApiEphemeraMessage) -> Self {
Self {
timestamp: message.timestamp,
label: message.label,
data: message.data,
certificate: message.certificate.into(),
}
}
}
impl Decode for RawApiEphemeraMessage {
type Output = Self;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
Codec::decode(bytes)
}
}
impl Encode for RawApiEphemeraMessage {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(self)
}
}
impl Encode for &RawApiEphemeraMessage {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(self)
}
}
impl Display for ApiBlockHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ApiBlockHeader(timestamp: {}, creator: {}, height: {}, hash: {})",
self.timestamp, self.creator, self.height, self.hash,
)
}
}
impl ApiBlock {
#[must_use]
pub fn as_raw_block(&self) -> ApiRawBlock {
ApiRawBlock {
header: self.header.clone(),
messages: self.messages.clone(),
}
}
#[must_use]
pub fn message_count(&self) -> usize {
self.messages.len()
}
#[must_use]
pub fn hash(&self) -> String {
self.header.hash.clone()
}
/// # Errors
/// - If the block is invalid.
/// - If the block's certificate is invalid.
/// - If the block's certificate is not signed by the block's creator.
pub fn verify(&self, certificate: &ApiCertificate) -> Result<bool, ApiError> {
let block: Block = self.clone().try_into()?;
let valid = block.verify(&(certificate.clone()).into()).map_err(|e| {
error!("Failed to verify block: {}", e);
ApiError::Internal("Failed to verify block certificate".to_string())
})?;
Ok(valid)
}
}
impl Display for ApiBlock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ApiBlock(header: {}, message_count: {})",
self.header,
self.message_count()
)
}
}
impl ApiRawBlock {
pub fn new(header: ApiBlockHeader, messages: Vec<ApiEphemeraMessage>) -> Self {
Self { header, messages }
}
}
impl ApiCertificate {
/// # Errors
/// - `EncodingError` if the message cannot be encoded.
/// - `KeyPairError` if the message cannot be signed.
pub fn prepare<D: Encode>(key_pair: &Keypair, data: &D) -> anyhow::Result<Self> {
Certificate::prepare(key_pair, data).map(Into::into)
}
/// # Errors
/// -`EncodingError` if the message cannot be encoded.
pub fn verify<D: Encode>(&self, data: &D) -> anyhow::Result<bool> {
let certificate: Certificate = (self.clone()).into();
Certificate::verify(&certificate, data)
}
}
impl From<EphemeraMessage> for ApiEphemeraMessage {
fn from(ephemera_message: EphemeraMessage) -> Self {
Self {
timestamp: ephemera_message.timestamp,
label: ephemera_message.label,
data: ephemera_message.data,
certificate: ApiCertificate {
signature: ephemera_message.certificate.signature.into(),
public_key: ephemera_message.certificate.public_key.into(),
},
}
}
}
impl From<Certificate> for ApiCertificate {
fn from(signature: Certificate) -> Self {
Self {
signature: signature.signature.into(),
public_key: signature.public_key.into(),
}
}
}
impl From<ApiCertificate> for Certificate {
fn from(value: ApiCertificate) -> Self {
Certificate {
signature: value.signature.into(),
public_key: value.public_key.into(),
}
}
}
impl From<&Block> for &ApiBlock {
fn from(block: &Block) -> Self {
let api_block: ApiBlock = block.clone().into();
Box::leak(Box::new(api_block))
}
}
impl From<Signature> for ApiSignature {
fn from(signature: Signature) -> Self {
Self(signature)
}
}
impl From<ApiSignature> for Signature {
fn from(signature: ApiSignature) -> Self {
signature.0
}
}
impl ApiPublicKey {
pub fn peer_id(&self) -> String {
self.0.peer_id().to_string()
}
}
impl From<PublicKey> for ApiPublicKey {
fn from(public_key: PublicKey) -> Self {
Self(public_key)
}
}
impl From<ApiPublicKey> for PublicKey {
fn from(public_key: ApiPublicKey) -> Self {
public_key.0
}
}
impl From<Block> for ApiBlock {
fn from(block: Block) -> Self {
Self {
header: ApiBlockHeader {
timestamp: block.header.timestamp,
creator: block.header.creator,
height: block.header.height,
hash: block.header.hash.to_string(),
},
messages: block.messages.into_iter().map(Into::into).collect(),
}
}
}
impl TryFrom<ApiBlock> for Block {
type Error = ApiError;
fn try_from(api_block: ApiBlock) -> Result<Self, ApiError> {
let messages: Vec<EphemeraMessage> = api_block
.messages
.into_iter()
.map(Into::into)
.collect::<Vec<EphemeraMessage>>();
Ok(Self {
header: BlockHeader {
timestamp: api_block.header.timestamp,
creator: api_block.header.creator,
height: api_block.header.height,
hash: api_block.header.hash.parse().map_err(|e| {
error!("Failed to parse block hash: {}", e);
ApiError::Internal("Failed to parse block hash".to_string())
})?,
},
messages,
})
}
}
impl ApiDhtStoreRequest {
#[must_use]
pub fn new(key: &[u8], value: &[u8]) -> Self {
let key = bytes2hex("0x", key);
let value = bytes2hex("0x", value);
Self { key, value }
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn key(&self) -> Vec<u8> {
//We can unwrap here because the key is always valid.
hex2bytes(&self.key).unwrap()
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn value(&self) -> Vec<u8> {
//We can unwrap here because the value is always valid.
hex2bytes(&self.value).unwrap()
}
}
impl ApiDhtQueryRequest {
#[must_use]
pub fn new(key: &[u8]) -> Self {
let key = bytes2hex("0x", key);
Self { key }
}
#[must_use]
pub fn key_encoded(&self) -> String {
self.key.clone()
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn key(&self) -> Vec<u8> {
//We can unwrap here because the value is always valid.
hex2bytes(&self.key).unwrap()
}
pub(crate) fn parse_key(key: &str) -> Vec<u8> {
hex2bytes(key).unwrap()
}
}
impl ApiDhtQueryResponse {
pub(crate) fn new(key: Vec<u8>, value: Vec<u8>) -> Self {
let key = bytes2hex("0x", key);
let value = bytes2hex("0x", value);
Self { key, value }
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn key(&self) -> Vec<u8> {
//We can unwrap here because the key is always valid.
hex2bytes(&self.key).unwrap()
}
#[allow(clippy::missing_panics_doc)]
#[must_use]
pub fn value(&self) -> Vec<u8> {
//We can unwrap here because the value is always valid.
hex2bytes(&self.value).unwrap()
}
}
#[cfg(test)]
mod test {
use crate::crypto::EphemeraKeypair;
use crate::crypto::Keypair;
use super::*;
#[test]
fn test_message_sign_ok() {
let message_signing_keypair = Keypair::generate(None);
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
let signed_message = message
.sign(&message_signing_keypair)
.expect("Failed to sign message");
let certificate = signed_message.certificate;
assert!(certificate.verify(&message).unwrap());
}
#[test]
fn test_message_sign_fail() {
let message_signing_keypair = Keypair::generate(None);
let message = RawApiEphemeraMessage::new("test1".to_string(), vec![1, 2, 3]);
let signed_message = message
.sign(&message_signing_keypair)
.expect("Failed to sign message");
let certificate = signed_message.certificate;
let modified_message = RawApiEphemeraMessage::new("test2".to_string(), vec![1, 2, 3]);
assert!(!certificate.verify(&modified_message).unwrap());
}
}
-74
View File
@@ -1,74 +0,0 @@
use std::collections::HashSet;
use std::{sync::Arc, time::Duration};
use log::{debug, info};
use crate::block::manager::State;
use crate::peer::ToPeerId;
use crate::{
block::{
manager::{BlockChainState, BlockManager},
message_pool::MessagePool,
producer::BlockProducer,
types::block::Block,
},
broadcast::signing::BlockSigner,
config::BlockManagerConfiguration,
crypto::Keypair,
storage::EphemeraDatabase,
};
pub(crate) struct BlockManagerBuilder {
config: BlockManagerConfiguration,
block_producer: BlockProducer,
keypair: Arc<Keypair>,
}
impl BlockManagerBuilder {
pub(crate) fn new(config: BlockManagerConfiguration, keypair: Arc<Keypair>) -> Self {
let block_producer = BlockProducer::new(keypair.peer_id());
Self {
config,
block_producer,
keypair,
}
}
pub(crate) fn build<D: EphemeraDatabase + ?Sized>(
self,
storage: &mut D,
) -> anyhow::Result<BlockManager> {
let mut most_recent_block = storage.get_last_block()?;
if most_recent_block.is_none() {
//Although Ephemera is not a blockchain(chain of historically dependent blocks),
//it's helpful to have some sort of notion of progress in time. So we use the concept of height.
//The genesis block helps to define the start of it.
info!("No last block found in database. Creating genesis block.");
let genesis_block = Block::new_genesis_block(self.block_producer.peer_id);
storage.store_block(&genesis_block, HashSet::new(), HashSet::new())?;
most_recent_block = Some(genesis_block);
}
let last_created_block = most_recent_block.expect("Block should be present");
debug!("Most recent block: {:?}", last_created_block);
let block_signer = BlockSigner::new(self.keypair.clone());
let message_pool = MessagePool::new();
let block_chain_state = BlockChainState::new(last_created_block);
let block_creation_interval =
tokio::time::interval(Duration::from_secs(self.config.creation_interval_sec));
Ok(BlockManager {
config: self.config,
block_producer: self.block_producer,
block_signer,
message_pool,
block_chain_state,
state: State::Paused,
backoff: None,
block_creation_interval,
})
}
}
-688
View File
@@ -1,688 +0,0 @@
use std::collections::HashSet;
use std::future::Future;
use std::task::Poll;
use std::time::Duration;
use std::{
num::NonZeroUsize,
pin::Pin,
task,
task::Poll::{Pending, Ready},
};
use anyhow::anyhow;
use futures::Stream;
use futures_util::FutureExt;
use log::{debug, error, info, trace};
use lru::LruCache;
use thiserror::Error;
use tokio::time;
use tokio::time::{Instant, Interval};
use crate::network::PeerId;
use crate::peer::ToPeerId;
use crate::{
api::application::RemoveMessages,
block::{
message_pool::MessagePool,
producer::BlockProducer,
types::{block::Block, message::EphemeraMessage},
},
broadcast::signing::BlockSigner,
config::BlockManagerConfiguration,
utilities::{crypto::Certificate, hash::Hash},
};
pub(crate) type Result<T> = std::result::Result<T, BlockManagerError>;
#[derive(Error, Debug)]
pub(crate) enum BlockManagerError {
#[error("Message is already in pool: {0}")]
DuplicateMessage(String),
//Just a placeholder for now
#[error("BlockManagerError: {0}")]
BlockManager(#[from] anyhow::Error),
}
/// It helps to use atomic state management for new blocks.
pub(crate) struct BlockChainState {
pub(crate) last_blocks: LruCache<Hash, Block>,
/// Last block that we created.
/// It's not Option because we always have genesis block
last_produced_block: Option<Block>,
/// Last block that we accepted
/// It's not Option because we always have genesis block
last_committed_block: Block,
}
impl BlockChainState {
pub(crate) fn new(last_committed_block: Block) -> Self {
Self {
//1000 is just a "big enough".
last_blocks: LruCache::new(NonZeroUsize::new(1000).unwrap()),
last_produced_block: None,
last_committed_block,
}
}
fn mark_last_produced_block_as_committed(&mut self) {
self.last_committed_block = self
.last_produced_block
.take()
.expect("Block should be present");
}
fn is_last_produced_block(&self, hash: Hash) -> bool {
match self.last_produced_block.as_ref() {
Some(block) => block.get_hash() == hash,
None => false,
}
}
fn is_last_produced_block_is_pending(&self) -> bool {
self.last_produced_block.is_some()
}
fn next_block_height(&self) -> u64 {
self.last_committed_block.get_height() + 1
}
fn remove_last_produced_block(&mut self) -> Block {
self.last_produced_block
.take()
.expect("Block should be present")
}
}
pub(crate) enum State {
Paused,
Running,
}
#[derive(Debug)]
pub(crate) struct BackOffInterval {
/// Maximum number of attempts before this backoff expires.
maximum_times: u32,
/// Number of attempts that have been made so far.
nr_of_attempts: u32,
/// Backoff rate. Previous delay is multiplied by this rate to get next delay.
backoff_rate: u32,
/// Delay between before next attempt.
delay: Interval,
}
impl BackOffInterval {
fn new(maximum_times: u32, backoff_rate: u32, initial_wait: Duration) -> Self {
let delay = time::interval_at(Instant::now() + initial_wait, initial_wait);
Self {
maximum_times,
nr_of_attempts: 0,
backoff_rate,
delay,
}
}
fn is_expired(&self) -> bool {
self.nr_of_attempts >= self.maximum_times
}
}
impl Future for BackOffInterval {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll<Self::Output> {
if self.nr_of_attempts >= self.maximum_times {
debug!("Backoff expired after {} attempts", self.nr_of_attempts);
return Pending;
}
match Pin::new(&mut self.delay).poll_tick(cx) {
Ready(_) => {
self.nr_of_attempts += 1;
let next_tick = Instant::now()
+ self.delay.period() * self.backoff_rate.pow(self.nr_of_attempts);
debug!("Backoff attempt: {}", self.nr_of_attempts);
self.delay = time::interval_at(next_tick, self.delay.period());
Ready(())
}
Pending => Pending,
}
}
}
pub(crate) struct BlockManager {
pub(crate) config: BlockManagerConfiguration,
/// Block producer. Simple helper that creates blocks
pub(crate) block_producer: BlockProducer,
/// Message pool. Contains all messages that we received from the network and not included in any(committed) block yet.
pub(crate) message_pool: MessagePool,
/// Delay between block creation attempts.
pub(crate) block_creation_interval: Interval,
/// Backoff between block creation attempts. When `last_produced_block` is not committed during
/// certain time window, and normal delay is not passed yet, we use backoff delay to try again.
pub(crate) backoff: Option<BackOffInterval>,
/// Signs and verifies blocks
pub(crate) block_signer: BlockSigner,
/// State management for new blocks
pub(crate) block_chain_state: BlockChainState,
/// Current state of the block manager
pub(crate) state: State,
}
impl BlockManager {
pub(crate) fn on_new_message(&mut self, msg: EphemeraMessage) -> Result<()> {
trace!("Message received: {:?}", msg);
let message_hash = msg.hash_with_default_hasher()?;
if self.message_pool.contains(&message_hash) {
return Err(BlockManagerError::DuplicateMessage(
message_hash.to_string(),
));
}
self.message_pool.add_message(msg)?;
Ok(())
}
pub(crate) fn on_block(
&mut self,
sender: &PeerId,
block: &Block,
certificate: &Certificate,
) -> Result<()> {
let hash = block.hash_with_default_hasher()?;
trace!(
"Received block: {:?} from peer {sender:?}",
block.get_hash()
);
//Reject blocks with invalid hash
if block.header.hash != hash {
return Err(anyhow!("Block hash is invalid: {} != {hash}", block.header.hash).into());
}
//Block signer should be also its sender
let signer_peer_id = certificate.public_key.peer_id();
if *sender != signer_peer_id {
return Err(anyhow!(
"Block signer is not the block sender: {sender:?} != {signer_peer_id:?}",
)
.into());
}
//Verify that block signature is valid
if self.block_signer.verify_block(block, certificate).is_err() {
return Err(anyhow!("Block signature is invalid: {hash}").into());
}
self.block_chain_state.last_blocks.put(hash, block.clone());
Ok(())
}
pub(crate) fn sign_block(&mut self, block: &Block) -> Result<Certificate> {
let hash = block.hash_with_default_hasher()?;
trace!("Signing block: {block}");
let certificate = self.block_signer.sign_block(block, &hash)?;
trace!("Block certificate: {certificate:?}",);
Ok(certificate)
}
pub(crate) fn on_application_rejected_block(
&mut self,
messages_to_remove: RemoveMessages,
) -> Result<()> {
debug!("Application rejected last created block");
let last_produced_block = self.block_chain_state.remove_last_produced_block();
match messages_to_remove {
RemoveMessages::All => {
let messages = last_produced_block
.messages
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
debug!("Removing block messages from pool: all: {messages:?}",);
self.message_pool.remove_messages(&messages)?;
}
RemoveMessages::Selected(messages) => {
debug!("Removing block messages from pool: selected: {messages:?}",);
let messages = messages.into_iter().map(Into::into).collect::<Vec<_>>();
self.message_pool.remove_messages(messages.as_slice())?;
}
};
Ok(())
}
/// After a block gets committed, clear up mempool from its messages
pub(crate) fn on_block_committed(&mut self, block: &Block) -> Result<()> {
info!("Block committed: {}", block);
let hash = &block.header.hash;
if !self.block_chain_state.is_last_produced_block(*hash) {
let last_produced_block = self
.block_chain_state
.last_produced_block
.as_ref()
.expect("Last produced block should be present");
log::error!(
"Received unexpected committed block: {hash}, was expecting: {}",
last_produced_block.get_hash()
);
panic!("Received committed block which isn't last produced block, this is a bug!");
}
match self.message_pool.remove_messages(&block.messages) {
Ok(_) => {
self.block_chain_state
.mark_last_produced_block_as_committed();
}
Err(e) => {
return Err(anyhow!("Failed to remove messages from mempool: {}", e).into());
}
}
Ok(())
}
pub(crate) fn get_block_by_hash(&mut self, block_id: &Hash) -> Option<Block> {
self.block_chain_state.last_blocks.get(block_id).cloned()
}
pub(crate) fn get_block_certificates(&mut self, hash: &Hash) -> Option<&HashSet<Certificate>> {
self.block_signer.get_block_certificates(hash)
}
pub(crate) fn stop(&mut self) {
debug!("Stopping block creation");
self.state = State::Paused;
self.backoff = None;
}
pub(crate) fn start(&mut self) {
if !self.config.producer {
return;
}
if let State::Running = self.state {
return;
}
debug!("Starting block creation");
self.state = State::Running;
self.block_creation_interval =
tokio::time::interval(Duration::from_secs(self.config.creation_interval_sec));
}
}
//Produces blocks at a predefined interval.
//If blocks will be actually broadcast depends on the application.
impl Stream for BlockManager {
type Item = (Block, Certificate);
fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context) -> Poll<Option<Self::Item>> {
//Optionally it is possible to turn off block production and let the node behave just as voter.
//For example for testing purposes.
if !self.config.producer {
return Pending;
}
//It is dynamically turned off when node is not part of most recent broadcast group.
if let State::Paused = self.state {
return Pending;
}
let is_previous_pending = self.block_chain_state.is_last_produced_block_is_pending();
if !is_previous_pending {
self.backoff = None;
}
if self.block_creation_interval.poll_tick(cx).is_pending() {
if let Some(mut backoff) = self.backoff.take() {
if backoff.is_expired() {
return Pending;
}
if backoff.poll_unpin(cx).is_pending() {
self.backoff = Some(backoff);
return Pending;
}
self.backoff = Some(backoff);
} else {
return Pending;
}
} else {
self.backoff = None;
}
//If backoff is expired and we still don't have previous block committed
let repeat_previous = is_previous_pending && self.config.repeat_last_block_messages;
let pending_messages = if repeat_previous {
let block = self
.block_chain_state
.last_produced_block
.clone()
.expect("Block should be present");
//Use only previous block messages but create new block with new timestamp.
debug!("Producing block with previous messages");
block.messages
} else {
debug!("Producing block with new messages");
self.message_pool.get_messages()
};
let new_height = self.block_chain_state.next_block_height();
let created_block = self
.block_producer
.create_block(new_height, pending_messages);
if let Ok(block) = created_block {
info!("Created block: {}", block);
let hash = block.get_hash();
self.block_chain_state.last_produced_block = Some(block.clone());
self.block_chain_state.last_blocks.put(hash, block.clone());
let certificate = self
.block_signer
.sign_block(&block, &hash)
.expect("Failed to sign block");
if self.backoff.is_none() {
let backoff = BackOffInterval::new(100, 2, Duration::from_secs(10));
self.backoff = Some(backoff);
}
Ready(Some((block, certificate)))
} else {
error!("Error producing block: {:?}", created_block);
Pending
}
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use std::time::Duration;
use assert_matches::assert_matches;
use futures_util::StreamExt;
use crate::crypto::{EphemeraKeypair, Keypair};
use crate::ephemera_api::RawApiEphemeraMessage;
use super::*;
#[tokio::test]
async fn test_add_message() {
let (mut manager, _) = block_manager_with_defaults();
let signed_message = message("test");
let hash = signed_message.hash_with_default_hasher().unwrap();
manager.on_new_message(signed_message).unwrap();
assert!(manager.message_pool.contains(&hash));
}
#[tokio::test]
async fn test_add_duplicate_message() {
let (mut manager, _) = block_manager_with_defaults();
let signed_message = message("test");
manager.on_new_message(signed_message.clone()).unwrap();
assert_matches!(
manager.on_new_message(signed_message),
Err(BlockManagerError::DuplicateMessage(_))
);
}
#[tokio::test]
async fn test_accept_valid_block() {
let (mut manager, peer_id) = block_manager_with_defaults();
let block = block();
let certificate = manager.sign_block(&block).unwrap();
let result = manager.on_block(&peer_id, &block, &certificate);
assert!(result.is_ok());
}
#[tokio::test]
async fn test_reject_invalid_sender() {
let (mut manager, _) = block_manager_with_defaults();
let block = block();
let certificate = manager.sign_block(&block).unwrap();
let invalid_peer_id = PeerId::from_public_key(&Keypair::generate(None).public_key());
let result = manager.on_block(&invalid_peer_id, &block, &certificate);
assert!(result.is_err());
}
#[tokio::test]
async fn test_reject_invalid_hash() {
let (mut manager, peer_id) = block_manager_with_defaults();
let mut block = block();
let certificate = manager.sign_block(&block).unwrap();
block.header.hash = Hash::new([0; 32]);
let result = manager.on_block(&peer_id, &block, &certificate);
assert!(result.is_err());
}
#[tokio::test]
async fn test_reject_invalid_signature() {
let (mut manager, peer_id) = block_manager_with_defaults();
let correct_block = block();
let fake_block = block();
let fake_certificate = manager.sign_block(&fake_block).unwrap();
let correct_certificate = manager.sign_block(&correct_block).unwrap();
let result = manager.on_block(&peer_id, &correct_block, &fake_certificate);
assert!(result.is_err());
let result = manager.on_block(&peer_id, &fake_block, &correct_certificate);
assert!(result.is_err());
}
#[tokio::test]
async fn test_next_block_empty() {
let (mut manager, _) = block_manager_with_defaults();
let (block, _) = manager.next().await.unwrap();
assert_eq!(block.header.height, 1);
assert!(block.messages.is_empty());
}
#[tokio::test]
async fn test_next_block_with_message() {
let (mut manager, _) = block_manager_with_defaults();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
match manager.next().await {
Some((block, _)) => {
assert_eq!(block.header.height, 1);
assert_eq!(block.messages.len(), 1);
}
None => {
panic!("No block produced");
}
}
}
#[tokio::test]
async fn test_next_block_previous_not_committed_repeat() {
let (mut manager, _) = block_manager_with_defaults();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
let (block1, _) = manager.next().await.unwrap();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
let (block2, _) = manager.next().await.unwrap();
assert_eq!(block1.messages.len(), block2.messages.len());
assert_eq!(block1.header.height, block2.header.height);
}
#[tokio::test]
async fn test_next_block_previous_not_committed_repeat_false() {
let config = BlockManagerConfiguration::new(true, 0, false);
let (mut manager, _) = block_manager_with_config(config);
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
let (block1, _) = manager.next().await.unwrap();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
let (block2, _) = manager.next().await.unwrap();
assert_eq!(block1.messages.len(), 1);
assert_eq!(block2.messages.len(), 2);
//We create new block but don't leave gap
assert_eq!(block1.header.height, block2.header.height);
}
#[tokio::test]
async fn test_on_committed_with_correct_pending_block() {
let (mut manager, _) = block_manager_with_defaults();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
let (block, _) = manager.next().await.unwrap();
let result = manager.on_block_committed(&block);
assert!(result.is_ok());
assert!(manager.message_pool.get_messages().is_empty());
}
#[tokio::test]
#[should_panic]
async fn test_on_committed_with_invalid_pending_block() {
let (mut manager, _) = block_manager_with_defaults();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
manager.next().await.unwrap();
//Create invalid block
let wrong_block = block();
//This shouldn't remove messages from the pool
manager.on_block_committed(&wrong_block).unwrap();
}
#[tokio::test]
async fn application_rejected_messages_all() {
let (mut manager, _) = block_manager_with_defaults();
//Add messages to pool
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
let signed_message = message("test");
manager.on_new_message(signed_message).unwrap();
//Produce new block
manager.next().await.unwrap();
//Application Rejects the block with ALL messages
manager
.on_application_rejected_block(RemoveMessages::All)
.unwrap();
assert!(manager.message_pool.get_messages().is_empty());
}
#[tokio::test]
async fn application_rejected_messages_selected() {
let (mut manager, _) = block_manager_with_defaults();
//Add messages to pool
let signed_message1 = message("test");
manager.on_new_message(signed_message1.clone()).unwrap();
let signed_message2 = message("test");
manager.on_new_message(signed_message2.clone()).unwrap();
//Produce new block
manager.next().await.unwrap();
//Application Rejects the block with ALL messages
manager
.on_application_rejected_block(RemoveMessages::Selected(vec![signed_message2.into()]))
.unwrap();
assert_eq!(manager.message_pool.get_messages().len(), 1);
let message = manager
.message_pool
.get_messages()
.into_iter()
.next()
.unwrap();
assert_eq!(message, signed_message1);
}
fn block_manager_with_defaults() -> (BlockManager, PeerId) {
let config = BlockManagerConfiguration::new(true, 0, true);
block_manager_with_config(config)
}
fn block_manager_with_config(config: BlockManagerConfiguration) -> (BlockManager, PeerId) {
let keypair: Arc<Keypair> = Keypair::generate(None).into();
let peer_id = keypair.public_key().peer_id();
let genesis_block = Block::new_genesis_block(peer_id);
let block_chain_state = BlockChainState::new(genesis_block);
(
BlockManager {
config,
block_producer: BlockProducer::new(peer_id),
message_pool: MessagePool::new(),
block_creation_interval: tokio::time::interval(Duration::from_millis(1)),
backoff: None,
block_signer: BlockSigner::new(keypair),
block_chain_state,
state: State::Running,
},
peer_id,
)
}
fn block() -> Block {
let keypair: Arc<Keypair> = Keypair::generate(None).into();
let peer_id = keypair.public_key().peer_id();
let mut producer = BlockProducer::new(peer_id);
producer.create_block(1, vec![]).unwrap()
}
fn message(label: &str) -> EphemeraMessage {
let message1 = RawApiEphemeraMessage::new(label.into(), vec![1, 2, 3]);
let keypair = Keypair::generate(None);
let signed_message1 = message1.sign(&keypair).expect("Failed to sign message");
signed_message1.into()
}
}
-87
View File
@@ -1,87 +0,0 @@
//! Message pool for Ephemera messages
//!
//! It stores pending Ephemera messages which will be added to a future block.
//! It doesn't have any other logic than just storing messages.
//!
//! It's up to the user provided [`crate::ephemera_api::Application::check_tx`] to decide which messages to include.
use std::collections::HashMap;
use log::{trace, warn};
use crate::block::types::message::EphemeraMessage;
use crate::utilities::hash::Hash;
pub(crate) struct MessagePool {
pending_messages: HashMap<Hash, EphemeraMessage>,
}
impl MessagePool {
pub(super) fn new() -> Self {
Self {
pending_messages: HashMap::default(),
}
}
pub(crate) fn contains(&self, hash: &Hash) -> bool {
self.pending_messages.contains_key(hash)
}
pub(super) fn add_message(&mut self, msg: EphemeraMessage) -> anyhow::Result<()> {
trace!("Adding message to pool: {:?}", msg);
let msg_hash = msg.hash_with_default_hasher()?;
self.pending_messages.insert(msg_hash, msg);
trace!("Message pool size: {:?}", self.pending_messages.len());
Ok(())
}
pub(super) fn remove_messages(&mut self, messages: &[EphemeraMessage]) -> anyhow::Result<()> {
trace!(
"Mempool size before removing messages {}",
self.pending_messages.len()
);
for msg in messages {
let hash = msg.hash_with_default_hasher()?;
if self.pending_messages.remove(&hash).is_none() {
warn!("Message not found in pool: {:?}", msg);
}
}
trace!(
"Mempool size after removing messages {}",
self.pending_messages.len()
);
Ok(())
}
/// Returns a `Vec` of all `EphemeraMessage`s in the message pool.
/// The message pool is not cleared.
pub(super) fn get_messages(&self) -> Vec<EphemeraMessage> {
self.pending_messages.values().cloned().collect()
}
}
#[cfg(test)]
mod test {
use crate::block::message_pool::MessagePool;
use crate::block::types::message::EphemeraMessage;
use crate::crypto::{EphemeraKeypair, Keypair};
use crate::ephemera_api::RawApiEphemeraMessage;
#[test]
fn test_add_remove() {
let keypair = Keypair::generate(None);
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
let signed_message = message.sign(&keypair).expect("Failed to sign message");
let signed_message: EphemeraMessage = signed_message.into();
let mut pool = MessagePool::new();
pool.add_message(signed_message.clone()).unwrap();
pool.remove_messages(&[signed_message]).unwrap();
assert_eq!(pool.get_messages().len(), 0);
}
}
-26
View File
@@ -1,26 +0,0 @@
//! # Block manager
//!
//! Block manager is quite simple. It keeps pending messages in memory and puts all of them into a block
//! at predefined intervals. That's all it does.
//!
//! If the block actually will be broadcast or not is decided by the application. If not, it will produce next block with
//! the same messages plus the new ones.
//!
//! When application shuts down, pending messages are lost.
//!
//! When a block gets accepted by reliable broadcast then Block Manager will remove all messages included in the block from the
//! pending messages queue.
//!
//! # Synchronization and duplicate messages in sequence of blocks
//!
//! When previous block hasn't been accepted yet, then the next block will contain the same messages as the previous one.
//! One way to solve this is that an application itself keeps track of duplicate messages and discards them if necessary.
//!
//! But it seems a reasonable assumption that in general duplicate messages are unwanted. Therefore, Ephemera solves this
//! by dropping previous blocks which get Finalised/Committed after a new block has been created.
pub(crate) mod builder;
pub(crate) mod manager;
pub(crate) mod message_pool;
pub(crate) mod producer;
pub(crate) mod types;
-92
View File
@@ -1,92 +0,0 @@
use crate::block::{
types::block::{Block, RawBlock, RawBlockHeader},
types::message::EphemeraMessage,
};
use crate::peer::PeerId;
use log::trace;
pub(crate) struct BlockProducer {
pub(crate) peer_id: PeerId,
}
impl BlockProducer {
pub(super) fn new(peer_id: PeerId) -> Self {
Self { peer_id }
}
pub(super) fn create_block(
&mut self,
height: u64,
pending_messages: Vec<EphemeraMessage>,
) -> anyhow::Result<Block> {
trace!("Pending messages for new block: {:?}", pending_messages);
let block = self.new_block(height, pending_messages)?;
Ok(block)
}
fn new_block(&self, height: u64, mut messages: Vec<EphemeraMessage>) -> anyhow::Result<Block> {
//Ordering is fundamental for block hash. Simple sort is fine for now.
messages.sort();
let raw_header = RawBlockHeader::new(self.peer_id, height);
let raw_block = RawBlock::new(raw_header, messages);
//Better idea is probably combine header hash with Merkle tree root hash
let block_hash = raw_block.hash_with_default_hasher()?;
let block = Block::new(raw_block, block_hash);
Ok(block)
}
}
#[cfg(test)]
mod test {
use crate::crypto::{EphemeraKeypair, Keypair};
use crate::ephemera_api::RawApiEphemeraMessage;
use std::cmp::Ordering;
use super::*;
#[test]
fn test_produce_block() {
let peer_id = PeerId::random();
let mut block_producer = BlockProducer::new(peer_id);
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
let signed_message = message
.sign(&Keypair::generate(None))
.expect("Failed to sign message");
let signed_message1: EphemeraMessage = signed_message.into();
let message = RawApiEphemeraMessage::new("test".to_string(), vec![1, 2, 3]);
let signed_message = message
.sign(&Keypair::generate(None))
.expect("Failed to sign message");
let signed_message2: EphemeraMessage = signed_message.into();
let messages = vec![signed_message1.clone(), signed_message2.clone()];
let block = block_producer.create_block(1, messages).unwrap();
assert_eq!(block.header.height, 1);
assert_eq!(block.header.creator, peer_id);
assert_eq!(block.messages.len(), 2);
//Nondeterministic because of timestamp
match signed_message1.cmp(&signed_message2) {
Ordering::Less => {
assert_eq!(block.messages[0], signed_message1);
assert_eq!(block.messages[1], signed_message2);
}
Ordering::Greater => {
assert_eq!(block.messages[0], signed_message2);
assert_eq!(block.messages[1], signed_message1);
}
Ordering::Equal => {
panic!("Messages are equal");
}
}
}
}
-327
View File
@@ -1,327 +0,0 @@
use std::fmt::{Debug, Display};
use serde::{Deserialize, Serialize};
use crate::utilities::merkle::MerkleTree;
use crate::{
block::types::message::EphemeraMessage,
codec::{Decode, Encode},
crypto::Keypair,
peer::PeerId,
utilities::{
codec::{Codec, DecodingError, EncodingError, EphemeraCodec},
crypto::Certificate,
hash::{EphemeraHash, EphemeraHasher},
hash::{Hash, Hasher},
time::EphemeraTime,
},
};
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct BlockHeader {
pub(crate) timestamp: u64,
pub(crate) creator: PeerId,
pub(crate) height: u64,
pub(crate) hash: Hash,
}
impl BlockHeader {
pub(crate) fn new(raw_header: &RawBlockHeader, hash: Hash) -> Self {
Self {
timestamp: raw_header.timestamp,
creator: raw_header.creator,
height: raw_header.height,
hash,
}
}
}
impl Display for BlockHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let hash = &self.hash;
let time = self.timestamp;
let creator = &self.creator;
let height = self.height;
write!(
f,
"hash: {hash}, timestamp: {time}, creator: {creator}, height: {height}",
)
}
}
impl Encode for BlockHeader {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(&self)
}
}
impl Decode for BlockHeader {
type Output = Self;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
Codec::decode(bytes)
}
}
impl EphemeraHash for BlockHeader {
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
let bytes = Codec::encode(&self)?;
state.update(&bytes);
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub(crate) struct RawBlockHeader {
pub(crate) timestamp: u64,
pub(crate) creator: PeerId,
pub(crate) height: u64,
}
impl RawBlockHeader {
pub(crate) fn new(creator: PeerId, height: u64) -> Self {
Self {
timestamp: EphemeraTime::now(),
creator,
height,
}
}
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
let mut hasher = Hasher::default();
self.hash(&mut hasher)?;
let header_hash = hasher.finish().into();
Ok(header_hash)
}
}
impl Display for RawBlockHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let creator = &self.creator;
let height = self.height;
write!(f, "creator: {creator}, height: {height}",)
}
}
impl From<BlockHeader> for RawBlockHeader {
fn from(block_header: BlockHeader) -> Self {
Self {
timestamp: block_header.timestamp,
creator: block_header.creator,
height: block_header.height,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct Block {
pub(crate) header: BlockHeader,
pub(crate) messages: Vec<EphemeraMessage>,
}
impl Block {
pub(crate) fn new(raw_block: RawBlock, block_hash: Hash) -> Self {
let header = BlockHeader::new(&raw_block.header, block_hash);
Self {
header,
messages: raw_block.messages,
}
}
pub(crate) fn get_hash(&self) -> Hash {
self.header.hash
}
pub(crate) fn get_height(&self) -> u64 {
self.header.height
}
pub(crate) fn new_genesis_block(creator: PeerId) -> Self {
let mut block = Self {
header: BlockHeader {
timestamp: EphemeraTime::now(),
creator,
height: 0,
hash: Hash::new([0; 32]),
},
messages: Vec::new(),
};
let hash = block
.hash_with_default_hasher()
.expect("Failed to hash genesis block");
block.header.hash = hash;
block
}
pub(crate) fn sign(&self, keypair: &Keypair) -> anyhow::Result<Certificate> {
let raw_block: RawBlock = self.clone().into();
let certificate = Certificate::prepare(keypair, &raw_block)?;
Ok(certificate)
}
pub(crate) fn verify(&self, certificate: &Certificate) -> anyhow::Result<bool> {
let raw_block: RawBlock = self.clone().into();
certificate.verify(&raw_block)
}
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
let raw_block: RawBlock = self.clone().into();
raw_block.hash_with_default_hasher()
}
pub(crate) fn merkle_tree(&self) -> anyhow::Result<MerkleTree> {
merkle_tree(&self.messages)
}
}
impl Display for Block {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let header = &self.header;
write!(f, "{header}, nr of messages: {}", self.messages.len())
}
}
impl Encode for Block {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(&self)
}
}
impl Decode for Block {
type Output = Block;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
Codec::decode(bytes)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct RawBlock {
pub(crate) header: RawBlockHeader,
pub(crate) messages: Vec<EphemeraMessage>,
}
impl RawBlock {
pub(crate) fn new(header: RawBlockHeader, messages: Vec<EphemeraMessage>) -> Self {
Self { header, messages }
}
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
let header_hash = self.header.hash_with_default_hasher()?;
let merkle_root = merkle_tree(&self.messages)?.root_hash();
let block_hash = Hasher::digest(&[header_hash.inner(), merkle_root.inner()].concat());
Ok(block_hash.into())
}
}
impl From<Block> for RawBlock {
fn from(block: Block) -> Self {
Self {
header: block.header.into(),
messages: block.messages,
}
}
}
impl Encode for RawBlockHeader {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(&self)
}
}
impl Decode for RawBlockHeader {
type Output = RawBlockHeader;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
Codec::decode(bytes)
}
}
impl Encode for RawBlock {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(&self)
}
}
impl Decode for RawBlock {
type Output = RawBlock;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
Codec::decode(bytes)
}
}
impl EphemeraHash for RawBlockHeader {
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
state.update(&self.encode()?);
Ok(())
}
}
impl EphemeraHash for RawBlock {
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
self.header.hash(state)?;
for message in &self.messages {
message.hash(state)?;
}
Ok(())
}
}
pub(crate) fn merkle_tree(messages: &[EphemeraMessage]) -> anyhow::Result<MerkleTree> {
let message_hashes = messages
.iter()
.map(EphemeraMessage::hash_with_default_hasher)
.collect::<anyhow::Result<Vec<Hash>>>()?;
let merkle_tree = MerkleTree::build_tree(&message_hashes);
Ok(merkle_tree)
}
#[cfg(test)]
mod test {
use crate::block::types::message::RawEphemeraMessage;
use crate::crypto::EphemeraKeypair;
use super::*;
#[test]
fn test_block_hash_no_messages() {
let block = Block::new_genesis_block(PeerId::random());
let block_hash = block.hash_with_default_hasher().unwrap();
assert_eq!(block_hash, block.get_hash());
}
#[test]
fn test_block_hash_with_messages() {
let messages = create_ephemera_messages(10);
let message_hashes = messages
.iter()
.map(EphemeraMessage::hash_with_default_hasher)
.collect::<anyhow::Result<Vec<Hash>>>()
.unwrap();
let raw_block = RawBlock::new(RawBlockHeader::new(PeerId::random(), 0), messages);
let block_hash = raw_block.hash_with_default_hasher().unwrap();
let header_hash = raw_block.header.hash_with_default_hasher().unwrap();
let merkle_root = MerkleTree::build_tree(&message_hashes).root_hash();
let expected_block_hash =
Hasher::digest(&[header_hash.inner(), merkle_root.inner()].concat());
assert_eq!(block_hash, expected_block_hash.into());
}
fn create_ephemera_messages(n: usize) -> Vec<EphemeraMessage> {
let keypair = Keypair::generate(None);
let mut messages = Vec::new();
for i in 0..n {
let label = format!("test {i}",);
let message = RawEphemeraMessage::new(label, vec![0; 32]);
let certificate = Certificate::prepare(&keypair, &message).unwrap();
let ephemera_message = EphemeraMessage::new(message, certificate);
messages.push(ephemera_message);
}
messages
}
}
-99
View File
@@ -1,99 +0,0 @@
use serde::{Deserialize, Serialize};
use crate::utilities::codec::{Codec, DecodingError, EncodingError, EphemeraCodec};
use crate::{
codec::{Decode, Encode},
utilities::{
crypto::Certificate,
hash::{EphemeraHash, EphemeraHasher},
hash::{Hash, Hasher},
},
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub(crate) struct EphemeraMessage {
///Timestamp of the message
pub(crate) timestamp: u64,
///Application specific logical identifier of the message
pub(crate) label: String,
///Application specific data
pub(crate) data: Vec<u8>,
///Signature of the raw message
pub(crate) certificate: Certificate,
}
impl EphemeraMessage {
#[cfg(test)]
pub(crate) fn new(raw_message: RawEphemeraMessage, certificate: Certificate) -> Self {
Self {
timestamp: raw_message.timestamp,
label: raw_message.label,
data: raw_message.data,
certificate,
}
}
pub(crate) fn hash_with_default_hasher(&self) -> anyhow::Result<Hash> {
let mut hasher = Hasher::default();
self.hash(&mut hasher)?;
let hash = hasher.finish().into();
Ok(hash)
}
}
impl Encode for EphemeraMessage {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(&self)
}
}
impl Decode for EphemeraMessage {
type Output = Self;
fn decode(bytes: &[u8]) -> Result<Self::Output, DecodingError> {
Codec::decode(bytes)
}
}
impl EphemeraHash for EphemeraMessage {
fn hash<H: EphemeraHasher>(&self, state: &mut H) -> anyhow::Result<()> {
state.update(&self.encode()?);
Ok(())
}
}
/// Raw message represents all the data what will be signed.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub(crate) struct RawEphemeraMessage {
pub(crate) timestamp: u64,
pub(crate) label: String,
pub(crate) data: Vec<u8>,
}
impl RawEphemeraMessage {
#[cfg(test)]
pub(crate) fn new(label: String, data: Vec<u8>) -> Self {
use crate::utilities::time::EphemeraTime;
Self {
timestamp: EphemeraTime::now(),
label,
data,
}
}
}
impl From<EphemeraMessage> for RawEphemeraMessage {
fn from(message: EphemeraMessage) -> Self {
Self {
timestamp: message.timestamp,
label: message.label,
data: message.data,
}
}
}
impl Encode for RawEphemeraMessage {
fn encode(&self) -> Result<Vec<u8>, EncodingError> {
Codec::encode(&self)
}
}
-2
View File
@@ -1,2 +0,0 @@
pub(crate) mod block;
pub(crate) mod message;
-320
View File
@@ -1,320 +0,0 @@
use std::num::NonZeroUsize;
use log::{debug, trace};
use lru::LruCache;
use crate::broadcast::bracha::quorum::BrachaMessageType;
use crate::peer::PeerId;
use crate::{
block::types::block::Block,
broadcast::{
bracha::quorum::Quorum,
MessageType::{Echo, Vote},
ProtocolContext, RawRbMsg,
},
utilities::hash::Hash,
};
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub(crate) enum BroadcastResponse {
Broadcast(RawRbMsg),
Deliver(Hash),
Drop(Hash),
}
pub(crate) struct Broadcaster {
/// Local peer id
local_peer_id: PeerId,
/// We keep a context for each block we are processing.
contexts: LruCache<Hash, ProtocolContext>,
/// Current cluster size
cluster_size: usize,
}
impl Broadcaster {
pub fn new(peer_id: PeerId) -> Broadcaster {
Broadcaster {
//At any given time we are processing in parallel about n messages, where n is the number of peers in the group.
//This is just large enough buffer.
contexts: LruCache::new(NonZeroUsize::new(1000).unwrap()),
cluster_size: 0,
local_peer_id: peer_id,
}
}
pub(crate) fn new_broadcast(&mut self, block: Block) -> anyhow::Result<BroadcastResponse> {
debug!("Starting broadcast for new block {:?}", block.get_hash());
self.handle(&RawRbMsg::new(block, self.local_peer_id))
}
pub(crate) fn handle(&mut self, rb_msg: &RawRbMsg) -> anyhow::Result<BroadcastResponse> {
trace!("Processing new broadcast message: {:?}", rb_msg);
let block = rb_msg.block();
let hash = block.hash_with_default_hasher()?;
let ctx = self.contexts.get_or_insert(hash, || {
ProtocolContext::new(hash, self.local_peer_id, Quorum::new(self.cluster_size))
});
if ctx.delivered {
trace!("Block {hash:?} already delivered");
return Ok(BroadcastResponse::Drop(hash));
}
match rb_msg.message_type {
Echo(_) => {
trace!("Processing ECHO {:?}", rb_msg.id);
Ok(self.process_echo(rb_msg, hash))
}
Vote(_) => {
trace!("Processing VOTE {:?}", rb_msg.id);
Ok(self.process_vote(rb_msg, hash))
}
}
}
fn process_echo(&mut self, rb_msg: &RawRbMsg, hash: Hash) -> BroadcastResponse {
let ctx = self.contexts.get_mut(&hash).expect("Context not found");
if self.local_peer_id != rb_msg.original_sender {
trace!("Adding echo from {:?}", rb_msg.original_sender);
ctx.add_echo(rb_msg.original_sender);
}
if !ctx.echoed() {
ctx.add_echo(self.local_peer_id);
trace!("Sending echo reply for {hash:?}",);
return BroadcastResponse::Broadcast(
rb_msg.echo_reply(self.local_peer_id, rb_msg.block()),
);
}
if !ctx.voted()
&& ctx
.quorum
.check_threshold(ctx, BrachaMessageType::Echo)
.is_vote()
{
ctx.add_vote(self.local_peer_id);
trace!("Sending vote reply for {hash:?}",);
return BroadcastResponse::Broadcast(
rb_msg.vote_reply(self.local_peer_id, rb_msg.block()),
);
}
BroadcastResponse::Drop(hash)
}
fn process_vote(&mut self, rb_msg: &RawRbMsg, hash: Hash) -> BroadcastResponse {
let block = rb_msg.block();
let ctx = self.contexts.get_mut(&hash).expect("Context not found");
if self.local_peer_id != rb_msg.original_sender {
trace!("Adding vote from {:?}", rb_msg.original_sender);
ctx.add_vote(rb_msg.original_sender);
}
if ctx
.quorum
.check_threshold(ctx, BrachaMessageType::Vote)
.is_vote()
{
ctx.add_vote(self.local_peer_id);
trace!("Sending vote reply for {hash:?}",);
return BroadcastResponse::Broadcast(rb_msg.vote_reply(self.local_peer_id, block));
}
if ctx
.quorum
.check_threshold(ctx, BrachaMessageType::Vote)
.is_deliver()
{
trace!("Commit complete for {:?}", rb_msg.id);
ctx.delivered = true;
return BroadcastResponse::Deliver(hash);
}
BroadcastResponse::Drop(hash)
}
pub(crate) fn group_updated(&mut self, size: usize) {
self.cluster_size = size;
}
}
#[cfg(test)]
mod tests {
//1.make sure before voting enough echo messages are received
//2.make sure before delivering enough vote messages are received
//a)Either f + 1
//b)Or n - f
//3.make sure that duplicate messages doesn't have impact
//4. "Ideally" make sure that when group changes, the ongoing broadcast can deal with it
use std::iter;
use assert_matches::assert_matches;
use crate::broadcast::bracha::broadcast::BroadcastResponse;
use crate::peer::PeerId;
use crate::utilities::hash::Hash;
use crate::{
block::types::block::{Block, RawBlock, RawBlockHeader},
broadcast::{self, bracha::broadcast::Broadcaster, RawRbMsg},
};
#[test]
fn test_state_transitions_from_start_to_end() {
let peers: Vec<PeerId> = iter::repeat_with(PeerId::random).take(10).collect();
let local_peer_id = peers[0];
let block_creator_peer_id = peers[1];
let mut broadcaster = Broadcaster::new(local_peer_id);
broadcaster.group_updated(peers.len());
let (block_hash, block) = create_block(block_creator_peer_id);
//After this echo set contains local and block creator(msg sender)
receive_echo_first_message(&mut broadcaster, &block, block_creator_peer_id);
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
assert_eq!(ctx.echo.len(), 2);
assert!(ctx.echoed());
assert!(!ctx.voted());
receive_nr_of_echo_messages_below_vote_threshold(&mut broadcaster, &block, &peers[2..6]);
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
assert_eq!(ctx.echo.len(), 6);
assert!(ctx.echoed());
assert!(!ctx.voted());
receive_echo_threshold_message(&mut broadcaster, &block, *peers.get(7).unwrap());
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
assert_eq!(ctx.echo.len(), 7);
assert_eq!(ctx.vote.len(), 1);
assert!(ctx.echoed());
assert!(ctx.voted());
receive_nr_of_vote_messages_below_deliver_threshold(&mut broadcaster, &block, &peers[2..7]);
let ctx = broadcaster.contexts.get(&block_hash).unwrap();
assert_eq!(ctx.echo.len(), 7);
assert_eq!(ctx.vote.len(), 6);
assert!(ctx.echoed());
assert!(ctx.voted());
receive_threshold_vote_message_for_deliver(
&mut broadcaster,
&block,
*peers.get(8).unwrap(),
);
}
fn receive_threshold_vote_message_for_deliver(
broadcaster: &mut Broadcaster,
block: &Block,
peer_id: PeerId,
) {
let rb_msg = RawRbMsg::new(block.clone(), PeerId::random());
let rb_msg = rb_msg.vote_reply(peer_id, block.clone());
let response = handle_double(broadcaster, &rb_msg);
assert_matches!(response, BroadcastResponse::Deliver(_));
}
fn receive_nr_of_echo_messages_below_vote_threshold(
broadcaster: &mut Broadcaster,
block: &Block,
peers: &[PeerId],
) {
for peer_id in peers {
let rb_msg = RawRbMsg::new(block.clone(), *peer_id);
let response = handle_double(broadcaster, &rb_msg);
assert_matches!(response, BroadcastResponse::Drop(_));
}
}
fn receive_nr_of_vote_messages_below_deliver_threshold(
broadcaster: &mut Broadcaster,
block: &Block,
peers: &[PeerId],
) {
for peer_id in peers {
let rb_msg = RawRbMsg::new(block.clone(), PeerId::random());
let rb_msg = rb_msg.vote_reply(*peer_id, block.clone());
let response = handle_double(broadcaster, &rb_msg);
assert_matches!(response, BroadcastResponse::Drop(_));
}
}
fn receive_echo_first_message(
broadcaster: &mut Broadcaster,
block: &Block,
block_creator: PeerId,
) {
let rb_msg = RawRbMsg::new(block.clone(), block_creator);
let response = handle_double(broadcaster, &rb_msg);
assert_matches!(
response,
BroadcastResponse::Broadcast(RawRbMsg {
id: _,
request_id: _,
original_sender: _,
timestamp: _,
message_type: broadcast::MessageType::Echo(_),
})
);
}
fn receive_echo_threshold_message(
broadcaster: &mut Broadcaster,
block: &Block,
peer_id: PeerId,
) {
let rb_msg = RawRbMsg::new(block.clone(), peer_id);
let response = handle_double(broadcaster, &rb_msg);
assert_matches!(
response,
BroadcastResponse::Broadcast(RawRbMsg {
id: _,
request_id: _,
original_sender: _,
timestamp: _,
message_type: broadcast::MessageType::Vote(_),
})
);
}
fn create_block(block_creator_peer_id: PeerId) -> (Hash, Block) {
let header = RawBlockHeader::new(block_creator_peer_id, 0);
let raw_block = RawBlock::new(header, vec![]);
let block_hash = raw_block.hash_with_default_hasher().unwrap();
let block = Block::new(raw_block, block_hash);
(block_hash, block)
}
//make sure that duplicate messages doesn't have impact
fn handle_double(broadcaster: &mut Broadcaster, rb_msg: &RawRbMsg) -> BroadcastResponse {
let response = broadcaster.handle(rb_msg).unwrap();
broadcaster.handle(rb_msg).unwrap();
response
}
}
-2
View File
@@ -1,2 +0,0 @@
pub(crate) mod broadcast;
pub(crate) mod quorum;
-254
View File
@@ -1,254 +0,0 @@
use log::trace;
use crate::broadcast::{MessageType, ProtocolContext};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BrachaMessageType {
Echo,
Vote,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BrachaAction {
Vote,
Deliver,
Ignore,
}
impl BrachaAction {
pub(crate) fn is_vote(self) -> bool {
matches!(self, BrachaAction::Vote)
}
pub(crate) fn is_deliver(self) -> bool {
matches!(self, BrachaAction::Deliver)
}
}
impl From<MessageType> for BrachaMessageType {
fn from(message_type: MessageType) -> Self {
match message_type {
MessageType::Echo(_) => BrachaMessageType::Echo,
MessageType::Vote(_) => BrachaMessageType::Vote,
}
}
}
const MAX_FAULTY_RATIO: f64 = 1.0 / 3.0;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Quorum {
pub(crate) cluster_size: usize,
pub(crate) max_faulty_nodes: usize,
}
impl Quorum {
pub fn new(cluster_size: usize) -> Self {
let max_faulty_nodes = Quorum::max_faulty_nodes(cluster_size);
Self {
cluster_size,
max_faulty_nodes,
}
}
pub(crate) fn check_threshold(
&self,
ctx: &ProtocolContext,
phase: BrachaMessageType,
) -> BrachaAction {
if self.cluster_size == 0 {
return BrachaAction::Ignore;
}
match phase {
BrachaMessageType::Echo => {
if ctx.echo.len() >= self.cluster_size - self.max_faulty_nodes {
trace!(
"Echo threshold reached: Echoed:{} / Threshold:{} for Block:{}",
ctx.echo.len(),
self.cluster_size - self.max_faulty_nodes,
ctx.hash
);
BrachaAction::Vote
} else {
trace!(
"Echo threshold not reached: Echoed:{} / Threshold:{} for Block:{}",
ctx.echo.len(),
self.cluster_size - self.max_faulty_nodes,
ctx.hash
);
BrachaAction::Ignore
}
}
BrachaMessageType::Vote => {
if !ctx.voted() {
// f + 1 votes are enough to send our vote
if ctx.vote.len() >= self.max_faulty_nodes {
trace!(
"Vote send threshold reached: Voted:{} / Threshold:{} for Block:{}",
ctx.vote.len(),
self.max_faulty_nodes + 1,
ctx.hash
);
return BrachaAction::Vote;
}
}
if ctx.voted() {
// n-f votes are enough to deliver the value
if ctx.vote.len() >= self.cluster_size - self.max_faulty_nodes {
trace!(
"Deliver threshold reached: Voted:{} / Threshold:{} for Block:{}",
ctx.vote.len(),
self.cluster_size - self.max_faulty_nodes,
ctx.hash
);
return BrachaAction::Deliver;
}
}
trace!(
"Vote threshold not reached: Voted:{} / Threshold:{} for Block:{}",
ctx.vote.len(),
self.max_faulty_nodes + 1,
ctx.hash
);
BrachaAction::Ignore
}
}
}
pub(crate) fn cluster_size_info(cluster_size: usize) -> String {
let max_faulty_nodes = Quorum::max_faulty_nodes(cluster_size);
format!("Cluster size: {cluster_size} / Max faulty nodes: {max_faulty_nodes}",)
}
#[allow(
clippy::cast_precision_loss,
clippy::cast_sign_loss,
clippy::cast_possible_truncation
)]
fn max_faulty_nodes(cluster_size: usize) -> usize {
(cluster_size as f64 * MAX_FAULTY_RATIO).floor() as usize
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use crate::broadcast::{
bracha::quorum::{BrachaAction, BrachaMessageType, Quorum},
ProtocolContext,
};
use crate::peer::PeerId;
#[test]
fn test_max_faulty_nodes() {
let quorum = Quorum::new(10);
assert_eq!(quorum.max_faulty_nodes, 3);
}
#[test]
fn test_vote_threshold_from_n_minus_f_peers() {
let quorum = Quorum::new(10);
let ctx = ctx_with_nr_echoes(0);
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Echo),
BrachaAction::Ignore
);
let ctx = ctx_with_nr_echoes(3);
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Echo),
BrachaAction::Ignore
);
let ctx = ctx_with_nr_echoes(8);
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Echo),
BrachaAction::Vote
);
}
#[test]
fn test_vote_threshold_from_f_plus_one_peers() {
let quorum = Quorum::new(10);
let ctx = ctx_with_nr_votes(0, None);
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
BrachaAction::Ignore
);
let ctx = ctx_with_nr_votes(2, None);
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
BrachaAction::Ignore
);
let ctx = ctx_with_nr_votes(5, None);
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
BrachaAction::Vote
);
}
#[test]
fn test_deliver_threshold_from_n_minus_f_peers() {
let quorum = Quorum::new(10);
let local_peer_id = PeerId::random();
let ctx = ctx_with_nr_votes(0, local_peer_id.into());
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
BrachaAction::Ignore
);
let ctx = ctx_with_nr_votes(3, local_peer_id.into());
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
BrachaAction::Ignore
);
let ctx = ctx_with_nr_votes(7, local_peer_id.into());
assert_eq!(
quorum.check_threshold(&ctx, BrachaMessageType::Vote),
BrachaAction::Deliver
);
}
fn ctx_with_nr_echoes(n: usize) -> ProtocolContext {
let mut ctx = ProtocolContext {
local_peer_id: PeerId::random(),
hash: [0; 32].into(),
echo: HashSet::default(),
vote: HashSet::default(),
quorum: Quorum::new(10),
delivered: false,
};
for _ in 0..n {
ctx.echo.insert(PeerId::random());
}
ctx
}
fn ctx_with_nr_votes(n: usize, local_peer_id: Option<PeerId>) -> ProtocolContext {
let mut ctx = ProtocolContext {
local_peer_id: local_peer_id.unwrap_or(PeerId::random()),
hash: [0; 32].into(),
echo: HashSet::default(),
vote: HashSet::default(),
quorum: Quorum::new(10),
delivered: false,
};
for _ in 0..n {
ctx.vote.insert(PeerId::random());
}
if let Some(id) = local_peer_id {
ctx.vote.insert(id);
}
ctx
}
}
-249
View File
@@ -1,249 +0,0 @@
use std::collections::HashSet;
use std::num::NonZeroUsize;
use log::warn;
use lru::LruCache;
use crate::peer::PeerId;
use crate::utilities::hash::Hash;
pub(crate) struct BroadcastGroup {
/// The id of current group. Incremented every time a new snapshot is added.
pub(crate) current_id: u64,
/// A cache of the group snapshots.
pub(crate) snapshots: LruCache<u64, HashSet<PeerId>>,
/// A cache of the groups for each block.
pub(crate) broadcast_groups: LruCache<Hash, u64>,
}
impl BroadcastGroup {
pub(crate) fn new() -> BroadcastGroup {
let mut snapshots = LruCache::new(NonZeroUsize::new(100).unwrap());
snapshots.put(0, HashSet::new());
BroadcastGroup {
current_id: 0,
snapshots,
broadcast_groups: LruCache::new(NonZeroUsize::new(100).unwrap()),
}
}
pub(crate) fn add_snapshot(&mut self, snapshot: HashSet<PeerId>) {
self.current_id += 1;
self.snapshots.put(self.current_id, snapshot);
}
pub(crate) fn is_member(&mut self, id: u64, peer_id: &PeerId) -> bool {
self.snapshots
.get(&id)
.map_or(false, |s| s.contains(peer_id))
}
pub(crate) fn is_empty(&mut self) -> bool {
self.snapshots
.get(&self.current_id)
.map_or(true, HashSet::is_empty)
}
// Returns empty snapshots(inserted in 'new' fn) if we haven't received any yet.
pub(crate) fn current(&mut self) -> &HashSet<PeerId> {
self.snapshots
.get(&self.current_id)
.expect("Current group should always exist")
}
// Checks if creator and sender are part of the expected group.
// If we see hash first time, it checks against the current group. And if check passes, it
// associates the hash with the current group.
pub(crate) fn check_membership(
&mut self,
hash: Hash,
block_creator: &PeerId,
message_sender: &PeerId,
) -> bool {
//We see this block first time
if !self.broadcast_groups.contains(&hash) {
//This can happen at startup for example when node is not ready yet(caught up with the network)
if self.is_empty() {
warn!(
"Received new block {:?} but current group is empty, rejecting the block",
hash
);
return false;
}
}
//Make sure that the sender peer_id and block peer_id are part of the block initial group
//1. If the block is new, the group is the current one
//2. If the block is old, the group is the one that was used when the block was created
//It's needed to make sure that
//1. The peer is authenticated(part of the network)
//2. Block processing is consistent regarding the group across rounds
let membership_id = *self.broadcast_groups.get(&hash).unwrap_or(&self.current_id);
//Node is excluded from group for some reason(for example health checks failed)
if !self.is_member(membership_id, message_sender) {
warn!(
"Received new block {} but sender {} is not part of the current group",
hash, message_sender
);
return false;
}
//Node is excluded from group for some reason(for example health checks failed)
if !self.is_member(membership_id, block_creator) {
warn!(
"Received new block {} but sender {} is not part of the current group",
hash, message_sender
);
return false;
}
self.broadcast_groups.put(hash, membership_id);
true
}
pub(crate) fn get_group_by_block_hash(&mut self, hash: Hash) -> Option<&HashSet<PeerId>> {
let membership_id = *self.broadcast_groups.get(&hash)?;
self.snapshots.get(&membership_id)
}
}
#[cfg(test)]
mod test {
use std::collections::HashSet;
use crate::broadcast::group::BroadcastGroup;
use crate::peer::PeerId;
use crate::utilities::hash::Hash;
#[test]
fn test_no_snapshot() {
let group = BroadcastGroup::new();
assert_eq!(group.current_id, 0);
//Including initial default snapshot
assert_eq!(group.snapshots.len(), 1);
}
#[test]
fn test_multiple_snapshots() {
let (mut group, snapshots) = group_with_snapshots(10);
assert_eq!(group.current_id, 10);
//Including initial default snapshot
assert_eq!(group.snapshots.len(), 11);
for (i, sn) in snapshots
.iter()
.enumerate()
.map(|(i, sn)| ((i + 1) as u64, sn))
{
let gsn = group.snapshots.get(&i).unwrap();
assert_eq!(sn, gsn);
}
}
#[test]
fn check_membership_empty_group() {
let mut group = BroadcastGroup::new();
let hash = Hash::new([0; 32]);
assert!(!group.check_membership(hash, &PeerId::random(), &PeerId::random()));
assert!(!group.broadcast_groups.contains(&hash));
}
#[test]
fn check_membership_creator_nor_sender_not_member() {
let (mut group, _snapshots) = group_with_snapshots(1);
assert!(!group.check_membership(Hash::new([0; 32]), &PeerId::random(), &PeerId::random()));
assert!(!group.broadcast_groups.contains(&Hash::new([0; 32])));
}
#[test]
fn check_membership_creator_not_member() {
let (mut group, snapshots) = group_with_snapshots(1);
let sender = snapshots[0].clone().into_iter().next().unwrap();
let hash = Hash::new([0; 32]);
assert!(!group.check_membership(hash, &PeerId::random(), &sender));
assert!(!group.broadcast_groups.contains(&hash));
}
#[test]
fn check_membership_sender_not_member() {
let (mut group, snapshots) = group_with_snapshots(1);
let creator = snapshots[0].clone().into_iter().next().unwrap();
let hash = Hash::new([0; 32]);
assert!(!group.check_membership(hash, &creator, &PeerId::random()));
assert!(!group.broadcast_groups.contains(&hash));
}
#[test]
fn check_snapshot_membership_both_are_members() {
let (mut group, snapshots) = group_with_snapshots(1);
let creator = snapshots[0].clone().into_iter().next().unwrap();
let sender = creator;
let hash = Hash::new([0; 32]);
assert!(group.check_membership(hash, &creator, &sender));
assert!(group.broadcast_groups.contains(&hash));
}
#[test]
fn check_snapshot_membership_of_current_snapshot() {
let (mut group, snapshots) = group_with_snapshots(2);
let current_snapshot = snapshots[1].clone();
let creator = current_snapshot.into_iter().next().unwrap();
let sender = creator;
let hash = Hash::new([0; 32]);
assert!(group.check_membership(hash, &creator, &sender));
assert!(group.broadcast_groups.contains(&hash));
//Remove the current snapshot
group.snapshots.pop(&group.current_id);
//Membership should fail
assert!(!group.check_membership(hash, &creator, &sender));
}
#[test]
fn check_snapshot_membership_of_previous_snapshot() {
let mut group = BroadcastGroup::new();
let first_snapshot = create_snapshot();
group.add_snapshot(first_snapshot.clone());
let creator = first_snapshot.into_iter().next().unwrap();
let sender = creator;
let hash = Hash::new([0; 32]);
assert!(group.check_membership(hash, &creator, &sender));
assert!(group.broadcast_groups.contains(&hash));
//Add second snapshot
group.add_snapshot(create_snapshot());
//Remove the current snapshot
group.snapshots.pop(&group.current_id);
//Membership should still pass
assert!(group.broadcast_groups.contains(&hash));
assert!(group.check_membership(hash, &creator, &sender));
}
fn group_with_snapshots(count: usize) -> (BroadcastGroup, Vec<HashSet<PeerId>>) {
let mut group = BroadcastGroup::new();
let mut snapshots = Vec::new();
for _ in 0..count {
let snapshot = create_snapshot();
snapshots.push(snapshot.clone());
group.add_snapshot(snapshot);
}
(group, snapshots)
}
fn create_snapshot() -> HashSet<PeerId> {
let mut snapshot = HashSet::new();
let peer_id = PeerId::random();
snapshot.insert(peer_id);
snapshot
}
}
-191
View File
@@ -1,191 +0,0 @@
//! Simple reliable broadcast protocol(Bracha) implementation
//!
use std::collections::HashSet;
use std::fmt::{Debug, Display};
use serde_derive::{Deserialize, Serialize};
use crate::broadcast::bracha::quorum::Quorum;
use crate::{
block::types::block::Block,
peer::PeerId,
utilities::{
crypto::Certificate,
hash::Hash,
id::{EphemeraId, EphemeraIdentifier},
time::EphemeraTime,
},
};
pub(crate) mod bracha;
pub(crate) mod group;
pub(crate) mod signing;
/// Context keeps the broadcast state for a block
#[derive(Debug, Clone)]
pub(crate) struct ProtocolContext {
pub(crate) local_peer_id: PeerId,
/// Block hash
pub(crate) hash: Hash,
/// Peers that sent prepare message(this peer included)
pub(crate) echo: HashSet<PeerId>,
/// Peers that sent commit message(this peer included)
pub(crate) vote: HashSet<PeerId>,
/// Quorum logic for Bracha protocol
pub(crate) quorum: Quorum,
/// Flag indicating if the message was delivered to the client
pub(crate) delivered: bool,
}
impl ProtocolContext {
pub(crate) fn new(hash: Hash, local_peer_id: PeerId, quorum: Quorum) -> ProtocolContext {
ProtocolContext {
local_peer_id,
hash,
echo: HashSet::new(),
vote: HashSet::new(),
quorum,
delivered: false,
}
}
fn add_echo(&mut self, peer: PeerId) {
self.echo.insert(peer);
}
fn add_vote(&mut self, peer: PeerId) {
self.vote.insert(peer);
}
fn echoed(&self) -> bool {
self.echo.contains(&self.local_peer_id)
}
fn voted(&self) -> bool {
self.vote.contains(&self.local_peer_id)
}
}
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct RbMsg {
///Unique id of the message which stays the same throughout the protocol
pub(crate) id: EphemeraId,
///Distinct id of the message which changes when the message is rebroadcast
pub(crate) request_id: EphemeraId,
///Id of the peer that CREATED the message(not necessarily the one that sent it, with gossip it can come through a different peer)
pub(crate) original_sender: PeerId,
///When the message was created by the sender.
pub(crate) timestamp: u64,
///Current phase of the protocol(Echo, Vote)
pub(crate) phase: MessageType,
///Signature of the message
pub(crate) certificate: Certificate,
}
impl RbMsg {
pub(crate) fn new(raw: RawRbMsg, signature: Certificate) -> RbMsg {
RbMsg {
id: raw.id,
request_id: raw.request_id,
original_sender: raw.original_sender,
timestamp: raw.timestamp,
phase: raw.message_type,
certificate: signature,
}
}
pub(crate) fn block(&self) -> &Block {
match &self.phase {
MessageType::Echo(block) | MessageType::Vote(block) => block,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) struct RawRbMsg {
pub(crate) id: EphemeraId,
pub(crate) request_id: EphemeraId,
pub(crate) original_sender: PeerId,
pub(crate) timestamp: u64,
pub(crate) message_type: MessageType,
}
impl RawRbMsg {
pub(crate) fn new(block: Block, original_sender: PeerId) -> RawRbMsg {
RawRbMsg {
id: EphemeraId::generate(),
request_id: EphemeraId::generate(),
original_sender,
timestamp: EphemeraTime::now(),
message_type: MessageType::Echo(block),
}
}
pub(crate) fn block(&self) -> Block {
match &self.message_type {
MessageType::Echo(block) | MessageType::Vote(block) => block.clone(),
}
}
pub(crate) fn reply(&self, local_id: PeerId, phase: MessageType) -> Self {
RawRbMsg {
id: self.id.clone(),
request_id: EphemeraId::generate(),
original_sender: local_id,
timestamp: EphemeraTime::now(),
message_type: phase,
}
}
pub(crate) fn echo_reply(&self, local_id: PeerId, data: Block) -> Self {
self.reply(local_id, MessageType::Echo(data))
}
pub(crate) fn vote_reply(&self, local_id: PeerId, data: Block) -> Self {
self.reply(local_id, MessageType::Vote(data))
}
}
impl From<RbMsg> for RawRbMsg {
fn from(msg: RbMsg) -> Self {
RawRbMsg {
id: msg.id,
request_id: msg.request_id,
original_sender: msg.original_sender,
timestamp: msg.timestamp,
message_type: msg.phase,
}
}
}
impl Display for RbMsg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[id: {}, peer: {}, block: {}, phase: {:?}]",
self.id,
self.original_sender,
self.block().get_hash(),
self.phase
)
}
}
impl Debug for RbMsg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[id: {}, peer: {}, block: {}, phase: {:?}]",
self.id,
self.original_sender,
self.block().get_hash(),
self.phase
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub(crate) enum MessageType {
Echo(Block),
Vote(Block),
}
-150
View File
@@ -1,150 +0,0 @@
use std::collections::HashSet;
use std::num::NonZeroUsize;
use std::sync::Arc;
use log::trace;
use lru::LruCache;
use crate::{
block::types::block::{Block, RawBlock},
crypto::Keypair,
utilities::{codec::Encode, crypto::Certificate, crypto::EphemeraPublicKey, hash::Hash},
};
pub(crate) struct BlockSigner {
/// All signatures of the last blocks that we received from the network(+ our own)
verified_signatures: LruCache<Hash, HashSet<Certificate>>,
/// Our own keypair
signing_keypair: Arc<Keypair>,
}
impl BlockSigner {
pub fn new(keypair: Arc<Keypair>) -> Self {
Self {
verified_signatures: LruCache::new(NonZeroUsize::new(1000).unwrap()),
signing_keypair: keypair,
}
}
pub(crate) fn get_block_certificates(
&mut self,
block_id: &Hash,
) -> Option<&HashSet<Certificate>> {
self.verified_signatures.get(block_id)
}
pub(crate) fn sign_block(&mut self, block: &Block, hash: &Hash) -> anyhow::Result<Certificate> {
trace!("Signing block: {:?}", block);
let certificate = block.sign(self.signing_keypair.as_ref())?;
self.add_certificate(hash, certificate.clone());
Ok(certificate)
}
/// This verification is part of reliable broadcast and verifies only the
/// signature of the sender.
pub(crate) fn verify_block(
&mut self,
block: &Block,
certificate: &Certificate,
) -> anyhow::Result<()> {
trace!("Verifying block: {block:?} against certificate {certificate:?}");
let raw_block: RawBlock = (*block).clone().into();
let raw_block = raw_block.encode()?;
if certificate
.public_key
.verify(&raw_block, &certificate.signature)
{
self.add_certificate(&block.header.hash, certificate.clone());
Ok(())
} else {
anyhow::bail!("Invalid block certificate");
}
}
fn add_certificate(&mut self, hash: &Hash, certificate: Certificate) {
trace!("Adding certificate to block: {}", hash);
self.verified_signatures
.get_or_insert_mut(*hash, HashSet::new)
.insert(certificate);
}
}
#[cfg(test)]
mod test {
use crate::block::types::block::RawBlockHeader;
use crate::block::types::message::{EphemeraMessage, RawEphemeraMessage};
use crate::crypto::EphemeraKeypair;
use crate::peer::ToPeerId;
use super::*;
#[test]
fn test_sign_verify_block_ok() {
let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None)));
let message_signing_keypair = Keypair::generate(None);
let block = new_block(&message_signing_keypair, "label1");
let hash = block.hash_with_default_hasher().unwrap();
let certificate = signer.sign_block(&block, &hash).unwrap();
assert!(signer.verify_block(&block, &certificate).is_ok());
}
#[test]
fn test_sign_signatures_cached_correctly() {
let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None)));
let block = new_block(&Keypair::generate(None), "label1");
let hash = block.hash_with_default_hasher().unwrap();
//Signed by node 1
let certificate1 = block.sign(&Keypair::generate(None)).unwrap();
signer.verify_block(&block, &certificate1).unwrap();
//Signed by node 2
let certificate2 = block.sign(&Keypair::generate(None)).unwrap();
signer.verify_block(&block, &certificate2).unwrap();
let block_certificates = signer.get_block_certificates(&hash).unwrap();
assert_eq!(block_certificates.len(), 2);
}
#[test]
fn test_sign_verify_block_fail() {
let mut signer = BlockSigner::new(Arc::new(Keypair::generate(None)));
let message_signing_keypair = Keypair::generate(None);
let block = new_block(&message_signing_keypair, "label1");
let certificate = block.sign(&message_signing_keypair).unwrap();
let modified_block = new_block(&message_signing_keypair, "label2");
assert!(signer.verify_block(&modified_block, &certificate).is_err());
}
fn new_block(keypair: &Keypair, message_label: &str) -> Block {
let peer_id = keypair.public_key().peer_id();
let raw_ephemera_message =
RawEphemeraMessage::new(message_label.to_string(), "payload".as_bytes().to_vec());
let message_certificate = Certificate::prepare(keypair, &raw_ephemera_message).unwrap();
let messages = vec![EphemeraMessage::new(
raw_ephemera_message,
message_certificate,
)];
let raw_block_header = RawBlockHeader::new(peer_id, 0);
let raw_block = RawBlock::new(raw_block_header, messages);
let block_hash = raw_block
.hash_with_default_hasher()
.expect("Hashing failed");
Block::new(raw_block, block_hash)
}
}

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