Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 34b8a0706d | |||
| 275417c186 | |||
| 40a92e07d6 | |||
| e83a12bd91 | |||
| 381162554e | |||
| 7a740c06fd | |||
| e65285ac7b | |||
| 715a3bd687 | |||
| 858f1ac13c | |||
| 3ee1328626 | |||
| 36ac825b43 | |||
| 165f189115 | |||
| b5fcfbe2fe | |||
| 7a38f1f469 | |||
| 5faca46235 | |||
| d780ac55b1 | |||
| c5f7d066b0 | |||
| 8cc90be8c6 | |||
| aae96e7537 | |||
| 39b521bc1f | |||
| 7339695ce8 | |||
| 0e1c9853aa | |||
| 76b9c669d7 | |||
| 553cfd098b | |||
| edeb8369df | |||
| 22b2405aa2 | |||
| 63254ecffe | |||
| 407d280019 | |||
| 1fafc126fb | |||
| 9a51135d22 | |||
| 1b2790da80 | |||
| f8943eebce | |||
| d7b53cba40 | |||
| 89d2f0ac12 | |||
| 110b4d384e | |||
| 4631c72c6b | |||
| 12aa5f1f4f | |||
| 825f25800a | |||
| d9b4d8fde6 | |||
| a98613d83c | |||
| 40e1243f3c | |||
| 50d2ca0a12 | |||
| 34de42fe7a | |||
| 6e2eaf29e7 | |||
| 32d9baaf02 | |||
| c6b193eb4f | |||
| 0179f7648c | |||
| 45c04d63e2 | |||
| 92b9edf0da | |||
| 0f8ac1506b | |||
| efdf27d1e9 |
@@ -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
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Generated
-12
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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(
|
||||
|
||||
Generated
+214
-33
@@ -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"
|
||||
|
||||
@@ -3,7 +3,6 @@ members = [
|
||||
"coconut-bandwidth",
|
||||
"coconut-dkg",
|
||||
"coconut-test",
|
||||
"ephemera",
|
||||
"mixnet",
|
||||
"mixnet-vesting-integration-tests",
|
||||
"multisig/cw3-flex-multisig",
|
||||
|
||||
@@ -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"
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
---
|
||||
|
||||
+116
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
})?
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub(crate) mod block;
|
||||
pub(crate) mod message;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub(crate) mod broadcast;
|
||||
pub(crate) mod quorum;
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user