Compare commits

..

23 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 5ea67a9376 missing test fix 2024-01-17 14:10:08 +00:00
Jędrzej Stuczyński f566dffc5b fixed tests 2024-01-17 14:10:08 +00:00
Jędrzej Stuczyński 05f8beedad api support: submit ed25519 public key alongside the bte public key 2024-01-17 14:10:07 +00:00
Jędrzej Stuczyński 2fff051e28 submit ed25519 public key alongside the bte public key 2024-01-17 14:10:07 +00:00
Jędrzej Stuczyński 44bd70c546 reusing already generated dealings 2024-01-17 14:07:03 +00:00
Jędrzej Stuczyński 702354d127 client support 2024-01-17 14:07:02 +00:00
Jędrzej Stuczyński 56b1010d16 schema 2024-01-17 14:05:53 +00:00
Jędrzej Stuczyński 0632517f5d contract query for dealing status 2024-01-17 14:05:53 +00:00
Jędrzej Stuczyński bfcc5e9b41 fixed dealings query arguments 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński 337aacd442 more clippy 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński da5b7302b5 updated dkg schema 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński 7a53e86b40 clippy 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński 654dd07d19 removed old debug code 2024-01-17 13:59:06 +00:00
Jędrzej Stuczyński ef0765face ephemera contract fix 2024-01-17 13:59:05 +00:00
Jędrzej Stuczyński 3685b4681c fixes 2024-01-17 13:59:05 +00:00
Jędrzej Stuczyński 47e2af2caa reintroducing bug in deterministic_filter_dealers to make tests pass
yes, it's as bad as it sounds
2024-01-17 13:59:03 +00:00
Jędrzej Stuczyński 5be555d79f ability to query for dkg contract state 2024-01-17 13:58:26 +00:00
Jędrzej Stuczyński 8cc2b3167e client support 2024-01-17 13:56:33 +00:00
Jędrzej Stuczyński 4d95955961 renaming 2024-01-17 13:50:23 +00:00
Jędrzej Stuczyński e36ae4091f removed todos from commented tests 2024-01-17 13:50:23 +00:00
Jędrzej Stuczyński b566147f2f storage and query tests 2024-01-17 13:50:22 +00:00
Jędrzej Stuczyński 12242bb3c6 updated dealings queries 2024-01-17 13:50:22 +00:00
Jędrzej Stuczyński 0a0b0e80f4 storing dealings in new map 2024-01-17 13:50:21 +00:00
527 changed files with 24904 additions and 607 deletions
+40
View File
@@ -0,0 +1,40 @@
name: ci-nym-vpn-ui-js
on:
workflow_dispatch:
pull_request:
paths:
- 'nym-vpn/ui/src/**'
- 'nym-vpn/ui/package.json'
- 'nym-vpn/ui/index.html'
jobs:
check:
runs-on: custom-linux
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
working-directory: nym-vpn/ui
run: yarn
- name: Type-check
working-directory: nym-vpn/ui
run: yarn typecheck
- name: Check lint
working-directory: nym-vpn/ui
run: yarn lint
- name: Check formatting
working-directory: nym-vpn/ui
run: yarn fmt:check
# - name: Run tests
# working-directory: nym-vpn/ui
# run: yarn test
- name: Check build
working-directory: nym-vpn/ui
run: yarn build
+63
View File
@@ -0,0 +1,63 @@
name: ci-nym-vpn-ui-rust
on:
workflow_dispatch:
pull_request:
paths:
- 'nym-vpn/ui/src-tauri/**'
jobs:
build:
runs-on: custom-linux
env:
CARGO_TERM_COLOR: always
CARGOTOML_PATH: ./nym-vpn/ui/src-tauri/Cargo.toml
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libayatana-appindicator3-dev
continue-on-error: true
- name: Checkout
uses: actions/checkout@v4
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Prepare build
run: mkdir nym-vpn/ui/dist
- name: Build
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path ${{ env.CARGOTOML_PATH }} --features custom-protocol
# - name: Run all tests
# uses: actions-rs/cargo@v1
# with:
# command: test
# args: --manifest-path ${{ env.CARGOTOML_PATH }}
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path ${{ env.CARGOTOML_PATH }} --all -- --check
- name: Annotate with clippy checks
uses: actions-rs/clippy-check@v1
continue-on-error: true
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path ${{ env.CARGOTOML_PATH }} --all-features
- name: Clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --manifest-path ${{ env.CARGOTOML_PATH }} --all-features --all-targets -- -D warnings
Generated
+1
View File
@@ -6309,6 +6309,7 @@ dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-utils",
"cw4",
"nym-contracts-common",
"nym-multisig-contract-common",
]
@@ -8,9 +8,15 @@ use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use nym_coconut_dkg_common::{
dealer::{ContractDealing, DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse},
dealer::{
DealerDetailsResponse, DealingResponse, DealingStatusResponse, PagedDealerResponse,
PagedDealingsResponse,
},
msg::QueryMsg as DkgQueryMsg,
types::{DealerDetails, Epoch, EpochId, InitialReplacementData},
types::{
DealerDetails, DealingIndex, Epoch, EpochId, InitialReplacementData,
PartialContractDealing, State,
},
verification_key::{ContractVKShare, PagedVKSharesResponse},
};
use serde::Deserialize;
@@ -22,10 +28,16 @@ pub trait DkgQueryClient {
where
for<'a> T: Deserialize<'a>;
async fn get_state(&self) -> Result<State, NyxdError> {
let request = DkgQueryMsg::GetState {};
self.query_dkg_contract(request).await
}
async fn get_current_epoch(&self) -> Result<Epoch, NyxdError> {
let request = DkgQueryMsg::GetCurrentEpochState {};
self.query_dkg_contract(request).await
}
async fn get_current_epoch_threshold(&self) -> Result<Option<u64>, NyxdError> {
let request = DkgQueryMsg::GetCurrentEpochThreshold {};
self.query_dkg_contract(request).await
@@ -64,14 +76,46 @@ pub trait DkgQueryClient {
self.query_dkg_contract(request).await
}
async fn get_dealings_paged(
async fn get_dealing_status(
&self,
idx: u64,
start_after: Option<String>,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<DealingStatusResponse, NyxdError> {
let request = DkgQueryMsg::GetDealingStatus {
epoch_id,
dealer,
dealing_index,
};
self.query_dkg_contract(request).await
}
async fn get_dealing(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<DealingResponse, NyxdError> {
let request = DkgQueryMsg::GetDealing {
epoch_id,
dealer,
dealing_index,
};
self.query_dkg_contract(request).await
}
async fn get_dealer_dealings_paged(
&self,
epoch_id: EpochId,
dealer: &str,
start_after: Option<DealingIndex>,
limit: Option<u32>,
) -> Result<PagedDealingsResponse, NyxdError> {
let request = DkgQueryMsg::GetDealing {
idx,
let request = DkgQueryMsg::GetDealings {
epoch_id,
dealer: dealer.to_string(),
limit,
start_after,
};
@@ -106,8 +150,12 @@ pub trait PagedDkgQueryClient: DkgQueryClient {
collect_paged!(self, get_past_dealers_paged, dealers)
}
async fn get_all_epoch_dealings(&self, idx: u64) -> Result<Vec<ContractDealing>, NyxdError> {
collect_paged!(self, get_dealings_paged, dealings, idx)
async fn get_all_dealer_dealings(
&self,
epoch_id: EpochId,
dealer: &str,
) -> Result<Vec<PartialContractDealing>, NyxdError> {
collect_paged!(self, get_dealer_dealings_paged, dealings, epoch_id, dealer)
}
async fn get_all_verification_key_shares(
@@ -151,6 +199,7 @@ mod tests {
msg: DkgQueryMsg,
) {
match msg {
DkgQueryMsg::GetState {} => client.get_state().ignore(),
DkgQueryMsg::GetCurrentEpochState {} => client.get_current_epoch().ignore(),
DkgQueryMsg::GetCurrentEpochThreshold {} => {
client.get_current_epoch_threshold().ignore()
@@ -165,11 +214,26 @@ mod tests {
DkgQueryMsg::GetPastDealers { limit, start_after } => {
client.get_past_dealers_paged(start_after, limit).ignore()
}
DkgQueryMsg::GetDealingStatus {
epoch_id,
dealer,
dealing_index,
} => client
.get_dealing_status(epoch_id, dealer, dealing_index)
.ignore(),
DkgQueryMsg::GetDealing {
idx,
epoch_id,
dealer,
dealing_index,
} => client.get_dealing(epoch_id, dealer, dealing_index).ignore(),
DkgQueryMsg::GetDealings {
epoch_id,
dealer,
limit,
start_after,
} => client.get_dealings_paged(idx, start_after, limit).ignore(),
} => client
.get_dealer_dealings_paged(epoch_id, &dealer, limit, start_after)
.ignore(),
DkgQueryMsg::GetVerificationKeys {
epoch_id,
limit,
@@ -10,9 +10,9 @@ use async_trait::async_trait;
use cosmrs::AccountId;
use cosmwasm_std::Addr;
use nym_coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg;
use nym_coconut_dkg_common::types::EncodedBTEPublicKeyWithProof;
use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, PartialContractDealing};
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_contracts_common::IdentityKey;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -42,12 +42,14 @@ pub trait DkgSigningClient {
async fn register_dealer(
&self,
bte_key: EncodedBTEPublicKeyWithProof,
identity_key: IdentityKey,
announce_address: String,
resharing: bool,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = DkgExecuteMsg::RegisterDealer {
bte_key_with_proof: bte_key,
identity_key,
announce_address,
resharing,
};
@@ -58,14 +60,11 @@ pub trait DkgSigningClient {
async fn submit_dealing_bytes(
&self,
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
resharing: bool,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let req = DkgExecuteMsg::CommitDealing {
dealing_bytes,
resharing,
};
let req = DkgExecuteMsg::CommitDealing { dealing, resharing };
self.execute_dkg_contract(fee, req, "dealing commitment".to_string(), vec![])
.await
@@ -148,16 +147,20 @@ mod tests {
match msg {
DkgExecuteMsg::RegisterDealer {
bte_key_with_proof,
identity_key,
announce_address,
resharing,
} => client
.register_dealer(bte_key_with_proof, announce_address, resharing, None)
.register_dealer(
bte_key_with_proof,
identity_key,
announce_address,
resharing,
None,
)
.ignore(),
DkgExecuteMsg::CommitDealing {
dealing_bytes,
resharing,
} => client
.submit_dealing_bytes(dealing_bytes, resharing, None)
DkgExecuteMsg::CommitDealing { dealing, resharing } => client
.submit_dealing_bytes(dealing, resharing, None)
.ignore(),
DkgExecuteMsg::CommitVerificationKeyShare { share, resharing } => client
.submit_verification_key_share(share, resharing, None)
@@ -6,7 +6,7 @@ use log::{debug, info};
use std::str::FromStr;
use nym_coconut_dkg_common::msg::InstantiateMsg;
use nym_coconut_dkg_common::types::TimeConfiguration;
use nym_coconut_dkg_common::types::{TimeConfiguration, DEFAULT_DEALINGS};
use nym_validator_client::nyxd::AccountId;
#[derive(Debug, Parser)]
@@ -93,6 +93,7 @@ pub async fn generate(args: Args) {
multisig_addr: multisig_addr.to_string(),
time_configuration: Some(time_configuration),
mix_denom,
key_size: DEFAULT_DEALINGS as u32,
};
debug!("instantiate_msg: {:?}", instantiate_msg);
@@ -10,6 +10,7 @@ license.workspace = true
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw-utils = { workspace = true }
cw4 = { workspace = true }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common" }
nym-multisig-contract-common = { path = "../multisig-contract" }
@@ -1,7 +1,10 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex};
use crate::types::{
ContractDealing, DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, NodeIndex,
PartialContractDealing,
};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
@@ -9,6 +12,7 @@ use cosmwasm_std::Addr;
pub struct DealerDetails {
pub address: Addr,
pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof,
pub ed25519_identity: String,
pub announce_address: String,
pub assigned_index: NodeIndex,
}
@@ -66,35 +70,50 @@ impl PagedDealerResponse {
}
#[cw_serde]
pub struct ContractDealing {
pub dealing: ContractSafeBytes,
pub struct DealingResponse {
pub epoch_id: EpochId,
pub dealer: Addr,
pub dealing_index: DealingIndex,
pub dealing: Option<ContractDealing>,
}
impl ContractDealing {
pub fn new(dealing: ContractSafeBytes, dealer: Addr) -> Self {
ContractDealing { dealing, dealer }
}
#[cw_serde]
pub struct DealingStatusResponse {
pub epoch_id: EpochId,
pub dealer: Addr,
pub dealing_index: DealingIndex,
pub dealing_submitted: bool,
}
#[cw_serde]
pub struct PagedDealingsResponse {
pub dealings: Vec<ContractDealing>,
pub per_page: usize,
pub epoch_id: EpochId,
pub dealer: Addr,
pub dealings: Vec<PartialContractDealing>,
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
pub start_next_after: Option<Addr>,
pub start_next_after: Option<DealingIndex>,
}
impl PagedDealingsResponse {
pub fn new(
dealings: Vec<ContractDealing>,
per_page: usize,
start_next_after: Option<Addr>,
epoch_id: EpochId,
dealer: Addr,
dealings: Vec<PartialContractDealing>,
start_next_after: Option<DealingIndex>,
) -> Self {
PagedDealingsResponse {
epoch_id,
dealer,
dealings,
per_page,
start_next_after,
}
}
@@ -1,17 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, EpochId, TimeConfiguration};
use crate::types::{
DealingIndex, EncodedBTEPublicKeyWithProof, EpochId, PartialContractDealing, TimeConfiguration,
};
use crate::verification_key::VerificationKeyShare;
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Addr;
#[cfg(feature = "schema")]
use crate::{
dealer::{DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse},
types::{Epoch, InitialReplacementData},
dealer::{
DealerDetailsResponse, DealingResponse, DealingStatusResponse, PagedDealerResponse,
PagedDealingsResponse,
},
types::{Epoch, InitialReplacementData, State},
verification_key::PagedVKSharesResponse,
};
use contracts_common::IdentityKey;
#[cfg(feature = "schema")]
use cosmwasm_schema::QueryResponses;
@@ -21,18 +27,22 @@ pub struct InstantiateMsg {
pub multisig_addr: String,
pub time_configuration: Option<TimeConfiguration>,
pub mix_denom: String,
/// Specifies the number of elements in the derived keys
pub key_size: u32,
}
#[cw_serde]
pub enum ExecuteMsg {
RegisterDealer {
bte_key_with_proof: EncodedBTEPublicKeyWithProof,
identity_key: IdentityKey,
announce_address: String,
resharing: bool,
},
CommitDealing {
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
resharing: bool,
},
@@ -55,6 +65,9 @@ pub enum ExecuteMsg {
#[cw_serde]
#[cfg_attr(feature = "schema", derive(QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(State))]
GetState {},
#[cfg_attr(feature = "schema", returns(Epoch))]
GetCurrentEpochState {},
@@ -79,11 +92,26 @@ pub enum QueryMsg {
start_after: Option<String>,
},
#[cfg_attr(feature = "schema", returns(PagedDealingsResponse))]
#[cfg_attr(feature = "schema", returns(DealingStatusResponse))]
GetDealingStatus {
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
},
#[cfg_attr(feature = "schema", returns(DealingResponse))]
GetDealing {
idx: u64,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
},
#[cfg_attr(feature = "schema", returns(PagedDealingsResponse))]
GetDealings {
epoch_id: EpochId,
dealer: String,
limit: Option<u32>,
start_after: Option<String>,
start_after: Option<DealingIndex>,
},
#[cfg_attr(feature = "schema", returns(PagedVKSharesResponse))]
@@ -8,14 +8,38 @@ use std::str::FromStr;
pub use crate::dealer::{DealerDetails, PagedDealerResponse};
pub use contracts_common::dealings::ContractSafeBytes;
pub use cosmwasm_std::{Addr, Coin, Timestamp};
pub use cw4::Cw4Contract;
pub type EncodedBTEPublicKeyWithProof = String;
pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str;
pub type NodeIndex = u64;
pub type EpochId = u64;
pub type DealingIndex = u32;
pub type ContractDealing = ContractSafeBytes;
// 2 public attributes, 2 private attributes, 1 fixed for coconut credential
pub const TOTAL_DEALINGS: usize = 2 + 2 + 1;
pub const DEFAULT_DEALINGS: usize = 2 + 2 + 1;
#[cw_serde]
pub struct PartialContractDealing {
pub index: DealingIndex,
pub data: ContractDealing,
}
impl PartialContractDealing {
pub fn new(index: DealingIndex, data: ContractDealing) -> Self {
PartialContractDealing { index, data }
}
}
impl From<(DealingIndex, ContractDealing)> for PartialContractDealing {
fn from(value: (DealingIndex, ContractDealing)) -> Self {
PartialContractDealing {
index: value.0,
data: value.1,
}
}
}
#[cw_serde]
pub struct InitialReplacementData {
@@ -73,6 +97,16 @@ impl Default for TimeConfiguration {
}
}
#[cw_serde]
pub struct State {
pub mix_denom: String,
pub multisig_addr: Addr,
pub group_addr: Cw4Contract,
/// Specifies the number of elements in the derived keys
pub key_size: u32,
}
#[cw_serde]
#[derive(Copy, Default)]
pub struct Epoch {
+2 -2
View File
@@ -14,8 +14,8 @@ use std::collections::HashMap;
use std::ops::Neg;
use zeroize::Zeroize;
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Ciphertexts {
pub rr: [G1Projective; NUM_CHUNKS],
pub ss: [G1Projective; NUM_CHUNKS],
+2 -2
View File
@@ -67,8 +67,8 @@ impl<'a> Instance<'a> {
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ProofOfChunking {
y0: G1Projective,
bb: Vec<G1Projective>,
+2 -2
View File
@@ -76,8 +76,8 @@ impl<'a> Instance<'a> {
}
}
#[derive(Debug)]
#[cfg_attr(test, derive(Clone, PartialEq, Eq))]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ProofOfSecretSharing {
ff: G1Projective,
aa: G2Projective,
+1 -1
View File
@@ -82,7 +82,7 @@ impl RecoveredVerificationKeys {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Dealing {
pub public_coefficients: PublicCoefficients,
+1 -1
View File
@@ -1222,7 +1222,6 @@ dependencies = [
"cw-storage-plus",
"cw4",
"cw4-group",
"lazy_static",
"nym-coconut-dkg-common",
"nym-group-contract-common",
"rusty-fork",
@@ -1237,6 +1236,7 @@ dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-utils",
"cw4",
"nym-contracts-common",
"nym-multisig-contract-common",
]
-1
View File
@@ -28,7 +28,6 @@ thiserror = { workspace = true }
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"
[features]
+299 -40
View File
@@ -8,6 +8,7 @@
"type": "object",
"required": [
"group_addr",
"key_size",
"mix_denom",
"multisig_addr"
],
@@ -15,6 +16,12 @@
"group_addr": {
"type": "string"
},
"key_size": {
"description": "Specifies the number of elements in the derived keys",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"mix_denom": {
"type": "string"
},
@@ -95,6 +102,7 @@
"required": [
"announce_address",
"bte_key_with_proof",
"identity_key",
"resharing"
],
"properties": {
@@ -104,6 +112,9 @@
"bte_key_with_proof": {
"type": "string"
},
"identity_key": {
"type": "string"
},
"resharing": {
"type": "boolean"
}
@@ -122,12 +133,12 @@
"commit_dealing": {
"type": "object",
"required": [
"dealing_bytes",
"dealing",
"resharing"
],
"properties": {
"dealing_bytes": {
"$ref": "#/definitions/ContractSafeBytes"
"dealing": {
"$ref": "#/definitions/PartialContractDealing"
},
"resharing": {
"type": "boolean"
@@ -227,6 +238,24 @@
"format": "uint8",
"minimum": 0.0
}
},
"PartialContractDealing": {
"type": "object",
"required": [
"data",
"index"
],
"properties": {
"data": {
"$ref": "#/definitions/ContractSafeBytes"
},
"index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
},
@@ -234,6 +263,19 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"get_state"
],
"properties": {
"get_state": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -352,6 +394,39 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_dealing_status"
],
"properties": {
"get_dealing_status": {
"type": "object",
"required": [
"dealer",
"dealing_index",
"epoch_id"
],
"properties": {
"dealer": {
"type": "string"
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -361,10 +436,47 @@
"get_dealing": {
"type": "object",
"required": [
"idx"
"dealer",
"dealing_index",
"epoch_id"
],
"properties": {
"idx": {
"dealer": {
"type": "string"
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_dealings"
],
"properties": {
"get_dealings": {
"type": "object",
"required": [
"dealer",
"epoch_id"
],
"properties": {
"dealer": {
"type": "string"
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
@@ -379,9 +491,11 @@
},
"start_after": {
"type": [
"string",
"integer",
"null"
]
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
@@ -480,7 +594,8 @@
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof"
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
@@ -496,6 +611,9 @@
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
@@ -744,7 +862,8 @@
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof"
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
@@ -760,6 +879,9 @@
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
@@ -776,34 +898,36 @@
},
"get_dealing": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealingsResponse",
"title": "DealingResponse",
"type": "object",
"required": [
"dealings",
"per_page"
"dealer",
"dealing_index",
"epoch_id"
],
"properties": {
"dealings": {
"type": "array",
"items": {
"$ref": "#/definitions/ContractDealing"
}
"dealer": {
"$ref": "#/definitions/Addr"
},
"per_page": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"dealing": {
"anyOf": [
{
"$ref": "#/definitions/Addr"
"$ref": "#/definitions/ContractSafeBytes"
},
{
"type": "null"
}
]
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false,
@@ -812,21 +936,91 @@
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"ContractDealing": {
"type": "object",
"required": [
"dealer",
"dealing"
"ContractSafeBytes": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
}
}
}
},
"get_dealing_status": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DealingStatusResponse",
"type": "object",
"required": [
"dealer",
"dealing_index",
"dealing_submitted",
"epoch_id"
],
"properties": {
"dealer": {
"$ref": "#/definitions/Addr"
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"dealing_submitted": {
"type": "boolean"
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
}
}
},
"get_dealings": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealingsResponse",
"type": "object",
"required": [
"dealer",
"dealings",
"epoch_id"
],
"properties": {
"dealer": {
"$ref": "#/definitions/Addr"
},
"dealings": {
"type": "array",
"items": {
"$ref": "#/definitions/PartialContractDealing"
}
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"type": [
"integer",
"null"
],
"properties": {
"dealer": {
"$ref": "#/definitions/Addr"
},
"dealing": {
"$ref": "#/definitions/ContractSafeBytes"
}
},
"additionalProperties": false
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"ContractSafeBytes": {
"type": "array",
@@ -835,6 +1029,24 @@
"format": "uint8",
"minimum": 0.0
}
},
"PartialContractDealing": {
"type": "object",
"required": [
"data",
"index"
],
"properties": {
"data": {
"$ref": "#/definitions/ContractSafeBytes"
},
"index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
},
@@ -921,7 +1133,8 @@
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof"
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
@@ -937,12 +1150,58 @@
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
}
}
},
"get_state": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "State",
"type": "object",
"required": [
"group_addr",
"key_size",
"mix_denom",
"multisig_addr"
],
"properties": {
"group_addr": {
"$ref": "#/definitions/Cw4Contract"
},
"key_size": {
"description": "Specifies the number of elements in the derived keys",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"mix_denom": {
"type": "string"
},
"multisig_addr": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"Cw4Contract": {
"description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
}
}
},
"get_verification_keys": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedVKSharesResponse",
+25 -3
View File
@@ -13,6 +13,7 @@
"required": [
"announce_address",
"bte_key_with_proof",
"identity_key",
"resharing"
],
"properties": {
@@ -22,6 +23,9 @@
"bte_key_with_proof": {
"type": "string"
},
"identity_key": {
"type": "string"
},
"resharing": {
"type": "boolean"
}
@@ -40,12 +44,12 @@
"commit_dealing": {
"type": "object",
"required": [
"dealing_bytes",
"dealing",
"resharing"
],
"properties": {
"dealing_bytes": {
"$ref": "#/definitions/ContractSafeBytes"
"dealing": {
"$ref": "#/definitions/PartialContractDealing"
},
"resharing": {
"type": "boolean"
@@ -145,6 +149,24 @@
"format": "uint8",
"minimum": 0.0
}
},
"PartialContractDealing": {
"type": "object",
"required": [
"data",
"index"
],
"properties": {
"data": {
"$ref": "#/definitions/ContractSafeBytes"
},
"index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}
@@ -4,6 +4,7 @@
"type": "object",
"required": [
"group_addr",
"key_size",
"mix_denom",
"multisig_addr"
],
@@ -11,6 +12,12 @@
"group_addr": {
"type": "string"
},
"key_size": {
"description": "Specifies the number of elements in the derived keys",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"mix_denom": {
"type": "string"
},
+89 -4
View File
@@ -2,6 +2,19 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"get_state"
],
"properties": {
"get_state": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -120,6 +133,39 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_dealing_status"
],
"properties": {
"get_dealing_status": {
"type": "object",
"required": [
"dealer",
"dealing_index",
"epoch_id"
],
"properties": {
"dealer": {
"type": "string"
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -129,10 +175,47 @@
"get_dealing": {
"type": "object",
"required": [
"idx"
"dealer",
"dealing_index",
"epoch_id"
],
"properties": {
"idx": {
"dealer": {
"type": "string"
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_dealings"
],
"properties": {
"get_dealings": {
"type": "object",
"required": [
"dealer",
"epoch_id"
],
"properties": {
"dealer": {
"type": "string"
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
@@ -147,9 +230,11 @@
},
"start_after": {
"type": [
"string",
"integer",
"null"
]
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
@@ -42,7 +42,8 @@
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof"
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
@@ -58,6 +59,9 @@
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
@@ -32,7 +32,8 @@
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof"
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
@@ -48,6 +49,9 @@
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
@@ -1,33 +1,35 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealingsResponse",
"title": "DealingResponse",
"type": "object",
"required": [
"dealings",
"per_page"
"dealer",
"dealing_index",
"epoch_id"
],
"properties": {
"dealings": {
"type": "array",
"items": {
"$ref": "#/definitions/ContractDealing"
}
"dealer": {
"$ref": "#/definitions/Addr"
},
"per_page": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"dealing": {
"anyOf": [
{
"$ref": "#/definitions/Addr"
"$ref": "#/definitions/ContractSafeBytes"
},
{
"type": "null"
}
]
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false,
@@ -36,22 +38,6 @@
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"ContractDealing": {
"type": "object",
"required": [
"dealer",
"dealing"
],
"properties": {
"dealer": {
"$ref": "#/definitions/Addr"
},
"dealing": {
"$ref": "#/definitions/ContractSafeBytes"
}
},
"additionalProperties": false
},
"ContractSafeBytes": {
"type": "array",
"items": {
@@ -0,0 +1,36 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "DealingStatusResponse",
"type": "object",
"required": [
"dealer",
"dealing_index",
"dealing_submitted",
"epoch_id"
],
"properties": {
"dealer": {
"$ref": "#/definitions/Addr"
},
"dealing_index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"dealing_submitted": {
"type": "boolean"
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
}
}
}
@@ -0,0 +1,68 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealingsResponse",
"type": "object",
"required": [
"dealer",
"dealings",
"epoch_id"
],
"properties": {
"dealer": {
"$ref": "#/definitions/Addr"
},
"dealings": {
"type": "array",
"items": {
"$ref": "#/definitions/PartialContractDealing"
}
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"ContractSafeBytes": {
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0.0
}
},
"PartialContractDealing": {
"type": "object",
"required": [
"data",
"index"
],
"properties": {
"data": {
"$ref": "#/definitions/ContractSafeBytes"
},
"index": {
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
}
}
}
@@ -42,7 +42,8 @@
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof"
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
@@ -58,6 +59,9 @@
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
@@ -0,0 +1,43 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "State",
"type": "object",
"required": [
"group_addr",
"key_size",
"mix_denom",
"multisig_addr"
],
"properties": {
"group_addr": {
"$ref": "#/definitions/Cw4Contract"
},
"key_size": {
"description": "Specifies the number of elements in the derived keys",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"mix_denom": {
"type": "string"
},
"multisig_addr": {
"$ref": "#/definitions/Addr"
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"Cw4Contract": {
"description": "Cw4Contract is a wrapper around Addr that provides a lot of helpers for working with cw4 contracts\n\nIf you wish to persist this, convert to Cw4CanonicalContract via .canonical()",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
}
}
}
+48 -11
View File
@@ -5,7 +5,7 @@ use crate::dealers::queries::{
query_current_dealers_paged, query_dealer_details, query_past_dealers_paged,
};
use crate::dealers::transactions::try_add_dealer;
use crate::dealings::queries::query_dealings_paged;
use crate::dealings::queries::{query_dealing, query_dealing_status, query_dealings_paged};
use crate::dealings::transactions::try_commit_dealings;
use crate::epoch_state::queries::{
query_current_epoch, query_current_epoch_threshold, query_initial_dealers,
@@ -13,7 +13,8 @@ use crate::epoch_state::queries::{
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::transactions::{advance_epoch_state, try_surpassed_threshold};
use crate::error::ContractError;
use crate::state::{State, MULTISIG, STATE};
use crate::state::queries::query_state;
use crate::state::storage::{MULTISIG, STATE};
use crate::verification_key_shares::queries::query_vk_shares_paged;
use crate::verification_key_shares::transactions::try_commit_verification_key_share;
use crate::verification_key_shares::transactions::try_verify_verification_key_share;
@@ -22,7 +23,7 @@ use cosmwasm_std::{
};
use cw4::Cw4Contract;
use nym_coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
use nym_coconut_dkg_common::types::{Epoch, EpochState};
use nym_coconut_dkg_common::types::{Epoch, EpochState, State};
/// Instantiate the contract.
///
@@ -39,7 +40,7 @@ pub fn instantiate(
let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?;
MULTISIG.set(deps.branch(), Some(multisig_addr.clone()))?;
let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
let group_addr = Cw4Contract::new(deps.api.addr_validate(&msg.group_addr).map_err(|_| {
ContractError::InvalidGroup {
addr: msg.group_addr.clone(),
}
@@ -49,6 +50,7 @@ pub fn instantiate(
group_addr,
multisig_addr,
mix_denom: msg.mix_denom,
key_size: msg.key_size,
};
STATE.save(deps.storage, &state)?;
@@ -76,13 +78,20 @@ pub fn execute(
match msg {
ExecuteMsg::RegisterDealer {
bte_key_with_proof,
identity_key,
announce_address,
resharing,
} => try_add_dealer(deps, info, bte_key_with_proof, announce_address, resharing),
ExecuteMsg::CommitDealing {
dealing_bytes,
} => try_add_dealer(
deps,
info,
bte_key_with_proof,
identity_key,
announce_address,
resharing,
} => try_commit_dealings(deps, info, dealing_bytes, resharing),
),
ExecuteMsg::CommitDealing { dealing, resharing } => {
try_commit_dealings(deps, info, dealing, resharing)
}
ExecuteMsg::CommitVerificationKeyShare { share, resharing } => {
try_commit_verification_key_share(deps, env, info, share, resharing)
}
@@ -97,6 +106,7 @@ pub fn execute(
#[entry_point]
pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
let response = match msg {
QueryMsg::GetState {} => to_binary(&query_state(deps.storage)?)?,
QueryMsg::GetCurrentEpochState {} => to_binary(&query_current_epoch(deps.storage)?)?,
QueryMsg::GetCurrentEpochThreshold {} => {
to_binary(&query_current_epoch_threshold(deps.storage)?)?
@@ -111,11 +121,33 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result<QueryResponse,
QueryMsg::GetPastDealers { limit, start_after } => {
to_binary(&query_past_dealers_paged(deps, start_after, limit)?)?
}
QueryMsg::GetDealingStatus {
epoch_id,
dealer,
dealing_index,
} => to_binary(&query_dealing_status(
deps,
epoch_id,
dealer,
dealing_index,
)?)?,
QueryMsg::GetDealing {
idx,
epoch_id,
dealer,
dealing_index,
} => to_binary(&query_dealing(deps, epoch_id, dealer, dealing_index)?)?,
QueryMsg::GetDealings {
epoch_id,
dealer,
limit,
start_after,
} => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?,
} => to_binary(&query_dealings_paged(
deps,
epoch_id,
dealer,
start_after,
limit,
)?)?,
QueryMsg::GetVerificationKeys {
epoch_id,
limit,
@@ -141,7 +173,7 @@ mod tests {
use cw4::Member;
use cw_multi_test::{App, AppBuilder, AppResponse, ContractWrapper, Executor};
use nym_coconut_dkg_common::msg::ExecuteMsg::RegisterDealer;
use nym_coconut_dkg_common::types::NodeIndex;
use nym_coconut_dkg_common::types::{NodeIndex, DEFAULT_DEALINGS};
use nym_group_contract_common::msg::InstantiateMsg as GroupInstantiateMsg;
fn instantiate_with_group(app: &mut App, members: &[Addr]) -> Addr {
@@ -178,6 +210,7 @@ mod tests {
multisig_addr: MULTISIG_CONTRACT.to_string(),
time_configuration: None,
mix_denom: TEST_MIX_DENOM.to_string(),
key_size: DEFAULT_DEALINGS as u32,
};
app.instantiate_contract(
coconut_dkg_code_id,
@@ -213,6 +246,7 @@ mod tests {
multisig_addr: "multisig_addr".to_string(),
time_configuration: None,
mix_denom: "nym".to_string(),
key_size: 5,
};
let info = mock_info("creator", &[]);
@@ -242,6 +276,7 @@ mod tests {
coconut_dkg_contract_addr.clone(),
&RegisterDealer {
bte_key_with_proof: "bte_key_with_proof".to_string(),
identity_key: "identity".to_string(),
announce_address: "127.0.0.1:8000".to_string(),
resharing: false,
},
@@ -256,6 +291,7 @@ mod tests {
coconut_dkg_contract_addr.clone(),
&RegisterDealer {
bte_key_with_proof: "bte_key_with_proof".to_string(),
identity_key: "identity".to_string(),
announce_address: "127.0.0.1:8000".to_string(),
resharing: false,
},
@@ -272,6 +308,7 @@ mod tests {
coconut_dkg_contract_addr,
&RegisterDealer {
bte_key_with_proof: "bte_key_with_proof".to_string(),
identity_key: "identity".to_string(),
announce_address: "127.0.0.1:8000".to_string(),
resharing: false,
},
@@ -5,7 +5,7 @@ use crate::dealers::storage as dealers_storage;
use crate::epoch_state::storage::INITIAL_REPLACEMENT_DATA;
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::STATE;
use crate::state::storage::STATE;
use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response};
use nym_coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState};
@@ -38,6 +38,7 @@ pub fn try_add_dealer(
mut deps: DepsMut<'_>,
info: MessageInfo,
bte_key_with_proof: EncodedBTEPublicKeyWithProof,
identity_key: String,
announce_address: String,
resharing: bool,
) -> Result<Response, ContractError> {
@@ -65,6 +66,7 @@ pub fn try_add_dealer(
let dealer_details = DealerDetails {
address: info.sender.clone(),
bte_public_key_with_proof: bte_key_with_proof,
ed25519_identity: identity_key,
announce_address,
assigned_index: node_index,
};
@@ -141,6 +143,7 @@ pub(crate) mod tests {
let mut env = mock_env();
let info = mock_info(owner.as_str(), &[]);
let bte_key_with_proof = String::from("bte_key_with_proof");
let identity = String::from("identity");
let announce_address = String::from("localhost:8000");
env.block.time = env
@@ -155,6 +158,7 @@ pub(crate) mod tests {
deps.as_mut(),
info,
bte_key_with_proof,
identity,
announce_address,
false,
)
+253 -142
View File
@@ -2,44 +2,74 @@
// SPDX-License-Identifier: Apache-2.0
use crate::dealings::storage;
use crate::dealings::storage::DEALINGS_BYTES;
use cosmwasm_std::{Deps, Order, StdResult};
use crate::dealings::storage::StoredDealing;
use cosmwasm_std::{Deps, StdResult};
use cw_storage_plus::Bound;
use nym_coconut_dkg_common::dealer::{ContractDealing, PagedDealingsResponse};
use nym_coconut_dkg_common::types::TOTAL_DEALINGS;
use nym_coconut_dkg_common::dealer::{
DealingResponse, DealingStatusResponse, PagedDealingsResponse,
};
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
// this does almost the same as query_dealing but doesn't return the actual dealing to make it easier on the validator
// so it wouldn't need to deal with the deserialization
pub fn query_dealing_status(
deps: Deps<'_>,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> StdResult<DealingStatusResponse> {
let dealer = deps.api.addr_validate(&dealer)?;
let dealing_submitted = StoredDealing::exists(deps.storage, epoch_id, &dealer, dealing_index);
Ok(DealingStatusResponse {
epoch_id,
dealer,
dealing_index,
dealing_submitted,
})
}
pub fn query_dealing(
deps: Deps<'_>,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> StdResult<DealingResponse> {
let dealer = deps.api.addr_validate(&dealer)?;
let dealing = StoredDealing::read(deps.storage, epoch_id, &dealer, dealing_index);
Ok(DealingResponse {
epoch_id,
dealer,
dealing_index,
dealing,
})
}
pub fn query_dealings_paged(
deps: Deps<'_>,
idx: u64,
start_after: Option<String>,
epoch_id: EpochId,
dealer: String,
start_after: Option<DealingIndex>,
limit: Option<u32>,
) -> StdResult<PagedDealingsResponse> {
let limit = limit
.unwrap_or(storage::DEALINGS_PAGE_DEFAULT_LIMIT)
.min(storage::DEALINGS_PAGE_MAX_LIMIT) as usize;
.min(storage::DEALINGS_PAGE_MAX_LIMIT);
let idx = idx as usize;
if idx >= TOTAL_DEALINGS {
return Ok(PagedDealingsResponse::new(vec![], limit, None));
}
let dealer = deps.api.addr_validate(&dealer)?;
let start = start_after.map(Bound::exclusive);
let addr = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?;
let start = addr.as_ref().map(Bound::exclusive);
let dealings = DEALINGS_BYTES[idx]
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|res| res.map(|(dealer, dealing)| ContractDealing::new(dealing, dealer)))
let dealings = StoredDealing::prefix_range(deps.storage, (epoch_id, &dealer), start)
.take(limit as usize)
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = dealings.last().map(|dealing| dealing.dealer.clone());
let start_next_after = dealings.last().map(|dealing| dealing.index);
Ok(PagedDealingsResponse::new(
epoch_id,
dealer,
dealings,
limit,
start_next_after,
))
}
@@ -48,148 +78,229 @@ pub fn query_dealings_paged(
pub(crate) mod tests {
use super::*;
use crate::dealings::storage::{DEALINGS_PAGE_DEFAULT_LIMIT, DEALINGS_PAGE_MAX_LIMIT};
use crate::support::tests::fixtures::dealing_bytes_fixture;
use crate::support::tests::fixtures::{dealing_bytes_fixture, partial_dealing_fixture};
use crate::support::tests::helpers::init_contract;
use cosmwasm_std::{Addr, DepsMut};
use nym_coconut_dkg_common::types::PartialContractDealing;
fn fill_dealings(deps: DepsMut<'_>, size: usize) {
for n in 0..size {
let dealing_share = dealing_bytes_fixture();
let sender = Addr::unchecked(format!("owner{}", n));
(0..TOTAL_DEALINGS).for_each(|idx| {
DEALINGS_BYTES[idx]
.save(deps.storage, &sender, &dealing_share)
.unwrap();
});
fn fill_dealings(deps: DepsMut<'_>, epoch: EpochId, dealers: usize, key_size: u32) {
for i in 0..dealers {
let dealer = Addr::unchecked(format!("dealer{i}"));
for dealing_index in 0..key_size {
StoredDealing::save(
deps.storage,
epoch,
&dealer,
PartialContractDealing {
index: dealing_index,
data: dealing_bytes_fixture(),
},
)
}
}
}
#[test]
fn empty_on_bad_idx() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 1000);
for idx in TOTAL_DEALINGS as u64..100 * TOTAL_DEALINGS as u64 {
let page1 = query_dealings_paged(deps.as_ref(), idx, None, None).unwrap();
assert_eq!(0, page1.dealings.len() as u32);
}
}
#[test]
fn dealings_empty_on_init() {
let deps = init_contract();
for idx in 0..TOTAL_DEALINGS as u64 {
let response = query_dealings_paged(deps.as_ref(), idx, None, Option::from(2)).unwrap();
assert_eq!(0, response.dealings.len());
}
}
#[test]
fn dealings_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_dealings(deps.as_mut(), 1000);
for idx in 0..TOTAL_DEALINGS as u64 {
let page1 =
query_dealings_paged(deps.as_ref(), idx, None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.dealings.len() as u32);
}
}
#[test]
fn dealings_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 1000);
for idx in 0..TOTAL_DEALINGS as u64 {
// query without explicitly setting a limit
let page1 = query_dealings_paged(deps.as_ref(), idx, None, None).unwrap();
assert_eq!(DEALINGS_PAGE_DEFAULT_LIMIT, page1.dealings.len() as u32);
}
}
#[test]
fn dealings_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 1000);
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * DEALINGS_PAGE_MAX_LIMIT;
for idx in 0..TOTAL_DEALINGS as u64 {
let page1 =
query_dealings_paged(deps.as_ref(), idx, None, Option::from(crazy_limit)).unwrap();
// we default to a decent sized upper bound instead
let expected_limit = DEALINGS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.dealings.len() as u32);
}
}
#[test]
fn dealings_pagination_works() {
fn test_query_dealing() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 1);
let bad_address = "FOOMP".to_string();
assert!(query_dealing(deps.as_ref(), 0, bad_address, 0).is_err());
let per_page = 2;
let empty = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
assert_eq!(empty.epoch_id, 0);
assert_eq!(empty.dealing_index, 0);
assert_eq!(empty.dealer, Addr::unchecked("foo"));
assert!(empty.dealing.is_none());
for idx in 0..TOTAL_DEALINGS as u64 {
let page1 =
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
// insert the dealing
let dealing = partial_dealing_fixture();
StoredDealing::save(
deps.as_mut().storage,
0,
&Addr::unchecked("foo"),
dealing.clone(),
);
// page should have 1 result on it
assert_eq!(1, page1.dealings.len());
let retrieved = query_dealing(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
assert_eq!(retrieved.epoch_id, 0);
assert_eq!(retrieved.dealing_index, dealing.index);
assert_eq!(retrieved.dealer, Addr::unchecked("foo"));
assert_eq!(retrieved.dealing.unwrap(), dealing.data);
}
#[test]
fn test_query_dealing_status() {
let mut deps = init_contract();
let bad_address = "FOOMP".to_string();
assert!(query_dealing_status(deps.as_ref(), 0, bad_address, 0).is_err());
let empty = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
assert_eq!(empty.epoch_id, 0);
assert_eq!(empty.dealing_index, 0);
assert_eq!(empty.dealer, Addr::unchecked("foo"));
assert!(!empty.dealing_submitted);
// insert the dealing
let dealing = partial_dealing_fixture();
StoredDealing::save(
deps.as_mut().storage,
0,
&Addr::unchecked("foo"),
dealing.clone(),
);
let retrieved = query_dealing_status(deps.as_ref(), 0, "foo".to_string(), 0).unwrap();
assert_eq!(retrieved.epoch_id, 0);
assert_eq!(retrieved.dealing_index, dealing.index);
assert_eq!(retrieved.dealer, Addr::unchecked("foo"));
assert!(retrieved.dealing_submitted)
}
#[cfg(test)]
mod query_dealings {
use super::*;
use nym_coconut_dkg_common::types::DEFAULT_DEALINGS;
#[test]
fn dealings_empty_on_init() {
let deps = init_contract();
let all_dealings = StoredDealing::unchecked_all_entries(&deps.storage);
assert!(all_dealings.is_empty())
}
// save another
fill_dealings(deps.as_mut(), 2);
#[test]
fn dealings_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32);
for idx in 0..TOTAL_DEALINGS as u64 {
// page1 should have 2 results on it
let page1 =
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealings.len());
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
let page1 =
query_dealings_paged(deps.as_ref(), 0, dealer, None, Option::from(limit))
.unwrap();
assert_eq!(limit, page1.dealings.len() as u32);
}
}
fill_dealings(deps.as_mut(), 3);
#[test]
fn dealings_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32);
for idx in 0..TOTAL_DEALINGS as u64 {
// page1 still has 2 results
let page1 =
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealings.len());
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
// query without explicitly setting a limit
let page1 = query_dealings_paged(deps.as_ref(), 0, dealer, None, None).unwrap();
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_dealings_paged(
deps.as_ref(),
idx,
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.dealings.len());
assert_eq!(DEALINGS_PAGE_DEFAULT_LIMIT, page1.dealings.len() as u32);
}
}
fill_dealings(deps.as_mut(), 4);
#[test]
fn dealings_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 0, 10, DEFAULT_DEALINGS as u32);
for idx in 0..TOTAL_DEALINGS as u64 {
let page1 =
query_dealings_paged(deps.as_ref(), idx, None, Option::from(per_page)).unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_dealings_paged(
deps.as_ref(),
idx,
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * DEALINGS_PAGE_MAX_LIMIT;
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
let page1 =
query_dealings_paged(deps.as_ref(), 0, dealer, None, Option::from(crazy_limit))
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.dealings.len());
// we default to a decent sized upper bound instead
let expected_limit = DEALINGS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.dealings.len() as u32);
}
}
#[test]
fn dealings_pagination_works() {
let mut deps = init_contract();
fill_dealings(deps.as_mut(), 0, 10, 1);
let per_page = 2;
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
let page1 =
query_dealings_paged(deps.as_ref(), 0, dealer, None, Option::from(per_page))
.unwrap();
// page should have 1 result on it
assert_eq!(1, page1.dealings.len());
}
// save another
fill_dealings(deps.as_mut(), 1, 10, 2);
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
// page1 should have 2 results on it
let page1 =
query_dealings_paged(deps.as_ref(), 1, dealer, None, Option::from(per_page))
.unwrap();
assert_eq!(2, page1.dealings.len());
}
fill_dealings(deps.as_mut(), 3, 10, 3);
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
// page1 still has 2 results
let page1 = query_dealings_paged(
deps.as_ref(),
3,
dealer.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.dealings.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_dealings_paged(
deps.as_ref(),
3,
dealer,
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.dealings.len());
}
fill_dealings(deps.as_mut(), 4, 10, 4);
for dealer in 0..10 {
let dealer = format!("dealer{dealer}");
let page1 = query_dealings_paged(
deps.as_ref(),
4,
dealer.clone(),
None,
Option::from(per_page),
)
.unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_dealings_paged(
deps.as_ref(),
4,
dealer,
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.dealings.len());
}
}
}
}
+284 -24
View File
@@ -1,33 +1,293 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_storage_plus::Map;
use nym_coconut_dkg_common::types::{ContractSafeBytes, TOTAL_DEALINGS};
use cosmwasm_std::{Addr, Order, Record, StdResult, Storage};
use cw_storage_plus::{Bound, Key, KeyDeserialize, Path, Prefix, Prefixer, PrimaryKey};
use nym_coconut_dkg_common::types::{
ContractDealing, ContractSafeBytes, DealingIndex, EpochId, PartialContractDealing,
};
pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2;
pub(crate) const DEALINGS_PAGE_DEFAULT_LIMIT: u32 = 1;
type DealingKey<'a> = &'a Addr;
type Dealer<'a> = &'a Addr;
// Note to whoever is looking at this implementation and is thinking of using something similar
// for storing small commitments/hashes of data on chain:
// If there's a lot of entries you want to store thinking, "oh, this digest is only 32 bytes, it's not that much",
// the default cosmwasm' serializer will bloat it to around ~100B. So you really don't want to be using
// Buckets/Maps, etc. for that purpose. Instead you want to use `storage` directly (look into the actual implementation of
// `Map` or `Bucket` to see what I mean. Instead of using the `to_vec` method on serde_json_wasm, you'd
// provide your data directly yourself.
// but you must be extremely careful when doing so, as you might end up overwriting some existing data
// if you don't choose your prefixes wisely.
// I didn't have to do it here as I'm storing relatively little data and after just base58-encoding
// my bytes, I was fine with the json overhead.
// dealings are stored in a multilevel map with the following hierarchy:
// - epoch-id:
// - issuer-address:
// - dealing id:
// - dealing content
// NOTE: we're storing raw bytes bypassing serialization, so we can't use the `Map` type,
// thus make sure you always use the below methods for using the storage!
// if TOTAL_DEALINGS is modified to anything other then current value (5), this part will also need
// to be modified
pub(crate) const DEALINGS_BYTES: [Map<'_, DealingKey<'_>, ContractSafeBytes>; TOTAL_DEALINGS] = [
Map::new("dbyt1"),
Map::new("dbyt2"),
Map::new("dbyt3"),
Map::new("dbyt4"),
Map::new("dbyt5"),
];
pub(crate) struct StoredDealing;
impl StoredDealing {
const NAMESPACE: &'static [u8] = b"dealing";
fn deserialize_dealing_record(kv: Record) -> StdResult<(DealingIndex, ContractDealing)> {
let (k, v) = kv;
let index = <DealingIndex as KeyDeserialize>::from_vec(k)?;
let data = ContractSafeBytes(v);
Ok((index, data))
}
fn storage_key(
epoch_id: EpochId,
dealer: Dealer,
dealing_index: DealingIndex,
) -> Path<Vec<u8>> {
// just replicate the behaviour from `Map::key`
let key = (epoch_id, dealer, dealing_index);
Path::new(
Self::NAMESPACE,
&key.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
)
}
fn prefix(prefix: (EpochId, Dealer)) -> Prefix<DealingIndex, ContractSafeBytes, DealingIndex> {
Prefix::with_deserialization_functions(
Self::NAMESPACE,
&prefix.prefix(),
&[],
// explicitly panic to make sure we're never attempting to call an unexpected deserializer on our data
|_, _, kv| Self::deserialize_dealing_record(kv),
|_, _, _| panic!("attempted to call custom de_fn_v"),
)
}
pub(crate) fn exists(
storage: &dyn Storage,
epoch_id: EpochId,
dealer: &Addr,
dealing_index: DealingIndex,
) -> bool {
StoredDealing::storage_key(epoch_id, dealer, dealing_index).has(storage)
}
pub(crate) fn save(
storage: &mut dyn Storage,
epoch_id: EpochId,
dealer: Dealer,
dealing: PartialContractDealing,
) {
// NOTE: we're storing bytes directly here!
let storage_key = StoredDealing::storage_key(epoch_id, dealer, dealing.index);
storage.set(&storage_key, dealing.data.as_slice());
}
pub(crate) fn read(
storage: &dyn Storage,
epoch_id: EpochId,
dealer: Dealer,
dealing_index: DealingIndex,
) -> Option<ContractDealing> {
let storage_key = StoredDealing::storage_key(epoch_id, dealer, dealing_index);
let raw_dealing = storage.get(&storage_key);
raw_dealing.map(ContractSafeBytes)
}
pub(crate) fn prefix_range<'a>(
storage: &'a dyn Storage,
prefix: (EpochId, Dealer),
start: Option<Bound<DealingIndex>>,
) -> impl Iterator<Item = StdResult<PartialContractDealing>> + 'a {
Self::prefix(prefix)
.range(storage, start, None, Order::Ascending)
.map(|maybe_record| maybe_record.map(Into::into))
}
// iterate over all values, only to be used in tests due to the amount of data being returned
#[cfg(test)]
pub(crate) fn unchecked_all_entries(
storage: &dyn Storage,
) -> Vec<((EpochId, Addr, DealingIndex), ContractDealing)> {
type StorageKey<'a> = (EpochId, Dealer<'a>, DealingIndex);
let empty_prefix: Prefix<StorageKey, ContractDealing, StorageKey> =
Prefix::with_deserialization_functions(
Self::NAMESPACE,
&[],
&[],
|_, _, kv| StorageKey::from_vec(kv.0).map(|kt| (kt, ContractSafeBytes(kv.1))),
|_, _, _| unimplemented!(),
);
empty_prefix
.range(storage, None, None, Order::Ascending)
.collect::<StdResult<_>>()
.unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::support::tests::helpers::init_contract;
use std::collections::HashMap;
fn dealing_data(
epoch_id: EpochId,
dealer: Dealer,
dealing_index: DealingIndex,
) -> ContractDealing {
ContractSafeBytes(
format!("{epoch_id},{dealer},{dealing_index}")
.as_bytes()
.to_vec(),
)
}
#[test]
fn saving_dealing() {
let mut deps = init_contract();
// make sure to check all combinations of epoch id, dealer address and dealing index to ensure nothing overlaps
let epochs = [54, 423, 754];
let dealers = [
Addr::unchecked("dealer1"),
Addr::unchecked("dealer2"),
Addr::unchecked("dealer3"),
Addr::unchecked("dealer4"),
Addr::unchecked("dealer5"),
];
let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7];
for epoch_id in &epochs {
for dealer in &dealers {
for dealing_index in &dealing_indices {
assert!(!StoredDealing::exists(
&deps.storage,
*epoch_id,
dealer,
*dealing_index
));
StoredDealing::save(
deps.as_mut().storage,
*epoch_id,
dealer,
PartialContractDealing {
index: *dealing_index,
data: dealing_data(*epoch_id, dealer, *dealing_index),
},
)
}
}
}
let all: HashMap<_, _> = StoredDealing::unchecked_all_entries(&deps.storage)
.into_iter()
.collect();
assert_eq!(
all.len(),
epochs.len() * dealers.len() * dealing_indices.len()
);
for epoch_id in &epochs {
for dealer in &dealers {
for dealing_index in &dealing_indices {
assert!(StoredDealing::exists(
&deps.storage,
*epoch_id,
dealer,
*dealing_index
));
let content =
StoredDealing::read(&deps.storage, *epoch_id, dealer, *dealing_index)
.unwrap();
let expected = dealing_data(*epoch_id, dealer, *dealing_index);
assert_eq!(expected, content);
assert_eq!(
&expected,
all.get(&(*epoch_id, dealer.clone(), *dealing_index))
.unwrap()
);
}
}
}
}
#[test]
fn iterating_over_dealings() {
let mut deps = init_contract();
let epochs = [54, 423, 754];
let dealers = [
Addr::unchecked("dealer1"),
Addr::unchecked("dealer2"),
Addr::unchecked("dealer3"),
Addr::unchecked("dealer4"),
Addr::unchecked("dealer5"),
];
let dealing_indices = [0, 1, 2, 3, 4, 5, 6, 7];
for epoch_id in &epochs {
for dealer in &dealers {
for dealing_index in &dealing_indices {
StoredDealing::save(
deps.as_mut().storage,
*epoch_id,
dealer,
PartialContractDealing {
index: *dealing_index,
data: dealing_data(*epoch_id, dealer, *dealing_index),
},
)
}
}
}
// remember, we're not testing the iterator implementation
// nothing under epoch 0
let dealings =
StoredDealing::prefix_range(&deps.storage, (0, &dealers[0]), None).collect::<Vec<_>>();
assert!(dealings.is_empty());
// nothing for dealer "foo"
let foo = Addr::unchecked("foo");
let dealings =
StoredDealing::prefix_range(&deps.storage, (epochs[0], &foo), None).collect::<Vec<_>>();
assert!(dealings.is_empty());
let all = StoredDealing::prefix_range(&deps.storage, (epochs[0], &dealers[0]), None)
.collect::<Vec<_>>();
assert_eq!(all.len(), dealing_indices.len());
for (i, dealing) in all.iter().enumerate() {
let expected = dealing_data(epochs[0], &dealers[0], dealing_indices[i]);
assert_eq!(expected, dealing.as_ref().unwrap().data);
assert_eq!(dealing_indices[i], dealing.as_ref().unwrap().index);
}
// for sanity sake, check another dealer with different epoch
let all_other = StoredDealing::prefix_range(&deps.storage, (epochs[2], &dealers[3]), None)
.collect::<Vec<_>>();
assert_eq!(all_other.len(), dealing_indices.len());
for (i, dealing) in all_other.iter().enumerate() {
let expected = dealing_data(epochs[2], &dealers[3], dealing_indices[i]);
assert_eq!(expected, dealing.as_ref().unwrap().data);
assert_eq!(dealing_indices[i], dealing.as_ref().unwrap().index);
}
let without_first = StoredDealing::prefix_range(
&deps.storage,
(epochs[0], &dealers[0]),
Some(Bound::exclusive(dealing_indices[0])),
)
.collect::<Vec<_>>();
assert_eq!(&all[1..], without_first);
let mid = StoredDealing::prefix_range(
&deps.storage,
(epochs[0], &dealers[0]),
Some(Bound::inclusive(dealing_indices[3])),
)
.collect::<Vec<_>>();
assert_eq!(&all[3..], mid);
}
}
@@ -2,17 +2,18 @@
// SPDX-License-Identifier: Apache-2.0
use crate::dealers::storage as dealers_storage;
use crate::dealings::storage::DEALINGS_BYTES;
use crate::epoch_state::storage::INITIAL_REPLACEMENT_DATA;
use crate::dealings::storage::StoredDealing;
use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::storage::STATE;
use cosmwasm_std::{DepsMut, MessageInfo, Response};
use nym_coconut_dkg_common::types::{ContractSafeBytes, EpochState};
use nym_coconut_dkg_common::types::{EpochState, PartialContractDealing};
pub fn try_commit_dealings(
deps: DepsMut<'_>,
info: MessageInfo,
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
resharing: bool,
) -> Result<Response, ContractError> {
check_epoch_state(deps.storage, EpochState::DealingExchange { resharing })?;
@@ -32,18 +33,32 @@ pub fn try_commit_dealings(
return Err(ContractError::NotAnInitialDealer);
}
// check if this dealer has already committed to all dealings
// (we don't want to allow overwriting anything)
for dealings in DEALINGS_BYTES {
if !dealings.has(deps.storage, &info.sender) {
dealings.save(deps.storage, &info.sender, &dealing_bytes)?;
return Ok(Response::default());
}
let state = STATE.load(deps.storage)?;
let epoch = CURRENT_EPOCH.load(deps.storage)?;
// check if the index is in range without doing expensive storage reads
// note: dealing indexing starts from 0
if dealing.index >= state.key_size {
return Err(ContractError::DealingOutOfRange {
epoch_id: epoch.epoch_id,
dealer: info.sender,
index: dealing.index,
key_size: state.key_size,
});
}
Err(ContractError::AlreadyCommitted {
commitment: String::from("dealing"),
})
// check if this dealer has already committed this particular dealing
if StoredDealing::exists(deps.storage, epoch.epoch_id, &info.sender, dealing.index) {
return Err(ContractError::DealingAlreadyCommitted {
epoch_id: epoch.epoch_id,
dealer: info.sender,
index: dealing.index,
});
}
StoredDealing::save(deps.storage, epoch.epoch_id, &info.sender, dealing);
Ok(Response::new())
}
#[cfg(test)]
@@ -51,13 +66,15 @@ pub(crate) mod tests {
use super::*;
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::transactions::advance_epoch_state;
use crate::support::tests::fixtures::{dealer_details_fixture, dealing_bytes_fixture};
use crate::support::tests::fixtures::{dealer_details_fixture, partial_dealing_fixture};
use crate::support::tests::helpers;
use crate::support::tests::helpers::add_fixture_dealer;
use cosmwasm_std::testing::{mock_env, mock_info};
use cosmwasm_std::Addr;
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::types::{InitialReplacementData, TimeConfiguration};
use nym_coconut_dkg_common::types::{
ContractSafeBytes, InitialReplacementData, TimeConfiguration, DEFAULT_DEALINGS,
};
#[test]
fn invalid_commit_dealing() {
@@ -65,10 +82,10 @@ pub(crate) mod tests {
let owner = Addr::unchecked("owner1");
let mut env = mock_env();
let info = mock_info(owner.as_str(), &[]);
let dealing_bytes = dealing_bytes_fixture();
let dealing = partial_dealing_fixture();
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
.unwrap_err();
let ret =
try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err();
assert_eq!(
ret,
ContractError::IncorrectEpochState {
@@ -84,13 +101,14 @@ pub(crate) mod tests {
add_fixture_dealer(deps.as_mut());
advance_epoch_state(deps.as_mut(), env).unwrap();
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), false)
.unwrap_err();
let ret =
try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err();
assert_eq!(ret, ContractError::NotADealer);
let dealer_details = DealerDetails {
address: owner.clone(),
bte_public_key_with_proof: String::new(),
ed25519_identity: String::new(),
announce_address: String::new(),
assigned_index: 1,
};
@@ -114,8 +132,8 @@ pub(crate) mod tests {
},
)
.unwrap();
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true)
.unwrap_err();
let ret =
try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), true).unwrap_err();
assert_eq!(ret, ContractError::NotAnInitialDealer);
INITIAL_REPLACEMENT_DATA
@@ -125,18 +143,60 @@ pub(crate) mod tests {
})
.unwrap();
for dealings in DEALINGS_BYTES {
assert!(!dealings.has(deps.as_mut().storage, &owner));
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing_bytes.clone(), true);
assert!(ret.is_ok());
assert!(dealings.has(deps.as_mut().storage, &owner));
}
let ret = try_commit_dealings(deps.as_mut(), info, dealing_bytes, true).unwrap_err();
// back to 'normal' mode
CURRENT_EPOCH
.update::<_, ContractError>(deps.as_mut().storage, |mut epoch| {
epoch.state = EpochState::DealingExchange { resharing: false };
Ok(epoch)
})
.unwrap();
// dealing out of range
let ret = try_commit_dealings(
deps.as_mut(),
info.clone(),
PartialContractDealing {
index: 42,
data: ContractSafeBytes(vec![1, 2, 3]),
},
false,
)
.unwrap_err();
assert_eq!(
ret,
ContractError::AlreadyCommitted {
commitment: String::from("dealing"),
ContractError::DealingOutOfRange {
epoch_id: 0,
dealer: info.sender.clone(),
index: 42,
key_size: DEFAULT_DEALINGS as u32,
}
);
// 'good' dealing
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false);
assert!(ret.is_ok());
// duplicate dealing
let ret =
try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false).unwrap_err();
assert_eq!(
ret,
ContractError::DealingAlreadyCommitted {
epoch_id: 0,
dealer: info.sender.clone(),
index: 0,
}
);
// same index, but next epoch
CURRENT_EPOCH
.update::<_, ContractError>(deps.as_mut().storage, |mut epoch| {
epoch.epoch_id += 1;
Ok(epoch)
})
.unwrap();
let ret = try_commit_dealings(deps.as_mut(), info.clone(), dealing.clone(), false);
assert!(ret.is_ok());
}
}
@@ -2,16 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
use crate::dealers::storage::{current_dealers, past_dealers};
use crate::dealings::storage::DEALINGS_BYTES;
use crate::epoch_state::storage::{CURRENT_EPOCH, INITIAL_REPLACEMENT_DATA, THRESHOLD};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::STATE;
use crate::state::storage::STATE;
use crate::verification_key_shares::storage::verified_dealers;
use cosmwasm_std::{Addr, Deps, DepsMut, Env, Order, Response, Storage};
use nym_coconut_dkg_common::types::{Epoch, EpochState, InitialReplacementData};
fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
fn reset_dkg_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
THRESHOLD.remove(storage);
let dealers: Vec<_> = current_dealers()
.keys(storage, None, None, Order::Ascending)
@@ -19,15 +18,6 @@ fn reset_epoch_state(storage: &mut dyn Storage) -> Result<(), ContractError> {
for dealer_addr in dealers {
let details = current_dealers().load(storage, &dealer_addr)?;
for dealings in DEALINGS_BYTES {
let dealing_keys: Vec<_> = dealings
.keys(storage, None, None, Order::Ascending)
.flatten()
.collect();
for key in dealing_keys {
dealings.remove(storage, &key);
}
}
current_dealers().remove(storage, &dealer_addr)?;
past_dealers().save(storage, &dealer_addr, &details)?;
}
@@ -123,6 +113,8 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
} else if dealers_eq_members(&deps)? {
// The dealer set hasn't changed, so we only extend the finish timestamp
// The epoch remains the same, as we use it as key for storing VKs
// TODO: change that behaviour in the following PR
Epoch::new(
current_epoch.state,
current_epoch.epoch_id,
@@ -152,7 +144,7 @@ pub(crate) fn advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Respons
EpochState::PublicKeySubmission { resharing: true }
};
reset_epoch_state(deps.storage)?;
reset_dkg_state(deps.storage)?;
Epoch::new(
state,
current_epoch.epoch_id + 1,
@@ -174,7 +166,7 @@ pub(crate) fn try_surpassed_threshold(
let threshold = THRESHOLD.load(deps.storage)?;
let dealers = verified_dealers(deps.storage)?;
if dealers_still_active(&deps.as_ref(), dealers.into_iter())? < threshold as usize {
reset_epoch_state(deps.storage)?;
reset_dkg_state(deps.storage)?;
CURRENT_EPOCH.update::<_, ContractError>(deps.storage, |epoch| {
Ok(Epoch::new(
EpochState::default(),
@@ -198,9 +190,7 @@ pub(crate) mod tests {
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::Addr;
use cw4::Member;
use nym_coconut_dkg_common::types::{
ContractSafeBytes, DealerDetails, EpochState, TimeConfiguration,
};
use nym_coconut_dkg_common::types::{DealerDetails, EpochState, TimeConfiguration};
use rusty_fork::rusty_fork_test;
// Because of the global variable handling group, we need individual process for each test
@@ -788,27 +778,12 @@ pub(crate) mod tests {
current_dealers()
.save(deps.as_mut().storage, &details.address, details)
.unwrap();
for dealings in DEALINGS_BYTES {
dealings
.save(
deps.as_mut().storage,
&details.address,
&ContractSafeBytes(vec![1, 2, 3]),
)
.unwrap();
}
}
reset_epoch_state(deps.as_mut().storage).unwrap();
reset_dkg_state(deps.as_mut().storage).unwrap();
assert!(THRESHOLD.may_load(&deps.storage).unwrap().is_none());
for details in all_details {
for dealings in DEALINGS_BYTES {
assert!(dealings
.may_load(&deps.storage, &details.address)
.unwrap()
.is_none());
}
assert!(current_dealers()
.may_load(deps.as_mut().storage, &details.address)
.unwrap()
@@ -838,6 +813,7 @@ pub(crate) mod tests {
&DealerDetails {
address: address.clone(),
bte_public_key_with_proof: "bte_public_key_with_proof".to_string(),
ed25519_identity: "identity".to_string(),
announce_address: "127.0.0.1".to_string(),
assigned_index: i,
},
+21 -1
View File
@@ -1,8 +1,9 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::StdError;
use cosmwasm_std::{Addr, StdError};
use cw_controllers::AdminError;
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
use thiserror::Error;
/// Custom errors for contract failure conditions.
@@ -43,6 +44,25 @@ pub enum ContractError {
#[error("This sender is not a dealer for the current resharing epoch")]
NotAnInitialDealer,
#[error(
"Dealer {dealer} has already committed dealing for epoch {epoch_id} with index {index}"
)]
DealingAlreadyCommitted {
epoch_id: EpochId,
dealer: Addr,
index: DealingIndex,
},
#[error(
"Dealer {dealer} has attempted to commit dealing for epoch {epoch_id} with index {index} while the key size is set to {key_size}"
)]
DealingOutOfRange {
epoch_id: EpochId,
dealer: Addr,
index: DealingIndex,
key_size: u32,
},
#[error("This dealer has already committed {commitment}")]
AlreadyCommitted { commitment: String },
+3 -17
View File
@@ -1,19 +1,5 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw4::Cw4Contract;
use cw_controllers::Admin;
use cw_storage_plus::Item;
use serde::{Deserialize, Serialize};
// unique items
pub const STATE: Item<State> = Item::new("state");
pub const MULTISIG: Admin = Admin::new("multisig");
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct State {
pub mix_denom: String,
pub multisig_addr: Addr,
pub group_addr: Cw4Contract,
}
pub mod queries;
pub mod storage;
@@ -0,0 +1,10 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::state::storage::STATE;
use cosmwasm_std::{StdResult, Storage};
use nym_coconut_dkg_common::types::State;
pub(crate) fn query_state(storage: &dyn Storage) -> StdResult<State> {
STATE.load(storage)
}
@@ -0,0 +1,10 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw_controllers::Admin;
use cw_storage_plus::Item;
use nym_coconut_dkg_common::types::State;
// unique items
pub const STATE: Item<State> = Item::new("state");
pub const MULTISIG: Admin = Admin::new("multisig");
@@ -3,7 +3,7 @@
use cosmwasm_std::Addr;
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::types::ContractSafeBytes;
use nym_coconut_dkg_common::types::{ContractSafeBytes, PartialContractDealing};
use nym_coconut_dkg_common::verification_key::ContractVKShare;
pub const TEST_MIX_DENOM: &str = "unym";
@@ -20,13 +20,21 @@ pub fn vk_share_fixture(owner: &str, index: u64) -> ContractVKShare {
}
pub fn dealing_bytes_fixture() -> ContractSafeBytes {
ContractSafeBytes(vec![])
ContractSafeBytes(vec![1, 2, 3])
}
pub fn partial_dealing_fixture() -> PartialContractDealing {
PartialContractDealing {
index: 0,
data: ContractSafeBytes(vec![1, 2, 3]),
}
}
pub fn dealer_details_fixture(assigned_index: u64) -> DealerDetails {
DealerDetails {
address: Addr::unchecked(format!("owner{}", assigned_index)),
bte_public_key_with_proof: "".to_string(),
ed25519_identity: "".to_string(),
announce_address: "".to_string(),
assigned_index,
}
@@ -9,9 +9,8 @@ use cosmwasm_std::{
QuerierResult, SystemResult, WasmQuery,
};
use cw4::{Cw4QueryMsg, Member, MemberListResponse, MemberResponse};
use lazy_static::lazy_static;
use nym_coconut_dkg_common::msg::InstantiateMsg;
use nym_coconut_dkg_common::types::DealerDetails;
use nym_coconut_dkg_common::types::{DealerDetails, DEFAULT_DEALINGS};
use std::sync::Mutex;
use super::fixtures::TEST_MIX_DENOM;
@@ -20,9 +19,7 @@ pub const ADMIN_ADDRESS: &str = "admin address";
pub const GROUP_CONTRACT: &str = "group contract address";
pub const MULTISIG_CONTRACT: &str = "multisig contract address";
lazy_static! {
pub static ref GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(vec![]);
}
pub(crate) static GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(Vec::new());
pub fn add_fixture_dealer(deps: DepsMut<'_>) {
let owner = Addr::unchecked("owner");
@@ -33,6 +30,7 @@ pub fn add_fixture_dealer(deps: DepsMut<'_>) {
&DealerDetails {
address: owner.clone(),
bte_public_key_with_proof: String::new(),
ed25519_identity: String::new(),
announce_address: String::new(),
assigned_index: 100,
},
@@ -87,6 +85,7 @@ pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>>
multisig_addr: String::from(MULTISIG_CONTRACT),
time_configuration: None,
mix_denom: TEST_MIX_DENOM.to_string(),
key_size: DEFAULT_DEALINGS as u32,
};
let env = mock_env();
let info = mock_info(ADMIN_ADDRESS, &[]);
@@ -6,7 +6,7 @@ use crate::dealers::storage as dealers_storage;
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::{MULTISIG, STATE};
use crate::state::storage::{MULTISIG, STATE};
use crate::verification_key_shares::storage::vk_shares;
use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response};
use nym_coconut_dkg_common::types::EpochState;
@@ -122,6 +122,7 @@ mod tests {
let dealer_details = DealerDetails {
address: dealer.clone(),
bte_public_key_with_proof: String::new(),
ed25519_identity: String::new(),
announce_address: announce_address.clone(),
assigned_index: 1,
};
@@ -193,6 +194,7 @@ mod tests {
let dealer_details = DealerDetails {
address: dealer.clone(),
bte_public_key_with_proof: String::new(),
ed25519_identity: String::new(),
announce_address: String::new(),
assigned_index: 1,
};
@@ -300,6 +302,7 @@ mod tests {
let dealer_details = DealerDetails {
address: owner.clone(),
bte_public_key_with_proof: String::new(),
ed25519_identity: String::new(),
announce_address: String::new(),
assigned_index: 1,
};
@@ -75,6 +75,7 @@ fn dkg_proposal() {
multisig_addr: multisig_contract_addr.to_string(),
time_configuration: None,
mix_denom: TEST_COIN_DENOM.to_string(),
key_size: 5,
};
let coconut_dkg_contract_addr = app
.instantiate_contract(
@@ -104,6 +105,7 @@ fn dkg_proposal() {
coconut_dkg_contract_addr.clone(),
&RegisterDealer {
bte_key_with_proof: "bte_key_with_proof".to_string(),
identity_key: "identity".to_string(),
announce_address: "127.0.0.1:8000".to_string(),
resharing: false,
},
+5 -5
View File
@@ -13,13 +13,13 @@ DENOMS_EXPONENT=6
REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa
MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n13902g92xfefeyzuyed49snlm5fxv5ms6mdq5kvrut27hasdw5a9q9vyw6c
GROUP_CONTRACT_ADDRESS=n18nczmqw6adwxg2wnlef3hf0etf8anccafp2pjpul5rrtmv96umyq5mv7t5
MULTISIG_CONTRACT_ADDRESS=n1q3zzxl78rlmxv3vn0uf4vkyz285lk8q2xzne299yt9x6mpfgk90qukuzmv
COCONUT_DKG_CONTRACT_ADDRESS=n1jsz20ggp5a6v76j060erkzvxmeus8htlpl77yxp878f0gf95cyaq6p2pee
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l
GROUP_CONTRACT_ADDRESS=n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju
MULTISIG_CONTRACT_ADDRESS=n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k
COCONUT_DKG_CONTRACT_ADDRESS=n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a
EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
NAME_SERVICE_CONTRACT_ADDRESS=n12ne7qtmdwd0j03t9t5es8md66wq4e5xg9neladrsag8fx3y89rcs36asfp
SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ps5yutd7sufwg058qd7ac7ldnlazsvmhzqwucsfxmm445d70u8asqxpur4
EPHEMERA_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
EXPLORER_API=https://sandbox-explorer.nymtech.net/api
+19 -5
View File
@@ -5,12 +5,13 @@ use crate::coconut::error::Result;
use cw3::ProposalResponse;
use cw4::MemberResponse;
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse;
use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse};
use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse, DealingStatusResponse};
use nym_coconut_dkg_common::types::{
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
PartialContractDealing, State,
};
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_contracts_common::IdentityKey;
use nym_dkg::Threshold;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{AccountId, Fee, Hash, TxResponse};
@@ -25,13 +26,25 @@ pub trait Client {
&self,
blinded_serial_number: String,
) -> Result<SpendCredentialResponse>;
async fn contract_state(&self) -> Result<State>;
async fn get_current_epoch(&self) -> Result<Epoch>;
async fn group_member(&self, addr: String) -> Result<MemberResponse>;
async fn get_current_epoch_threshold(&self) -> Result<Option<Threshold>>;
async fn get_initial_dealers(&self) -> Result<Option<InitialReplacementData>>;
async fn get_self_registered_dealer_details(&self) -> Result<DealerDetailsResponse>;
async fn get_dealing_status(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> Result<DealingStatusResponse>;
async fn get_current_dealers(&self) -> Result<Vec<DealerDetails>>;
async fn get_dealings(&self, idx: usize) -> Result<Vec<ContractDealing>>;
async fn get_dealings(
&self,
epoch_id: EpochId,
dealer: &str,
) -> Result<Vec<PartialContractDealing>>;
async fn get_verification_key_shares(&self, epoch_id: EpochId) -> Result<Vec<ContractVKShare>>;
async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option<Fee>)
-> Result<()>;
@@ -40,12 +53,13 @@ pub trait Client {
async fn register_dealer(
&self,
bte_key: EncodedBTEPublicKeyWithProof,
identity_key: IdentityKey,
announce_address: String,
resharing: bool,
) -> Result<ExecuteResult>;
async fn submit_dealing(
&self,
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
resharing: bool,
) -> Result<ExecuteResult>;
async fn submit_verification_key_share(
+57 -11
View File
@@ -5,22 +5,25 @@ use crate::coconut::client::Client;
use crate::coconut::error::CoconutError;
use cw3::ProposalResponse;
use cw4::MemberResponse;
use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse};
use nym_coconut_dkg_common::dealer::{DealerDetails, DealerDetailsResponse};
use nym_coconut_dkg_common::types::{
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex,
DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, NodeIndex,
PartialContractDealing, State as ContractState,
};
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_contracts_common::IdentityKey;
use nym_dkg::Threshold;
use nym_validator_client::nyxd::cosmwasm_client::logs::{find_attribute, NODE_INDEX};
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::AccountId;
use std::time::Duration;
pub(crate) struct DkgClient {
inner: Box<dyn Client + Send + Sync>,
}
impl DkgClient {
// FIXME:
// Some queries simply don't work the first time
// Until we determine why that is, retry the query a few more times
const RETRIES: usize = 3;
@@ -44,11 +47,24 @@ impl DkgClient {
if ret.is_ok() {
return ret;
}
tokio::time::sleep(Duration::from_millis(200)).await;
ret = self.inner.get_current_epoch().await;
}
ret
}
pub(crate) async fn get_contract_state(&self) -> Result<ContractState, CoconutError> {
let mut ret = self.inner.contract_state().await;
for _ in 0..Self::RETRIES {
if ret.is_ok() {
return ret;
}
tokio::time::sleep(Duration::from_millis(200)).await;
ret = self.inner.contract_state().await;
}
ret
}
pub(crate) async fn group_member(&self) -> Result<MemberResponse, CoconutError> {
self.inner
.group_member(self.get_address().await.to_string())
@@ -77,16 +93,44 @@ impl DkgClient {
self.inner.get_current_dealers().await
}
pub(crate) async fn get_dealings(
pub(crate) async fn get_dealing_status(
&self,
idx: usize,
) -> Result<Vec<ContractDealing>, CoconutError> {
let mut ret = self.inner.get_dealings(idx).await;
epoch_id: EpochId,
dealing_index: DealingIndex,
) -> Result<bool, CoconutError> {
let address = self.inner.address().await.to_string();
let mut ret = self
.inner
.get_dealing_status(epoch_id, address.clone(), dealing_index)
.await
.map(|r| r.dealing_submitted);
for _ in 0..Self::RETRIES {
if ret.is_ok() {
return ret;
}
ret = self.inner.get_dealings(idx).await;
tokio::time::sleep(Duration::from_millis(200)).await;
ret = self
.inner
.get_dealing_status(epoch_id, address.clone(), dealing_index)
.await
.map(|r| r.dealing_submitted);
}
ret
}
pub(crate) async fn get_dealings(
&self,
epoch_id: EpochId,
dealer: String,
) -> Result<Vec<PartialContractDealing>, CoconutError> {
let mut ret = self.inner.get_dealings(epoch_id, &dealer).await;
for _ in 0..Self::RETRIES {
if ret.is_ok() {
return ret;
}
tokio::time::sleep(Duration::from_millis(200)).await;
ret = self.inner.get_dealings(epoch_id, &dealer).await;
}
ret
}
@@ -109,12 +153,13 @@ impl DkgClient {
pub(crate) async fn register_dealer(
&self,
bte_key: EncodedBTEPublicKeyWithProof,
identity_key: IdentityKey,
announce_address: String,
resharing: bool,
) -> Result<NodeIndex, CoconutError> {
let res = self
.inner
.register_dealer(bte_key, announce_address, resharing)
.register_dealer(bte_key, identity_key, announce_address, resharing)
.await?;
let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX)
.ok_or(CoconutError::NodeIndexRecoveryError {
@@ -131,10 +176,10 @@ impl DkgClient {
pub(crate) async fn submit_dealing(
&self,
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
resharing: bool,
) -> Result<(), CoconutError> {
self.inner.submit_dealing(dealing_bytes, resharing).await?;
self.inner.submit_dealing(dealing, resharing).await?;
Ok(())
}
@@ -151,6 +196,7 @@ impl DkgClient {
if let Ok(res) = ret {
return Ok(res);
}
tokio::time::sleep(Duration::from_millis(200)).await;
ret = self
.inner
.submit_verification_key_share(share.clone(), resharing)
+7 -1
View File
@@ -15,6 +15,7 @@ use crate::nyxd;
use crate::support::config;
use anyhow::{bail, Result};
use nym_coconut_dkg_common::types::EpochState;
use nym_crypto::asymmetric::identity;
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
use nym_task::{TaskClient, TaskManager};
use rand::rngs::OsRng;
@@ -51,6 +52,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
config: &config::CoconutSigner,
nyxd_client: nyxd::Client,
coconut_keypair: CoconutKeyPair,
identity_key: identity::PublicKey,
rng: R,
) -> Result<Self> {
let Some(announce_address) = &config.announce_address else {
@@ -82,6 +84,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
persistent_state,
announce_address.clone(),
dkg_keypair,
identity_key,
coconut_keypair,
),
rng,
@@ -140,6 +143,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
verification_key_submission(
&self.dkg_client,
&mut self.state,
epoch.epoch_id,
&keypair_path,
resharing,
)
@@ -207,6 +211,7 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
config: &config::CoconutSigner,
nyxd_client: nyxd::Client,
coconut_keypair: CoconutKeyPair,
identity_key: identity::PublicKey,
rng: R,
shutdown: &TaskManager,
) -> Result<()>
@@ -214,7 +219,8 @@ impl<R: RngCore + CryptoRng + Clone> DkgController<R> {
R: Sync + Send + 'static,
{
let shutdown_listener = shutdown.subscribe();
let dkg_controller = DkgController::new(config, nyxd_client, coconut_keypair, rng).await?;
let dkg_controller =
DkgController::new(config, nyxd_client, coconut_keypair, identity_key, rng).await?;
tokio::spawn(async move { dkg_controller.run(shutdown_listener).await });
Ok(())
}
+89 -29
View File
@@ -1,13 +1,12 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::dkg;
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::state::{ConsistentState, State};
use crate::coconut::error::CoconutError;
use log::debug;
use nym_coconut_dkg_common::types::TOTAL_DEALINGS;
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_dkg::bte::setup;
use nym_coconut_dkg_common::types::{ContractDealing, PartialContractDealing};
use nym_dkg::Dealing;
use rand::RngCore;
use std::collections::VecDeque;
@@ -24,6 +23,11 @@ pub(crate) async fn dealing_exchange(
return Ok(());
}
let contract_state = dkg_client.get_contract_state().await?;
let expected_key_size = contract_state.key_size;
let epoch_id = dkg_client.get_current_epoch().await?.epoch_id;
let dealers = dkg_client.get_current_dealers().await?;
let threshold = dkg_client.get_current_epoch_threshold().await?;
let initial_dealers = dkg_client
@@ -44,7 +48,7 @@ pub(crate) async fn dealing_exchange(
// Double check that we are in resharing mode
if resharing {
let sk = keypair.secret_key();
if sk.size() + 1 != TOTAL_DEALINGS {
if sk.size() + 1 != expected_key_size as usize {
return Err(CoconutError::CorruptedCoconutKeyPair);
}
@@ -64,23 +68,54 @@ pub(crate) async fn dealing_exchange(
};
let mut prior_resharing_secrets = VecDeque::from(prior_resharing_secrets);
if !resharing || initial_dealers.iter().any(|d| *d == own_address) {
let params = setup();
for _ in 0..TOTAL_DEALINGS {
let params = dkg::params();
for dealing_index in 0..expected_key_size {
debug!(
"dealing {dealing_index} ({} out of {expected_key_size})",
dealing_index + 1
);
// see if we have already submitted this one (we might have crashed)
if dkg_client
.get_dealing_status(epoch_id, dealing_index)
.await?
{
warn!("we have already submitted dealing {dealing_index} before - we probably crashed!");
continue;
}
// see if we have already generated, but not submitted this one before (we might have crashed or validator might have had a problem)
let contract_dealing = if let Some(prior_dealing) =
state.get_dealing(epoch_id, dealing_index)
{
warn!("we have already generated dealing {dealing_index} before, but failed to submit it");
PartialContractDealing::new(dealing_index, ContractDealing::from(prior_dealing))
} else {
// generate fresh dealing
let (dealing, _) = Dealing::create(
rng.clone(),
params,
dealer_index,
state.threshold()?,
&receivers,
prior_resharing_secrets.pop_front(),
);
let contract_dealing =
PartialContractDealing::new(dealing_index, ContractDealing::from(&dealing));
state.store_dealing(epoch_id, dealing_index, dealing);
contract_dealing
};
debug!(
"Submitting dealing for indexes {:?} with resharing: {}",
receivers.keys().collect::<Vec<_>>(),
prior_resharing_secrets.front().is_some()
);
let (dealing, _) = Dealing::create(
rng.clone(),
&params,
dealer_index,
state.threshold()?,
&receivers,
prior_resharing_secrets.pop_front(),
);
dkg_client
.submit_dealing(ContractSafeBytes::from(&dealing), resharing)
.submit_dealing(contract_dealing, resharing)
.await?;
}
} else {
@@ -104,10 +139,12 @@ pub(crate) mod tests {
use nym_coconut::{ttp_keygen, Parameters};
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::types::InitialReplacementData;
use nym_crypto::asymmetric::identity;
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
use nym_dkg::bte::{Params, PublicKeyWithProof};
use nym_validator_client::nyxd::AccountId;
use rand::rngs::OsRng;
use rand_07::thread_rng;
use std::collections::HashMap;
use std::path::PathBuf;
use std::str::FromStr;
@@ -128,6 +165,8 @@ pub(crate) mod tests {
let mut keypairs = vec![];
for (idx, addr) in TEST_VALIDATORS_ADDRESS.iter().enumerate() {
let keypair = DkgKeyPair::new(params, OsRng);
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let bte_public_key_with_proof =
bs58::encode(&keypair.public_key().to_bytes()).into_string();
keypairs.push(keypair);
@@ -137,6 +176,7 @@ pub(crate) mod tests {
DealerDetails {
address: Addr::unchecked(*addr),
bte_public_key_with_proof,
ed25519_identity: identity_keypair.public_key().to_base58_string(),
announce_address: format!("localhost:80{}", idx),
assigned_index: (idx + 1) as u64,
},
@@ -160,16 +200,20 @@ pub(crate) mod tests {
.with_dealings(&dealings_db)
.with_threshold(&threshold_db),
);
let params = setup();
let params = dkg::params();
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&params, OsRng),
DkgKeyPair::new(params, OsRng),
*identity_keypair.public_key(),
KeyPair::new(),
);
state.set_node_index(Some(self_index));
let keypairs = insert_dealers(&params, &dealer_details_db);
let keypairs = insert_dealers(params, &dealer_details_db);
let contract_state = dkg_client.get_contract_state().await.unwrap();
dealing_exchange(&dkg_client, &mut state, OsRng, false)
.await
@@ -187,10 +231,12 @@ pub(crate) mod tests {
let dealings = dealings_db
.read()
.unwrap()
.get(&0)
.unwrap()
.get(TEST_VALIDATORS_ADDRESS[0])
.unwrap()
.clone();
assert_eq!(dealings.len(), TOTAL_DEALINGS);
assert_eq!(dealings.len(), contract_state.key_size as usize);
dealing_exchange(&dkg_client, &mut state, OsRng, false)
.await
@@ -198,6 +244,8 @@ pub(crate) mod tests {
let new_dealings = dealings_db
.read()
.unwrap()
.get(&0)
.unwrap()
.get(TEST_VALIDATORS_ADDRESS[0])
.unwrap()
.clone();
@@ -217,16 +265,18 @@ pub(crate) mod tests {
.with_dealings(&dealings_db)
.with_threshold(&threshold_db),
);
let params = setup();
let params = dkg::params();
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&params, OsRng),
DkgKeyPair::new(params, OsRng),
*identity_keypair.public_key(),
KeyPair::new(),
);
state.set_node_index(Some(self_index));
insert_dealers(&params, &dealer_details_db);
insert_dealers(params, &dealer_details_db);
dealer_details_db
.write()
@@ -285,20 +335,24 @@ pub(crate) mod tests {
.with_threshold(&threshold_db)
.with_initial_dealers_db(&initial_dealers_db),
);
let params = setup();
let contract_state = dkg_client.get_contract_state().await.unwrap();
let params = dkg::params();
let mut keys = ttp_keygen(&Parameters::new(4).unwrap(), 3, 4).unwrap();
let coconut_keypair = KeyPair::new();
coconut_keypair.set(Some(keys.pop().unwrap())).await;
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&params, OsRng),
DkgKeyPair::new(params, OsRng),
*identity_keypair.public_key(),
coconut_keypair.clone(),
);
state.set_node_index(Some(self_index));
let keypairs = insert_dealers(&params, &dealer_details_db);
let keypairs = insert_dealers(params, &dealer_details_db);
dealing_exchange(&dkg_client, &mut state, OsRng, true)
.await
@@ -313,14 +367,18 @@ pub(crate) mod tests {
);
assert_eq!(state.threshold().unwrap(), 3);
assert_eq!(state.receiver_index().unwrap(), 1);
let addr = dkg_client.get_address().await;
assert!(dealings_db.read().unwrap().get(addr.as_ref()).is_none());
// let addr = dkg_client.get_address().await;
// no dealings submitted for the first (zeroth) epoch
assert!(dealings_db.read().unwrap().get(&0).is_none());
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&params, OsRng),
DkgKeyPair::new(params, OsRng),
*identity_keypair.public_key(),
coconut_keypair,
);
state.set_node_index(Some(self_index));
@@ -340,9 +398,11 @@ pub(crate) mod tests {
let dealings = dealings_db
.read()
.unwrap()
.get(&0)
.unwrap()
.get(TEST_VALIDATORS_ADDRESS[0])
.unwrap()
.clone();
assert_eq!(dealings.len(), TOTAL_DEALINGS);
assert_eq!(dealings.len(), contract_state.key_size as usize);
}
}
+7
View File
@@ -1,6 +1,13 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::sync::OnceLock;
pub(crate) fn params() -> &'static nym_dkg::bte::Params {
static PARAMS: OnceLock<nym_dkg::bte::Params> = OnceLock::new();
PARAMS.get_or_init(nym_dkg::bte::setup)
}
pub(crate) mod client;
pub(crate) mod complaints;
pub(crate) mod controller;
+16 -2
View File
@@ -38,7 +38,12 @@ pub(crate) async fn public_key_submission(
// If it was a dealer in a previous epoch, re-register it for this epoch
debug!("Registering for the current DKG round, with keys from a previous epoch");
dkg_client
.register_dealer(bte_key, state.announce_address().to_string(), resharing)
.register_dealer(
bte_key,
state.identity_key().to_base58_string(),
state.announce_address().to_string(),
resharing,
)
.await?;
}
details.assigned_index
@@ -46,7 +51,12 @@ pub(crate) async fn public_key_submission(
debug!("Registering for the first time to be a dealer");
// First time registration
dkg_client
.register_dealer(bte_key, state.announce_address().to_string(), resharing)
.register_dealer(
bte_key,
state.identity_key().to_base58_string(),
state.announce_address().to_string(),
resharing,
)
.await?
};
state.set_node_index(Some(index));
@@ -61,9 +71,11 @@ pub(crate) mod tests {
use crate::coconut::dkg::state::PersistentState;
use crate::coconut::tests::DummyClient;
use crate::coconut::KeyPair;
use nym_crypto::asymmetric::identity;
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
use nym_validator_client::nyxd::AccountId;
use rand::rngs::OsRng;
use rand_07::thread_rng;
use std::path::PathBuf;
use std::str::FromStr;
use url::Url;
@@ -76,11 +88,13 @@ pub(crate) mod tests {
let dkg_client = DkgClient::new(DummyClient::new(
AccountId::from_str(TEST_VALIDATOR_ADDRESS).unwrap(),
));
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let mut state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
DkgKeyPair::new(&nym_dkg::bte::setup(), OsRng),
*identity_keypair.public_key(),
KeyPair::new(),
);
+78 -6
View File
@@ -7,12 +7,13 @@ use crate::coconut::keypair::KeyPair as CoconutKeyPair;
use cosmwasm_std::Addr;
use log::debug;
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::types::EpochState;
use nym_coconut_dkg_common::types::{DealingIndex, EpochId, EpochState};
use nym_crypto::asymmetric::identity;
use nym_dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof};
use nym_dkg::{NodeIndex, RecoveredVerificationKeys, Threshold};
use nym_dkg::{Dealing, NodeIndex, RecoveredVerificationKeys, Threshold};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use url::Url;
@@ -159,10 +160,54 @@ where
.collect()
}
mod generated_dealings {
use nym_coconut_dkg_common::types::{DealingIndex, EpochId};
use nym_dkg::Dealing;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
type Helper = HashMap<EpochId, HashMap<DealingIndex, Vec<u8>>>;
pub fn serialize<S: Serializer>(
dealings: &HashMap<EpochId, HashMap<DealingIndex, Dealing>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut helper = HashMap::new();
for (epoch, dealings) in dealings {
let mut inner = HashMap::new();
for (dealing_index, dealing) in dealings {
inner.insert(*dealing_index, dealing.to_bytes());
}
helper.insert(*epoch, inner);
}
helper.serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<HashMap<EpochId, HashMap<DealingIndex, Dealing>>, D::Error> {
let helper = <Helper>::deserialize(deserializer)?;
let mut epoch_dealings = HashMap::with_capacity(helper.len());
for (epoch, dealings) in helper {
let mut inner = HashMap::with_capacity(dealings.len());
for (dealing_index, raw_dealing) in dealings {
let dealing =
Dealing::try_from_bytes(&raw_dealing).map_err(serde::de::Error::custom)?;
inner.insert(dealing_index, dealing);
}
epoch_dealings.insert(epoch, inner);
}
Ok(epoch_dealings)
}
}
#[derive(Default, Deserialize, Serialize)]
pub(crate) struct PersistentState {
node_index: Option<NodeIndex>,
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
#[serde(with = "generated_dealings")]
generated_dealings: HashMap<EpochId, HashMap<DealingIndex, Dealing>>,
receiver_index: Option<usize>,
threshold: Option<Threshold>,
#[serde(serialize_with = "vks_serialize")]
@@ -179,6 +224,7 @@ impl From<&State> for PersistentState {
PersistentState {
node_index: s.node_index,
dealers: s.dealers.clone(),
generated_dealings: s.generated_dealings.clone(),
receiver_index: s.receiver_index,
threshold: s.threshold,
recovered_vks: s.recovered_vks.clone(),
@@ -204,10 +250,12 @@ impl PersistentState {
pub(crate) struct State {
persistent_state_path: PathBuf,
announce_address: Url,
identity_key: identity::PublicKey,
dkg_keypair: DkgKeyPair,
coconut_keypair: CoconutKeyPair,
node_index: Option<NodeIndex>,
dealers: BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>>,
generated_dealings: HashMap<EpochId, HashMap<DealingIndex, Dealing>>,
receiver_index: Option<usize>,
threshold: Option<Threshold>,
recovered_vks: Vec<RecoveredVerificationKeys>,
@@ -223,15 +271,18 @@ impl State {
persistent_state: PersistentState,
announce_address: Url,
dkg_keypair: DkgKeyPair,
identity_key: identity::PublicKey,
coconut_keypair: CoconutKeyPair,
) -> Self {
State {
persistent_state_path,
announce_address,
identity_key,
dkg_keypair,
coconut_keypair,
node_index: persistent_state.node_index,
dealers: persistent_state.dealers,
generated_dealings: persistent_state.generated_dealings,
receiver_index: persistent_state.receiver_index,
threshold: persistent_state.threshold,
recovered_vks: persistent_state.recovered_vks,
@@ -265,6 +316,10 @@ impl State {
&self.announce_address
}
pub fn identity_key(&self) -> identity::PublicKey {
self.identity_key
}
pub fn dkg_keypair(&self) -> &DkgKeyPair {
&self.dkg_keypair
}
@@ -277,6 +332,24 @@ impl State {
self.coconut_keypair.take().await
}
pub fn get_dealing(&self, epoch_id: EpochId, dealing_index: DealingIndex) -> Option<&Dealing> {
self.generated_dealings
.get(&epoch_id)
.and_then(|epoch_dealings| epoch_dealings.get(&dealing_index))
}
pub fn store_dealing(
&mut self,
epoch_id: EpochId,
dealing_index: DealingIndex,
dealing: Dealing,
) {
self.generated_dealings
.entry(epoch_id)
.or_default()
.insert(dealing_index, dealing);
}
#[cfg(test)]
pub async fn coconut_keypair(
&self,
@@ -304,6 +377,7 @@ impl State {
.collect()
}
// FIXME: BUG: if we remove dealers, we won't be able to verify shares of other parties...
pub fn current_dealers_by_idx(&self) -> BTreeMap<NodeIndex, PublicKey> {
self.dealers
.iter()
@@ -364,8 +438,7 @@ impl State {
.find(|(addr, _)| *addr == dealer_addr)
{
debug!(
"Dealer {} misbehaved: {:?}. It will be marked locally as bad dealer and ignored",
dealer_addr, reason
"Dealer {dealer_addr} misbehaved: {reason:?}. It will be marked locally as bad dealer and ignored",
);
*value = Err(reason);
}
@@ -395,7 +468,6 @@ impl State {
self.was_in_progress = true;
}
#[cfg(test)]
pub fn all_dealers(&self) -> &BTreeMap<Addr, Result<DkgParticipant, ComplaintReason>> {
&self.dealers
}
+163 -77
View File
@@ -1,6 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::coconut::dkg;
use crate::coconut::dkg::client::DkgClient;
use crate::coconut::dkg::complaints::ComplaintReason;
use crate::coconut::dkg::state::{ConsistentState, State};
@@ -13,24 +14,30 @@ use log::debug;
use nym_coconut::tests::helpers::transpose_matrix;
use nym_coconut::{check_vk_pairing, Base58, KeyPair, SecretKey, VerificationKey};
use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID;
use nym_coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS};
use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
use nym_coconut_dkg_common::verification_key::owner_from_cosmos_msgs;
use nym_coconut_interface::KeyPair as CoconutKeyPair;
use nym_dkg::bte::{decrypt_share, setup};
use nym_dkg::bte::decrypt_share;
use nym_dkg::error::DkgError;
use nym_dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold};
use nym_pemstore::KeyPairPath;
use nym_validator_client::nyxd::cosmwasm_client::logs::find_attribute;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
// Filter the dealers based on what dealing they posted (or not) in the contract
// TODO: change the return type to make sure that:
// - each entry has the same number of dealings
// - dealer data is not duplicated
// - each dealer has submitted all or nothing
async fn deterministic_filter_dealers(
dkg_client: &DkgClient,
state: &mut State,
epoch_id: EpochId,
threshold: Threshold,
resharing: bool,
) -> Result<Vec<BTreeMap<NodeIndex, (Addr, Dealing)>>, CoconutError> {
let mut dealings_maps = vec![];
let mut dealings_maps = Vec::new();
let initial_dealers_by_addr = state.current_dealers_by_addr();
let initial_receivers = state.current_dealers_by_idx();
let initial_resharing_dealers = if resharing {
@@ -43,36 +50,46 @@ async fn deterministic_filter_dealers(
vec![]
};
let params = setup();
let params = dkg::params();
for idx in 0..TOTAL_DEALINGS {
let dealings = dkg_client.get_dealings(idx).await?;
// note: this is a temporary solution to replicate the behaviour of the old code so that I wouldn't need to
// fix the filtering in this PR, because the old code is quite buggy and misses few edge cases
let mut raw_dealings = HashMap::new();
for dealer in state.all_dealers().keys() {
let dealer_dealings = dkg_client
.get_dealings(epoch_id, dealer.to_string())
.await?;
for dealing in dealer_dealings {
let old_contract_dealing = raw_dealings.entry(dealing.index).or_insert(Vec::new());
old_contract_dealing.push((dealer.clone(), dealing.data))
}
}
// this is a temporary thing to reintroduce the bug to make sure tests still pass : )
// i will fix it properly in next PR
for dealing_index in 0..5 {
let dealings = raw_dealings.remove(&dealing_index).unwrap_or_default();
let dealings_map =
BTreeMap::from_iter(dealings.into_iter().filter_map(|contract_dealing| {
match Dealing::try_from(&contract_dealing.dealing) {
BTreeMap::from_iter(dealings.into_iter().filter_map(|(dealer, dealing)| {
match Dealing::try_from(&dealing) {
Ok(dealing) => {
if dealing
.verify(&params, threshold, &initial_receivers, None)
.verify(params, threshold, &initial_receivers, None)
.is_err()
{
state.mark_bad_dealer(
&contract_dealing.dealer,
&dealer,
ComplaintReason::DealingVerificationError,
);
None
} else if let Some(idx) =
initial_dealers_by_addr.get(&contract_dealing.dealer)
{
Some((*idx, (contract_dealing.dealer, dealing)))
} else {
None
initial_dealers_by_addr
.get(&dealer)
.map(|idx| (*idx, (dealer, dealing)))
}
}
Err(_) => {
state.mark_bad_dealer(
&contract_dealing.dealer,
ComplaintReason::MalformedDealing,
);
state.mark_bad_dealer(&dealer, ComplaintReason::MalformedDealing);
None
}
}
@@ -80,6 +97,40 @@ async fn deterministic_filter_dealers(
dealings_maps.push(dealings_map);
}
//
//
// for dealer in initial_dealers_by_addr.keys() {
// let Some(dealer_index) = initial_dealers_by_addr.get(dealer) else {
// warn!("could not obtain dealer index of {dealer}");
// continue;
// };
//
// let dealer_dealings = dkg_client
// .get_dealings(epoch_id, dealer.to_string())
// .await?;
//
// for contract_dealing in dealer_dealings {
// match Dealing::try_from(&contract_dealing.data) {
// // FIXME: bug: this doesn't check resharing
// Ok(dealing) => {
// if let Err(err) = dealing.verify(params, threshold, &initial_receivers, None) {
// println!("dealing verification failure from {dealer}: {err}");
// state.mark_bad_dealer(dealer, ComplaintReason::DealingVerificationError);
// } else {
// let entry = dealings_maps
// .entry(contract_dealing.index)
// .or_insert(BTreeMap::new());
// entry.insert(*dealer_index, (dealer.clone(), dealing));
// }
// }
// Err(err) => {
// warn!("malformed dealing from {dealer}: {err}");
// state.mark_bad_dealer(dealer, ComplaintReason::MalformedDealing);
// }
// }
// }
// }
for (addr, _) in initial_dealers_by_addr.iter() {
// in resharing mode, we don't commit dealings from dealers outside the initial set
if !resharing || initial_resharing_dealers.contains(addr) {
@@ -155,6 +206,7 @@ fn derive_partial_keypair(
pub(crate) async fn verification_key_submission(
dkg_client: &DkgClient,
state: &mut State,
epoch_id: EpochId,
keypair_path: &KeyPairPath,
resharing: bool,
) -> Result<(), CoconutError> {
@@ -165,7 +217,7 @@ pub(crate) async fn verification_key_submission(
let threshold = state.threshold()?;
let dealings_maps =
deterministic_filter_dealers(dkg_client, state, threshold, resharing).await?;
deterministic_filter_dealers(dkg_client, state, epoch_id, threshold, resharing).await?;
debug!(
"Filtered dealers to {:?}",
dealings_maps[0].keys().collect::<Vec<_>>()
@@ -307,13 +359,14 @@ pub(crate) mod tests {
use crate::coconut::KeyPair;
use nym_coconut::aggregate_verification_keys;
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::types::InitialReplacementData;
use nym_coconut_dkg_common::types::{EpochId, InitialReplacementData, PartialContractDealing};
use nym_coconut_dkg_common::verification_key::ContractVKShare;
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_crypto::asymmetric::identity;
use nym_dkg::bte::keys::KeyPair as DkgKeyPair;
use nym_validator_client::nyxd::AccountId;
use rand::rngs::OsRng;
use rand::Rng;
use rand_07::thread_rng;
use std::collections::HashMap;
use std::env::temp_dir;
use std::path::PathBuf;
@@ -323,7 +376,9 @@ pub(crate) mod tests {
struct MockContractDb {
dealer_details_db: Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
dealings_db: Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
// it's a really bad practice, but I'm not going to be changing it now...
#[allow(clippy::type_complexity)]
dealings_db: Arc<RwLock<HashMap<EpochId, HashMap<String, Vec<PartialContractDealing>>>>>,
proposal_db: Arc<RwLock<HashMap<u64, ProposalResponse>>>,
verification_share_db: Arc<RwLock<HashMap<String, ContractVKShare>>>,
threshold_db: Arc<RwLock<Option<Threshold>>>,
@@ -351,8 +406,9 @@ pub(crate) mod tests {
];
async fn prepare_clients_and_states(db: &MockContractDb) -> Vec<(DkgClient, State)> {
let params = setup();
let params = dkg::params();
let mut clients_and_states = vec![];
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
for addr in TEST_VALIDATORS_ADDRESS {
let dkg_client = DkgClient::new(
@@ -364,12 +420,13 @@ pub(crate) mod tests {
.with_threshold(&db.threshold_db)
.with_initial_dealers_db(&db.initial_dealers_db),
);
let keypair = DkgKeyPair::new(&params, OsRng);
let keypair = DkgKeyPair::new(params, OsRng);
let state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
*identity_keypair.public_key(),
KeyPair::new(),
);
clients_and_states.push((dkg_client, state));
@@ -403,7 +460,7 @@ pub(crate) mod tests {
let private_key_path = temp_dir().join(format!("private{}.pem", random_file));
let public_key_path = temp_dir().join(format!("public{}.pem", random_file));
let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone());
verification_key_submission(dkg_client, state, &keypair_path, false)
verification_key_submission(dkg_client, state, 0, &keypair_path, false)
.await
.unwrap();
std::fs::remove_file(private_key_path).unwrap();
@@ -441,11 +498,13 @@ pub(crate) mod tests {
async fn check_dealers_filter_all_good() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
for (dkg_client, state) in clients_and_states.iter_mut() {
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
for mapping in filtered.iter() {
assert_eq!(mapping.len(), 4);
}
@@ -457,23 +516,27 @@ pub(crate) mod tests {
async fn check_dealers_filter_one_bad_dealing() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
// corrupt just one dealing
db.dealings_db
.write()
.unwrap()
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.and_modify(|dealings| {
let mut last = dealings.pop().unwrap();
last.0.pop();
dealings.push(last);
.entry(0)
.and_modify(|epoch_dealings| {
let validator_dealings = epoch_dealings
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.or_default();
let mut last = validator_dealings.pop().unwrap();
last.data.0.pop();
validator_dealings.push(last);
});
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
let corrupted_status = state
.all_dealers()
.get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0]))
@@ -489,6 +552,7 @@ pub(crate) mod tests {
async fn check_dealers_resharing_filter_one_missing_dealing() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
// add all but the first dealing
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
@@ -502,10 +566,10 @@ pub(crate) mod tests {
initial_dealers: vec![Addr::unchecked(TEST_VALIDATORS_ADDRESS[0])],
initial_height: 1,
});
let filtered = deterministic_filter_dealers(dkg_client, state, 2, true)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, true)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
let corrupted_status = state
.all_dealers()
.get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0]))
@@ -522,6 +586,7 @@ pub(crate) mod tests {
async fn check_dealers_resharing_filter_one_noninitial_missing_dealing() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
// add all but the first dealing
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
@@ -535,10 +600,10 @@ pub(crate) mod tests {
initial_dealers: vec![],
initial_height: 1,
});
let filtered = deterministic_filter_dealers(dkg_client, state, 2, true)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, true)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
assert!(state
.all_dealers()
.get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0]))
@@ -553,23 +618,27 @@ pub(crate) mod tests {
async fn check_dealers_filter_all_bad_dealings() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
// corrupt all dealings of one address
db.dealings_db
.write()
.unwrap()
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.and_modify(|dealings| {
dealings.iter_mut().for_each(|dealing| {
dealing.0.pop();
.entry(0)
.and_modify(|epoch_dealings| {
let validator_dealings = epoch_dealings
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.or_default();
validator_dealings.iter_mut().for_each(|dealing| {
dealing.data.0.pop();
});
});
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
for mapping in filtered.iter() {
assert_eq!(mapping.len(), 3);
}
@@ -588,28 +657,32 @@ pub(crate) mod tests {
async fn check_dealers_filter_malformed_dealing() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
// corrupt just one dealing
db.dealings_db
.write()
.unwrap()
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.and_modify(|dealings| {
let mut last = dealings.pop().unwrap();
last.0.pop();
dealings.push(last);
.entry(0)
.and_modify(|epoch_dealings| {
let validator_dealings = epoch_dealings
.get_mut(TEST_VALIDATORS_ADDRESS[0])
.expect("no dealing");
let mut last = validator_dealings.pop().unwrap();
last.data.0.pop();
validator_dealings.push(last);
});
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
deterministic_filter_dealers(dkg_client, state, 2, false)
deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
// second filter will leave behind the bad dealer and surface why it was left out
// in the first place
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
let corrupted_status = state
.all_dealers()
.get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0]))
@@ -625,33 +698,37 @@ pub(crate) mod tests {
async fn check_dealers_filter_dealing_verification_error() {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await;
let contract_state = clients_and_states[0].0.get_contract_state().await.unwrap();
// corrupt just one dealing
db.dealings_db
.write()
.unwrap()
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.and_modify(|dealings| {
let mut last = dealings.pop().unwrap();
let value = last.0.pop().unwrap();
.entry(0)
.and_modify(|epoch_dealings| {
let validator_dealings = epoch_dealings
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.or_default();
let mut last = validator_dealings.pop().unwrap();
let value = last.data.0.pop().unwrap();
if value == 42 {
last.0.push(43);
last.data.0.push(43);
} else {
last.0.push(42);
last.data.0.push(42);
}
dealings.push(last);
validator_dealings.push(last);
});
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
deterministic_filter_dealers(dkg_client, state, 2, false)
deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
// second filter will leave behind the bad dealer and surface why it was left out
// in the first place
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert_eq!(filtered.len(), TOTAL_DEALINGS);
assert_eq!(filtered.len(), contract_state.key_size as usize);
let corrupted_status = state
.all_dealers()
.get(&Addr::unchecked(TEST_VALIDATORS_ADDRESS[0]))
@@ -668,7 +745,7 @@ pub(crate) mod tests {
let db = MockContractDb::new();
let mut clients_and_states = prepare_clients_and_states_with_dealing(&db).await;
for (dkg_client, state) in clients_and_states.iter_mut() {
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert!(derive_partial_keypair(state, 2, filtered).is_ok());
@@ -685,15 +762,18 @@ pub(crate) mod tests {
db.dealings_db
.write()
.unwrap()
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.and_modify(|dealings| {
let mut last = dealings.pop().unwrap();
last.0.pop();
dealings.push(last);
.entry(0)
.and_modify(|epoch_dealings| {
let validator_dealings = epoch_dealings
.entry(TEST_VALIDATORS_ADDRESS[0].to_string())
.or_default();
let mut last = validator_dealings.pop().unwrap();
last.data.0.pop();
validator_dealings.push(last);
});
for (dkg_client, state) in clients_and_states.iter_mut().skip(1) {
let filtered = deterministic_filter_dealers(dkg_client, state, 2, false)
let filtered = deterministic_filter_dealers(dkg_client, state, 0, 2, false)
.await
.unwrap();
assert!(derive_partial_keypair(state, 2, filtered).is_ok());
@@ -863,12 +943,14 @@ pub(crate) mod tests {
.with_threshold(&db.threshold_db)
.with_initial_dealers_db(&db.initial_dealers_db),
);
let keypair = DkgKeyPair::new(&setup(), OsRng);
let keypair = DkgKeyPair::new(dkg::params(), OsRng);
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
*identity_keypair.public_key(),
KeyPair::new(),
);
@@ -906,7 +988,7 @@ pub(crate) mod tests {
let private_key_path = temp_dir().join(format!("private{}.pem", random_file));
let public_key_path = temp_dir().join(format!("public{}.pem", random_file));
let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone());
verification_key_submission(dkg_client, state, &keypair_path, true)
verification_key_submission(dkg_client, state, 0, &keypair_path, true)
.await
.unwrap();
std::fs::remove_file(private_key_path).unwrap();
@@ -967,12 +1049,14 @@ pub(crate) mod tests {
.with_threshold(&db.threshold_db)
.with_initial_dealers_db(&db.initial_dealers_db),
);
let keypair = DkgKeyPair::new(&setup(), OsRng);
let keypair = DkgKeyPair::new(dkg::params(), OsRng);
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let state = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
*identity_keypair.public_key(),
KeyPair::new(),
);
let new_dkg_client2 = DkgClient::new(
@@ -986,12 +1070,14 @@ pub(crate) mod tests {
.with_threshold(&db.threshold_db)
.with_initial_dealers_db(&db.initial_dealers_db),
);
let keypair = DkgKeyPair::new(&setup(), OsRng);
let keypair = DkgKeyPair::new(dkg::params(), OsRng);
let identity_keypair = identity::KeyPair::new(&mut thread_rng());
let state2 = State::new(
PathBuf::default(),
PersistentState::default(),
Url::parse("localhost:8000").unwrap(),
keypair,
*identity_keypair.public_key(),
KeyPair::new(),
);
@@ -1022,7 +1108,7 @@ pub(crate) mod tests {
let private_key_path = temp_dir().join(format!("private{}.pem", random_file));
let public_key_path = temp_dir().join(format!("public{}.pem", random_file));
let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone());
verification_key_submission(dkg_client, state, &keypair_path, false)
verification_key_submission(dkg_client, state, 0, &keypair_path, false)
.await
.unwrap();
std::fs::remove_file(private_key_path).unwrap();
@@ -1098,7 +1184,7 @@ pub(crate) mod tests {
let private_key_path = temp_dir().join(format!("private{}.pem", random_file));
let public_key_path = temp_dir().join(format!("public{}.pem", random_file));
let keypair_path = KeyPairPath::new(private_key_path.clone(), public_key_path.clone());
verification_key_submission(dkg_client, state, &keypair_path, true)
verification_key_submission(dkg_client, state, 0, &keypair_path, true)
.await
.unwrap();
std::fs::remove_file(private_key_path).unwrap();
+5
View File
@@ -124,6 +124,11 @@ pub enum CoconutError {
// I guess we should make this one a bit more detailed
#[error("the provided query arguments were invalid")]
InvalidQueryArguments,
#[error("insufficient number of dealings provided to derive the key")]
InsufficientDealings {
// TODO: details
},
}
impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError {
+57 -23
View File
@@ -8,7 +8,7 @@ use crate::support::storage::NymApiStorage;
use async_trait::async_trait;
use cosmwasm_std::{coin, to_binary, Addr, CosmosMsg, Decimal, WasmMsg};
use cw3::ProposalResponse;
use cw4::MemberResponse;
use cw4::{Cw4Contract, MemberResponse};
use nym_api_requests::coconut::models::{IssuedCredentialBody, IssuedCredentialResponse};
use nym_api_requests::coconut::{
BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse,
@@ -23,16 +23,17 @@ use nym_coconut_bandwidth_contract_common::spend_credential::{
SpendCredential, SpendCredentialResponse,
};
use nym_coconut_dkg_common::dealer::{
ContractDealing, DealerDetails, DealerDetailsResponse, DealerType,
DealerDetails, DealerDetailsResponse, DealerType, DealingStatusResponse,
};
use nym_coconut_dkg_common::event_attributes::{DKG_PROPOSAL_ID, NODE_INDEX};
use nym_coconut_dkg_common::types::{
EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData, TOTAL_DEALINGS,
DealingIndex, EncodedBTEPublicKeyWithProof, Epoch, EpochId, InitialReplacementData,
PartialContractDealing, State as ContractState,
};
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_coconut_interface::{hash_to_scalar, Credential, VerificationKey};
use nym_config::defaults::VOUCHER_INFO;
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_contracts_common::IdentityKey;
use nym_credentials::coconut::bandwidth::BandwidthVoucher;
use nym_crypto::asymmetric::{encryption, identity};
use nym_dkg::Threshold;
@@ -67,9 +68,12 @@ pub(crate) struct DummyClient {
spent_credential_db: Arc<RwLock<HashMap<String, SpendCredentialResponse>>>,
epoch: Arc<RwLock<Epoch>>,
contract_state: Arc<RwLock<ContractState>>,
dealer_details: Arc<RwLock<HashMap<String, (DealerDetails, bool)>>>,
threshold: Arc<RwLock<Option<Threshold>>>,
dealings: Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
// it's a really bad practice, but I'm not going to be changing it now...
#[allow(clippy::type_complexity)]
dealings: Arc<RwLock<HashMap<EpochId, HashMap<String, Vec<PartialContractDealing>>>>>,
verification_share: Arc<RwLock<HashMap<String, ContractVKShare>>>,
group_db: Arc<RwLock<HashMap<String, MemberResponse>>>,
initial_dealers_db: Arc<RwLock<Option<InitialReplacementData>>>,
@@ -83,6 +87,12 @@ impl DummyClient {
proposal_db: Arc::new(RwLock::new(HashMap::new())),
spent_credential_db: Arc::new(RwLock::new(HashMap::new())),
epoch: Arc::new(RwLock::new(Epoch::default())),
contract_state: Arc::new(RwLock::new(ContractState {
mix_denom: TEST_COIN_DENOM.to_string(),
multisig_addr: Addr::unchecked("dummy address"),
group_addr: Cw4Contract::new(Addr::unchecked("dummy cw4")),
key_size: 5,
})),
dealer_details: Arc::new(RwLock::new(HashMap::new())),
threshold: Arc::new(RwLock::new(None)),
dealings: Arc::new(RwLock::new(HashMap::new())),
@@ -131,9 +141,11 @@ impl DummyClient {
self
}
// it's a really bad practice, but I'm not going to be changing it now...
#[allow(clippy::type_complexity)]
pub fn with_dealings(
mut self,
dealings: &Arc<RwLock<HashMap<String, Vec<ContractSafeBytes>>>>,
dealings: &Arc<RwLock<HashMap<EpochId, HashMap<String, Vec<PartialContractDealing>>>>>,
) -> Self {
self.dealings = Arc::clone(dealings);
self
@@ -203,6 +215,10 @@ impl super::client::Client for DummyClient {
})
}
async fn contract_state(&self) -> Result<ContractState> {
Ok(self.contract_state.read().unwrap().clone())
}
async fn get_current_epoch(&self) -> Result<Epoch> {
Ok(*self.epoch.read().unwrap())
}
@@ -248,6 +264,21 @@ impl super::client::Client for DummyClient {
})
}
async fn get_dealing_status(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> crate::coconut::error::Result<DealingStatusResponse> {
let dealings = self.get_dealings(epoch_id, &dealer).await?;
Ok(DealingStatusResponse {
epoch_id,
dealer: Addr::unchecked(dealer),
dealing_index,
dealing_submitted: dealings.get(dealing_index as usize).is_some(),
})
}
async fn get_current_dealers(&self) -> Result<Vec<DealerDetails>> {
Ok(self
.dealer_details
@@ -259,17 +290,21 @@ impl super::client::Client for DummyClient {
.collect())
}
async fn get_dealings(&self, idx: usize) -> Result<Vec<ContractDealing>> {
async fn get_dealings(
&self,
epoch_id: EpochId,
dealer: &str,
) -> Result<Vec<PartialContractDealing>> {
Ok(self
.dealings
.read()
.unwrap()
.iter()
.map(|(dealer, dealings)| ContractDealing {
dealing: dealings.get(idx).unwrap().clone(),
dealer: Addr::unchecked(dealer),
})
.collect())
.get(&epoch_id)
.cloned()
.unwrap_or_default()
.get(dealer)
.cloned()
.unwrap_or_default())
}
async fn get_verification_key_shares(
@@ -322,6 +357,7 @@ impl super::client::Client for DummyClient {
async fn register_dealer(
&self,
bte_public_key_with_proof: EncodedBTEPublicKeyWithProof,
identity_key: IdentityKey,
announce_address: String,
_resharing: bool,
) -> Result<ExecuteResult> {
@@ -345,6 +381,7 @@ impl super::client::Client for DummyClient {
DealerDetails {
address: Addr::unchecked(self.validator_address.to_string()),
bte_public_key_with_proof,
ed25519_identity: identity_key,
announce_address,
assigned_index,
},
@@ -367,19 +404,16 @@ impl super::client::Client for DummyClient {
async fn submit_dealing(
&self,
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
_resharing: bool,
) -> Result<ExecuteResult> {
self.dealings
.write()
.unwrap()
let current_epoch = self.epoch.read().unwrap().epoch_id;
let mut guard = self.dealings.write().unwrap();
let epoch_dealings = guard.entry(current_epoch).or_default();
let existing_dealings = epoch_dealings
.entry(self.validator_address.to_string())
.and_modify(|v| {
if v.len() < TOTAL_DEALINGS {
v.push(dealing_bytes.clone())
}
})
.or_insert_with(|| vec![dealing_bytes]);
.or_default();
existing_dealings.push(dealing);
Ok(ExecuteResult {
logs: vec![],
+2
View File
@@ -70,6 +70,7 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result<ShutdownHandles>
let coconut_keypair = coconut::keypair::KeyPair::new();
let identity_keypair = config.base.storage_paths.load_identity()?;
let identity_public_key = *identity_keypair.public_key();
// let's build our rocket!
let rocket = http::setup_rocket(
@@ -137,6 +138,7 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result<ShutdownHandles>
&config.coconut_signer,
nyxd_client.clone(),
coconut_keypair,
identity_public_key,
OsRng,
&shutdown,
)
+32 -9
View File
@@ -9,15 +9,17 @@ use async_trait::async_trait;
use cw3::ProposalResponse;
use cw4::MemberResponse;
use nym_coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse;
use nym_coconut_dkg_common::dealer::DealingStatusResponse;
use nym_coconut_dkg_common::msg::QueryMsg as DkgQueryMsg;
use nym_coconut_dkg_common::types::InitialReplacementData;
use nym_coconut_dkg_common::types::{
DealingIndex, InitialReplacementData, PartialContractDealing, State,
};
use nym_coconut_dkg_common::{
dealer::{ContractDealing, DealerDetails, DealerDetailsResponse},
dealer::{DealerDetails, DealerDetailsResponse},
types::{EncodedBTEPublicKeyWithProof, Epoch, EpochId},
verification_key::{ContractVKShare, VerificationKeyShare},
};
use nym_config::defaults::{ChainDetails, NymNetworkDetails};
use nym_contracts_common::dealings::ContractSafeBytes;
use nym_ephemera_common::msg::QueryMsg as EphemeraQueryMsg;
use nym_ephemera_common::types::JsonPeerInfo;
use nym_mixnet_contract_common::families::FamilyHead;
@@ -374,6 +376,10 @@ impl crate::coconut::client::Client for Client {
))
}
async fn contract_state(&self) -> crate::coconut::error::Result<State> {
Ok(nyxd_query!(self, get_state().await?))
}
async fn get_current_epoch(&self) -> crate::coconut::error::Result<Epoch> {
Ok(nyxd_query!(self, get_current_epoch().await?))
}
@@ -401,15 +407,31 @@ impl crate::coconut::client::Client for Client {
Ok(nyxd_query!(self, get_dealer_details(self_address).await?))
}
async fn get_dealing_status(
&self,
epoch_id: EpochId,
dealer: String,
dealing_index: DealingIndex,
) -> crate::coconut::error::Result<DealingStatusResponse> {
Ok(nyxd_query!(
self,
get_dealing_status(epoch_id, dealer, dealing_index).await?
))
}
async fn get_current_dealers(&self) -> crate::coconut::error::Result<Vec<DealerDetails>> {
Ok(nyxd_query!(self, get_all_current_dealers().await?))
}
async fn get_dealings(
&self,
idx: usize,
) -> crate::coconut::error::Result<Vec<ContractDealing>> {
Ok(nyxd_query!(self, get_all_epoch_dealings(idx as u64).await?))
epoch_id: EpochId,
dealer: &str,
) -> crate::coconut::error::Result<Vec<PartialContractDealing>> {
Ok(nyxd_query!(
self,
get_all_dealer_dealings(epoch_id, dealer).await?
))
}
async fn get_verification_key_shares(
@@ -445,23 +467,24 @@ impl crate::coconut::client::Client for Client {
async fn register_dealer(
&self,
bte_key: EncodedBTEPublicKeyWithProof,
identity_key: IdentityKey,
announce_address: String,
resharing: bool,
) -> Result<ExecuteResult, CoconutError> {
Ok(nyxd_signing!(
self,
register_dealer(bte_key, announce_address, resharing, None).await?
register_dealer(bte_key, announce_address, identity_key, resharing, None).await?
))
}
async fn submit_dealing(
&self,
dealing_bytes: ContractSafeBytes,
dealing: PartialContractDealing,
resharing: bool,
) -> Result<ExecuteResult, CoconutError> {
Ok(nyxd_signing!(
self,
submit_dealing_bytes(dealing_bytes, resharing, None).await?
submit_dealing_bytes(dealing, resharing, None).await?
))
}
+1
View File
@@ -3814,6 +3814,7 @@ dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-utils",
"cw4",
"nym-contracts-common",
"nym-multisig-contract-common",
]
+71
View File
@@ -0,0 +1,71 @@
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
out/
release/
build/
# Gradle files
.gradle/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/
# .idea/workspace.xml
# .idea/tasks.xml
# .idea/gradle.xml
# .idea/assetWizardSettings.xml
# .idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
*.jks
*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# MacOS
.DS_Store
# App Specific cases
app/release/output.json
.idea/codeStyles/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 NymConnect
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
## NymConnect for Android
### Prerequisites
_TODO_
### Getting started
[Install](https://developer.android.com/studio/install) Android Studio and open
the project.\
Setup an android emulator using AVD.\
[Run](https://developer.android.com/studio/run/emulator) the project.
**⚠ NOTE**: be sure
to [set](https://developer.android.com/studio/run#changing-variant)
the build variant to `x86_64Debug` when running on emulator
### Features
* Add tunnels via .conf file
* Auto connect to VPN based on Wi-Fi SSID
* Split tunneling by application with search
* Always-on VPN for Android support
* Quick tile support for vpn toggling
* Dynamic shortcuts support for automation integration
* Configurable Trusted Network list
* Optional auto connect on mobile data
* Automatic service restart after reboot
* Service will stay running in background after app has been closed
### Building
_TODO_
### Credits
This project is based on the "WG Tunnel" project made by Zane Schepke
https://github.com/zaneschepke/wgtunnel
### License
MIT
+2
View File
@@ -0,0 +1,2 @@
/build
/release
+159
View File
@@ -0,0 +1,159 @@
val rExtra = rootProject.extra
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
kotlin("kapt")
id("com.google.dagger.hilt.android")
id("org.jetbrains.kotlin.plugin.serialization")
id("io.objectbox")
}
android {
namespace = "net.nymtech.nymconnect"
compileSdk = 34
val versionMajor = 1
val versionMinor = 0
val versionPatch = 0
val versionBuild = 0
defaultConfig {
applicationId = "net.nymtech.nymconnect"
minSdk = 28
targetSdk = 34
versionCode = versionMajor * 10000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
}
buildTypes {
release {
isDebuggable = false
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.8"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
/* flavorDimensions += "abi"
productFlavors {
create("universal") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64", "x86")
}
}
create("arch64") {
dimension = "abi"
ndk {
abiFilters += listOf("arm64-v8a", "x86_64")
}
}
create("arm64") {
dimension = "abi"
ndk {
abiFilters += "arm64-v8a"
}
}
create("arm") {
dimension = "abi"
ndk {
abiFilters += "armeabi-v7a"
}
}
create("x86_64") {
dimension = "abi"
ndk {
abiFilters += "x86_64"
}
}
create("x86") {
dimension = "abi"
ndk {
abiFilters += "x86"
}
}
} */
}
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2023.03.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
implementation("androidx.compose.ui:ui-tooling-preview")
implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.appcompat:appcompat:1.6.1")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2023.03.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
//wireguard tunnel
implementation("com.wireguard.android:tunnel:1.0.20230706")
//logging
implementation("com.jakewharton.timber:timber:5.0.1")
// compose navigation
implementation("androidx.navigation:navigation-compose:2.7.1")
implementation("androidx.hilt:hilt-navigation-compose:1.0.0")
// hilt
implementation("com.google.dagger:hilt-android:${rExtra.get("hiltVersion")}")
kapt("com.google.dagger:hilt-android-compiler:${rExtra.get("hiltVersion")}")
//accompanist
implementation("com.google.accompanist:accompanist-systemuicontroller:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-permissions:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-flowlayout:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-navigation-animation:${rExtra.get("accompanistVersion")}")
implementation("com.google.accompanist:accompanist-drawablepainter:${rExtra.get("accompanistVersion")}")
//db
implementation("io.objectbox:objectbox-kotlin:${rExtra.get("objectBoxVersion")}")
//lifecycle
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
//icons
implementation("androidx.compose.material:material-icons-extended:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
}
kapt {
correctErrorTypes = true
}
@@ -0,0 +1,99 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "9:4468844863383145378",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
},
{
"id": "9:4468844863383145378",
"name": "isAlwaysOnVpnEnabled",
"type": 1
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
@@ -0,0 +1,94 @@
{
"_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
"entities": [
{
"id": "1:2692736974585027589",
"lastPropertyId": "15:5057486545428188436",
"name": "TunnelConfig",
"properties": [
{
"id": "1:1985347930017457084",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "12:2409068226744965585",
"name": "name",
"indexId": "1:4811206443952699137",
"type": 9,
"flags": 34848
},
{
"id": "13:8987443291286312275",
"name": "wgQuick",
"type": 9
}
],
"relations": []
},
{
"id": "2:8887605597748372702",
"lastPropertyId": "8:4981008812459251156",
"name": "Settings",
"properties": [
{
"id": "1:7485739868216068651",
"name": "id",
"type": 6,
"flags": 1
},
{
"id": "2:5814013113141456749",
"name": "isAutoTunnelEnabled",
"type": 1
},
{
"id": "4:5645665441196906014",
"name": "trustedNetworkSSIDs",
"type": 30
},
{
"id": "5:4989886999117763881",
"name": "isTunnelOnMobileDataEnabled",
"type": 1
},
{
"id": "6:3370284381040192129",
"name": "defaultTunnel",
"type": 9
}
],
"relations": []
}
],
"lastEntityId": "2:8887605597748372702",
"lastIndexId": "1:4811206443952699137",
"lastRelationId": "0:0",
"lastSequenceId": "0:0",
"modelVersion": 5,
"modelVersionParserMinimum": 5,
"retiredEntityUids": [],
"retiredIndexUids": [],
"retiredPropertyUids": [
1763475292291320186,
6483820955437198310,
8323071516033820771,
5904440563612311217,
1408037976996390989,
7737847485212546994,
8215616901775229364,
8021610768066328637,
6174306582797008721,
2175939938544485767,
7555225587864607050,
969146862000617878,
5057486545428188436,
2814640993034665120,
4981008812459251156
],
"retiredRelationUids": [],
"version": 1
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package net.nymtech.nymconnect
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("net.nymtech.nymconnect", appContext.packageName)
}
}
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"
android:maxSdkVersion="30"
tools:ignore="LeanbackUsesWifi" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING"/>
<!--foreground service permissions-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!--start service on boot permission-->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!--android tv support-->
<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<uses-feature
android:name="android.hardware.screen.portrait"
android:required="false" />
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
</intent>
</queries>
<application
android:allowBackup="true"
android:name=".WireGuardAutoTunnel"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:banner="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.WireguardAutoTunnel"
tools:targetApi="31">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.WireguardAutoTunnel">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
</activity>
<activity
android:finishOnTaskLaunch="true"
android:theme="@android:style/Theme.NoDisplay"
android:name=".service.shortcut.ShortcutsActivity"/>
<service
android:name=".service.foreground.ForegroundService"
android:enabled="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
</service>
<service
android:exported="true"
android:name=".service.tile.TunnelControlTile"
android:icon="@drawable/shield"
android:label="NymConnect"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE"
android:value="true" />
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE"
android:value="true" />
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service
android:name=".service.foreground.WireGuardTunnelService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:enabled="true"
android:persistent="true"
android:foregroundServiceType="remoteMessaging"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService"/>
</intent-filter>
<meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
android:value="true"/>
</service>
<service
android:name=".service.foreground.WireGuardConnectivityWatcherService"
android:enabled="true"
android:stopWithTask="false"
android:persistent="true"
android:foregroundServiceType="location"
android:permission=""
android:exported="false">
</service>
<receiver android:enabled="true" android:name=".receiver.BootReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:exported="false" android:name=".receiver.NotificationActionReceiver"/>
</application>
</manifest>
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,6 @@
package net.nymtech.nymconnect
object Constants {
const val VPN_CONNECTIVITY_CHECK_INTERVAL = 3000L;
const val VPN_STATISTIC_CHECK_INTERVAL = 10000L;
}
@@ -0,0 +1,27 @@
package net.nymtech.nymconnect
import android.app.Application
import android.content.Context
import android.content.pm.PackageManager
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
@HiltAndroidApp
class WireGuardAutoTunnel : Application() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onCreate() {
super.onCreate()
settingsRepo.init()
}
companion object {
fun isRunningOnAndroidTv(context : Context) : Boolean {
return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
}
}
}
@@ -0,0 +1,40 @@
package net.nymtech.nymconnect.module
import android.content.Context
import net.nymtech.nymconnect.service.tunnel.model.MyObjectBox
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import io.objectbox.Box
import io.objectbox.BoxStore
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class BoxModule {
@Provides
@Singleton
fun provideBoxStore(@ApplicationContext context : Context) : BoxStore {
return MyObjectBox.builder()
.androidContext(context.applicationContext)
.build()
}
@Provides
@Singleton
fun provideBoxForSettings(store : BoxStore) : Box<Settings> {
return store.boxFor(Settings::class.java)
}
@Provides
@Singleton
fun provideBoxForTunnels(store : BoxStore) : Box<TunnelConfig> {
return store.boxFor(TunnelConfig::class.java)
}
}
@@ -0,0 +1,25 @@
package net.nymtech.nymconnect.module
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.repository.SettingsBox
import net.nymtech.nymconnect.repository.TunnelBox
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun provideSettingsRepository(settingsBox: SettingsBox) : Repository<Settings>
@Binds
@Singleton
abstract fun provideTunnelRepository(tunnelBox: TunnelBox) : Repository<TunnelConfig>
}
@@ -0,0 +1,29 @@
package net.nymtech.nymconnect.module
import net.nymtech.nymconnect.service.network.MobileDataService
import net.nymtech.nymconnect.service.network.NetworkService
import net.nymtech.nymconnect.service.network.WifiService
import net.nymtech.nymconnect.service.notification.NotificationService
import net.nymtech.nymconnect.service.notification.WireGuardNotification
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ServiceComponent
import dagger.hilt.android.scopes.ServiceScoped
@Module
@InstallIn(ServiceComponent::class)
abstract class ServiceModule {
@Binds
@ServiceScoped
abstract fun provideNotificationService(wireGuardNotification: WireGuardNotification) : NotificationService
@Binds
@ServiceScoped
abstract fun provideWifiService(wifiService: WifiService) : NetworkService<WifiService>
@Binds
@ServiceScoped
abstract fun provideMobileDataService(mobileDataService : MobileDataService) : NetworkService<MobileDataService>
}
@@ -0,0 +1,31 @@
package net.nymtech.nymconnect.module
import android.content.Context
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.GoBackend
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.WireGuardTunnel
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
class TunnelModule {
@Provides
@Singleton
fun provideBackend(@ApplicationContext context : Context) : Backend {
return GoBackend(context)
}
@Provides
@Singleton
fun provideVpnService(backend: Backend) : VpnService {
return WireGuardTunnel(backend)
}
}
@@ -0,0 +1,39 @@
package net.nymtech.nymconnect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class BootReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.isAutoTunnelEnabled && setting.defaultTunnel != null) {
ServiceManager.startWatcherService(context, setting.defaultTunnel!!)
}
}
} finally {
cancel()
}
}
}
}
}
@@ -0,0 +1,39 @@
package net.nymtech.nymconnect.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
class NotificationActionReceiver : BroadcastReceiver() {
@Inject
lateinit var settingsRepo : Repository<Settings>
override fun onReceive(context: Context, intent: Intent?) {
CoroutineScope(Dispatchers.IO).launch {
try {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if (setting.defaultTunnel != null) {
ServiceManager.stopVpnService(context)
delay(1000)
ServiceManager.startVpnService(context, setting.defaultTunnel.toString())
}
}
} finally {
cancel()
}
}
}
}
@@ -0,0 +1,16 @@
package net.nymtech.nymconnect.repository
import kotlinx.coroutines.flow.Flow
interface Repository<T> {
suspend fun save(t : T)
suspend fun saveAll(t : List<T>)
suspend fun getById(id : Long) : T?
suspend fun getAll() : List<T>?
suspend fun delete(t : T) : Boolean?
suspend fun count() : Long?
val itemFlow : Flow<MutableList<T>>
fun init()
}
@@ -0,0 +1,63 @@
package net.nymtech.nymconnect.repository
import net.nymtech.nymconnect.service.tunnel.model.Settings
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import javax.inject.Inject
class SettingsBox @Inject constructor(private val box : Box<Settings>, private val boxStore : BoxStore) : Repository<Settings> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
CoroutineScope(Dispatchers.IO).launch {
if(getAll().isNullOrEmpty()) {
save(Settings())
}
}
}
override suspend fun save(t : Settings) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<Settings>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): Settings? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<Settings>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : Settings): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,57 @@
package net.nymtech.nymconnect.repository
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import io.objectbox.Box
import io.objectbox.BoxStore
import io.objectbox.kotlin.awaitCallInTx
import io.objectbox.kotlin.toFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import timber.log.Timber
import javax.inject.Inject
class TunnelBox @Inject constructor(private val box : Box<TunnelConfig>,private val boxStore : BoxStore) : Repository<TunnelConfig> {
@OptIn(ExperimentalCoroutinesApi::class)
override val itemFlow = box.query().build().subscribe().toFlow()
override fun init() {
}
override suspend fun save(t : TunnelConfig) {
Timber.d("Saving tunnel config")
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun saveAll(t : List<TunnelConfig>) {
boxStore.awaitCallInTx {
box.put(t)
}
}
override suspend fun getById(id: Long): TunnelConfig? {
return boxStore.awaitCallInTx {
box[id]
}
}
override suspend fun getAll(): List<TunnelConfig>? {
return boxStore.awaitCallInTx {
box.all
}
}
override suspend fun delete(t : TunnelConfig): Boolean? {
return boxStore.awaitCallInTx {
box.remove(t)
}
}
override suspend fun count() : Long? {
return boxStore.awaitCallInTx {
box.count()
}
}
}
@@ -0,0 +1,6 @@
package net.nymtech.nymconnect.service.foreground
enum class Action {
START,
STOP
}
@@ -0,0 +1,64 @@
package net.nymtech.nymconnect.service.foreground
import android.app.Service
import android.content.Intent
import android.os.Bundle
import android.os.IBinder
import timber.log.Timber
open class ForegroundService : Service() {
private var isServiceStarted = false
override fun onBind(intent: Intent): IBinder? {
// We don't provide binding, so return null
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Timber.d("onStartCommand executed with startId: $startId")
if (intent != null) {
val action = intent.action
Timber.d("using an intent with action $action")
when (action) {
Action.START.name -> startService(intent.extras)
Action.STOP.name -> stopService(intent.extras)
"android.net.VpnService" -> {
Timber.d("Always-on VPN starting service")
startService(intent.extras)
}
else -> Timber.d("This should never happen. No action in the received intent")
}
} else {
Timber.d(
"with a null intent. It has been probably restarted by the system."
)
}
// by returning this we make sure the service is restarted if the system kills the service
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
Timber.d("The service has been destroyed")
}
protected open fun startService(extras : Bundle?) {
if (isServiceStarted) return
Timber.d("Starting ${this.javaClass.simpleName}")
isServiceStarted = true
}
protected open fun stopService(extras : Bundle?) {
Timber.d("Stopping ${this.javaClass.simpleName}")
try {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
} catch (e: Exception) {
Timber.d("Service stopped without being started: ${e.message}")
}
isServiceStarted = false
}
}
@@ -0,0 +1,90 @@
package net.nymtech.nymconnect.service.foreground
import android.app.ActivityManager
import android.app.Application
import android.app.Service
import android.content.Context
import android.content.Context.ACTIVITY_SERVICE
import android.content.Intent
import net.nymtech.nymconnect.R
import timber.log.Timber
object ServiceManager {
@Suppress("DEPRECATION")
private // Deprecated for third party Services.
fun <T> Context.isServiceRunning(service: Class<T>) =
(getSystemService(ACTIVITY_SERVICE) as ActivityManager)
.getRunningServices(Integer.MAX_VALUE)
.any { it.service.className == service.name }
fun <T : Service> getServiceState(context: Context, cls : Class<T>): ServiceState {
val isServiceRunning = context.isServiceRunning(cls)
return if(isServiceRunning) ServiceState.STARTED else ServiceState.STOPPED
}
private fun <T : Service> actionOnService(action: Action, context: Context, cls : Class<T>, extras : Map<String,String>? = null) {
if (getServiceState(context, cls) == ServiceState.STOPPED && action == Action.STOP) return
if (getServiceState(context, cls) == ServiceState.STARTED && action == Action.START) return
val intent = Intent(context, cls).also {
it.action = action.name
extras?.forEach {(k, v) ->
it.putExtra(k, v)
}
}
intent.component?.javaClass
try {
when(action) {
Action.START -> {
try {
context.startForegroundService(intent)
} catch (e : Exception) {
Timber.e("Unable to start service foreground ${e.message}")
context.startService(intent)
}
}
Action.STOP -> context.startService(intent)
}
} catch (e : Exception) {
Timber.tag("ServiceManager").e(e)
}
}
fun startVpnService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START,
context,
WireGuardTunnelService::class.java,
mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig))
}
fun stopVpnService(context : Context) {
actionOnService(
Action.STOP,
context,
WireGuardTunnelService::class.java
)
}
fun startWatcherService(context : Context, tunnelConfig : String) {
actionOnService(
Action.START, context,
WireGuardConnectivityWatcherService::class.java, mapOf(context.
getString(R.string.tunnel_extras_key) to
tunnelConfig))
}
fun stopWatcherService(context : Context) {
actionOnService(
Action.STOP, context,
WireGuardConnectivityWatcherService::class.java)
}
fun toggleWatcherService(context: Context, tunnelConfig : String) {
when(getServiceState(
context,
WireGuardConnectivityWatcherService::class.java,
)) {
ServiceState.STARTED -> stopWatcherService(context)
ServiceState.STOPPED -> startWatcherService(context, tunnelConfig)
}
}
}
@@ -0,0 +1,6 @@
package net.nymtech.nymconnect.service.foreground
enum class ServiceState {
STARTED,
STOPPED,
}
@@ -0,0 +1,210 @@
package net.nymtech.nymconnect.service.foreground
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.PowerManager
import android.os.SystemClock
import com.wireguard.android.backend.Tunnel
import net.nymtech.nymconnect.Constants
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.network.MobileDataService
import net.nymtech.nymconnect.service.network.NetworkService
import net.nymtech.nymconnect.service.network.NetworkStatus
import net.nymtech.nymconnect.service.network.WifiService
import net.nymtech.nymconnect.service.notification.NotificationService
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.model.Settings
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardConnectivityWatcherService : ForegroundService() {
private val foregroundId = 122;
@Inject
lateinit var wifiService : NetworkService<WifiService>
@Inject
lateinit var mobileDataService : NetworkService<MobileDataService>
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var notificationService : NotificationService
@Inject
lateinit var vpnService : VpnService
private var isWifiConnected = false;
private var isMobileDataConnected = false;
private var currentNetworkSSID = "";
private lateinit var watcherJob : Job;
private lateinit var setting : Settings
private lateinit var tunnelConfig: String
private var wakeLock: PowerManager.WakeLock? = null
private val tag = this.javaClass.name;
override fun startService(extras: Bundle?) {
super.startService(extras)
val tunnelId = extras?.getString(getString(R.string.tunnel_extras_key))
if (tunnelId != null) {
this.tunnelConfig = tunnelId
}
// we need this lock so our service gets not affected by Doze Mode
initWakeLock()
cancelWatcherJob()
launchWatcherNotification()
if(this::tunnelConfig.isInitialized) {
startWatcherJob()
} else {
stopService(extras)
}
}
override fun stopService(extras: Bundle?) {
super.stopService(extras)
wakeLock?.let {
if (it.isHeld) {
it.release()
}
}
cancelWatcherJob()
stopSelf()
}
private fun launchWatcherNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.watcher_channel_id),
channelName = getString(R.string.watcher_channel_name),
description = getString(R.string.watcher_notification_text))
super.startForeground(foregroundId, notification)
}
//try to start task again if killed
override fun onTaskRemoved(rootIntent: Intent) {
Timber.d("Task Removed called")
val restartServiceIntent = Intent(rootIntent)
val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE);
applicationContext.getSystemService(Context.ALARM_SERVICE);
val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
}
private fun initWakeLock() {
wakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "$tag::lock").apply {
acquire()
}
}
}
private fun cancelWatcherJob() {
if(this::watcherJob.isInitialized) {
watcherJob.cancel()
}
}
private fun startWatcherJob() {
watcherJob = CoroutineScope(Dispatchers.IO).launch {
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
setting = settings[0]
}
launch {
watchForWifiConnectivityChanges()
}
if(setting.isTunnelOnMobileDataEnabled) {
launch {
watchForMobileDataConnectivityChanges()
}
}
launch {
manageVpn()
}
}
}
private suspend fun watchForMobileDataConnectivityChanges() {
mobileDataService.networkStatus.collect {
when(it) {
is NetworkStatus.Available -> {
Timber.d("Gained Mobile data connection")
isMobileDataConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
isMobileDataConnected = true
Timber.d("Mobile data capabilities changed")
}
is NetworkStatus.Unavailable -> {
isMobileDataConnected = false
Timber.d("Lost mobile data connection")
}
else -> {}
}
}
}
private suspend fun watchForWifiConnectivityChanges() {
wifiService.networkStatus.collect {
when (it) {
is NetworkStatus.Available -> {
Timber.d("Gained Wi-Fi connection")
isWifiConnected = true
}
is NetworkStatus.CapabilitiesChanged -> {
Timber.d("Wifi capabilities changed")
isWifiConnected = true
currentNetworkSSID = wifiService.getNetworkName(it.networkCapabilities) ?: "";
}
is NetworkStatus.Unavailable -> {
isWifiConnected = false
Timber.d("Lost Wi-Fi connection")
}
else -> {}
}
}
}
private suspend fun manageVpn() {
while(watcherJob.isActive) {
if(setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
isMobileDataConnected
&& vpnService.getState() == Tunnel.State.DOWN) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if(!setting.isTunnelOnMobileDataEnabled &&
!isWifiConnected &&
vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this)
} else if(isWifiConnected &&
!setting.trustedNetworkSSIDs.contains(currentNetworkSSID) &&
(vpnService.getState() != Tunnel.State.UP)) {
ServiceManager.startVpnService(this, tunnelConfig)
} else if((isWifiConnected &&
setting.trustedNetworkSSIDs.contains(currentNetworkSSID)) &&
(vpnService.getState() == Tunnel.State.UP)) {
ServiceManager.stopVpnService(this)
}
delay(Constants.VPN_CONNECTIVITY_CHECK_INTERVAL)
}
}
}
@@ -0,0 +1,154 @@
package net.nymtech.nymconnect.service.foreground
import android.app.PendingIntent
import android.content.Intent
import android.os.Bundle
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.receiver.NotificationActionReceiver
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.notification.NotificationService
import net.nymtech.nymconnect.service.tunnel.HandshakeStatus
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class WireGuardTunnelService : ForegroundService() {
private val foregroundId = 123;
@Inject
lateinit var vpnService : VpnService
@Inject
lateinit var settingsRepo: Repository<Settings>
@Inject
lateinit var notificationService : NotificationService
private lateinit var job : Job
private var tunnelName : String = ""
override fun startService(extras : Bundle?) {
super.startService(extras)
val tunnelConfigString = extras?.getString(getString(R.string.tunnel_extras_key))
cancelJob()
job = CoroutineScope(Dispatchers.IO).launch {
if(tunnelConfigString != null) {
try {
val tunnelConfig = TunnelConfig.from(tunnelConfigString)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
launchVpnStartingNotification()
} catch (e : Exception) {
Timber.e("Problem starting tunnel: ${e.message}")
stopService(extras)
}
} else {
Timber.d("Tunnel config null, starting default tunnel")
val settings = settingsRepo.getAll();
if(!settings.isNullOrEmpty()) {
val setting = settings[0]
if(setting.defaultTunnel != null && setting.isAlwaysOnVpnEnabled) {
val tunnelConfig = TunnelConfig.from(setting.defaultTunnel!!)
tunnelName = tunnelConfig.name
vpnService.startTunnel(tunnelConfig)
launchVpnStartingNotification()
}
}
}
}
CoroutineScope(job).launch {
var didShowConnected = false
var didShowFailedHandshakeNotification = false
vpnService.handshakeStatus.collect {
when(it) {
HandshakeStatus.NOT_STARTED -> {
}
HandshakeStatus.NEVER_CONNECTED -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.initial_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
HandshakeStatus.HEALTHY -> {
if(!didShowConnected) {
launchVpnConnectedNotification()
didShowConnected = true
}
}
HandshakeStatus.UNHEALTHY -> {
if(!didShowFailedHandshakeNotification) {
launchVpnConnectionFailedNotification(getString(R.string.lost_connection_failure_message))
didShowFailedHandshakeNotification = true
didShowConnected = false
}
}
}
}
}
}
override fun stopService(extras : Bundle?) {
super.stopService(extras)
CoroutineScope(Dispatchers.IO).launch() {
vpnService.stopTunnel()
}
cancelJob()
stopSelf()
}
private fun launchVpnConnectedNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.tunnel_start_title),
onGoing = false,
showTimestamp = true,
description = "${getString(R.string.tunnel_start_text)} $tunnelName"
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnStartingNotification() {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
title = getString(R.string.vpn_starting),
onGoing = false,
showTimestamp = true,
description = getString(R.string.attempt_connection)
)
super.startForeground(foregroundId, notification)
}
private fun launchVpnConnectionFailedNotification(message : String) {
val notification = notificationService.createNotification(
channelId = getString(R.string.vpn_channel_id),
channelName = getString(R.string.vpn_channel_name),
action = PendingIntent.getBroadcast(this,0,Intent(this, NotificationActionReceiver::class.java),PendingIntent.FLAG_IMMUTABLE),
actionText = getString(R.string.restart),
title = getString(R.string.vpn_connection_failed),
onGoing = false,
showTimestamp = true,
description = message
)
super.startForeground(foregroundId, notification)
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel()
}
}
}
@@ -0,0 +1,117 @@
package net.nymtech.nymconnect.service.network
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.net.wifi.SupplicantState
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.map
abstract class BaseNetworkService<T : BaseNetworkService<T>>(val context: Context, networkCapability : Int) : NetworkService<T> {
private val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val wifiManager =
context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
override val networkStatus = callbackFlow {
val networkStatusCallback = when (Build.VERSION.SDK_INT) {
in Build.VERSION_CODES.S..Int.MAX_VALUE -> {
object : ConnectivityManager.NetworkCallback(
FLAG_INCLUDE_LOCATION_INFO
) {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
}
}
}
else -> {
object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(NetworkStatus.Available(network))
}
override fun onLost(network: Network) {
trySend(NetworkStatus.Unavailable(network))
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities
) {
trySend(NetworkStatus.CapabilitiesChanged(network, networkCapabilities))
}
}
}
}
val request = NetworkRequest.Builder()
.addTransportType(networkCapability)
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
.build()
connectivityManager.registerNetworkCallback(request, networkStatusCallback)
awaitClose {
connectivityManager.unregisterNetworkCallback(networkStatusCallback)
}
}
override fun getNetworkName(networkCapabilities: NetworkCapabilities): String? {
var ssid: String? = getWifiNameFromCapabilities(networkCapabilities)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
val info = wifiManager.connectionInfo
if (info.supplicantState === SupplicantState.COMPLETED) {
ssid = info.ssid
}
}
return ssid?.trim('"')
}
companion object {
private fun getWifiNameFromCapabilities(networkCapabilities: NetworkCapabilities): String? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val info: WifiInfo
if (networkCapabilities.transportInfo is WifiInfo) {
info = networkCapabilities.transportInfo as WifiInfo
return info.ssid
}
}
return null
}
}
}
inline fun <Result> Flow<NetworkStatus>.map(
crossinline onUnavailable: suspend (network : Network) -> Result,
crossinline onAvailable: suspend (network : Network) -> Result,
crossinline onCapabilitiesChanged: suspend (network : Network, networkCapabilities : NetworkCapabilities) -> Result,
): Flow<Result> = map { status ->
when (status) {
is NetworkStatus.Unavailable -> onUnavailable(status.network)
is NetworkStatus.Available -> onAvailable(status.network)
is NetworkStatus.CapabilitiesChanged -> onCapabilitiesChanged(status.network, status.networkCapabilities)
}
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class MobileDataService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<MobileDataService>(context, NetworkCapabilities.TRANSPORT_CELLULAR) {
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.net.NetworkCapabilities
import kotlinx.coroutines.flow.Flow
interface NetworkService<T> {
fun getNetworkName(networkCapabilities: NetworkCapabilities) : String?
val networkStatus : Flow<NetworkStatus>
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.net.Network
import android.net.NetworkCapabilities
sealed class NetworkStatus {
class Available(val network : Network) : NetworkStatus()
class Unavailable(val network : Network) : NetworkStatus()
class CapabilitiesChanged(val network : Network, val networkCapabilities : NetworkCapabilities) : NetworkStatus()
}
@@ -0,0 +1,10 @@
package net.nymtech.nymconnect.service.network
import android.content.Context
import android.net.NetworkCapabilities
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WifiService @Inject constructor(@ApplicationContext context: Context) :
BaseNetworkService<WifiService>(context, NetworkCapabilities.TRANSPORT_WIFI) {
}
@@ -0,0 +1,21 @@
package net.nymtech.nymconnect.service.notification
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
interface NotificationService {
fun createNotification(
channelId: String,
channelName: String,
title: String = "",
action: PendingIntent? = null,
actionText: String? = null,
description: String,
showTimestamp : Boolean = false,
importance: Int = NotificationManager.IMPORTANCE_HIGH,
vibration: Boolean = true,
onGoing: Boolean = true,
lights: Boolean = true
): Notification
}
@@ -0,0 +1,77 @@
package net.nymtech.nymconnect.service.notification
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.ui.MainActivity
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
class WireGuardNotification @Inject constructor(@ApplicationContext private val context: Context) : NotificationService {
private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager;
override fun createNotification(
channelId: String,
channelName: String,
title: String,
action: PendingIntent?,
actionText: String?,
description: String,
showTimestamp: Boolean,
importance: Int,
vibration: Boolean,
onGoing: Boolean,
lights: Boolean
): Notification {
val channel = NotificationChannel(
channelId,
channelName,
importance
).let {
it.description = title
it.enableLights(lights)
it.lightColor = Color.RED
it.enableVibration(vibration)
it.vibrationPattern = longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
it
}
notificationManager.createNotificationChannel(channel)
val pendingIntent: PendingIntent =
Intent(context, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(
context,
0,
notificationIntent,
PendingIntent.FLAG_IMMUTABLE
)
}
val builder: Notification.Builder =
Notification.Builder(
context,
channelId
)
return builder.let {
if(action != null && actionText != null) {
//TODO find a not deprecated way to do this
it.addAction(
Notification.Action.Builder(0, actionText, action)
.build())
it.setAutoCancel(true)
}
it.setContentTitle(title)
.setContentText(description)
.setContentIntent(pendingIntent)
.setOngoing(onGoing)
.setShowWhen(showTimestamp)
.setSmallIcon(R.mipmap.ic_launcher_foreground)
.build()
}
}
}
@@ -0,0 +1,28 @@
package net.nymtech.nymconnect.service.shortcut
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.service.foreground.Action
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.foreground.WireGuardTunnelService
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ShortcutsActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.getStringExtra(ShortcutsManager.CLASS_NAME_EXTRA_KEY)
.equals(WireGuardTunnelService::class.java.name)) {
intent.getStringExtra(getString(R.string.tunnel_extras_key))?.let {
ServiceManager.toggleWatcherService(this, it)
}
when(intent.action){
Action.STOP.name -> ServiceManager.stopVpnService(this)
Action.START.name -> intent.getStringExtra(getString(R.string.tunnel_extras_key))
?.let { ServiceManager.startVpnService(this, it) }
}
}
finish()
}
}
@@ -0,0 +1,73 @@
package net.nymtech.nymconnect.service.shortcut
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.service.foreground.Action
import net.nymtech.nymconnect.service.foreground.WireGuardTunnelService
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
object ShortcutsManager {
private const val SHORT_LABEL_MAX_SIZE = 10;
private const val LONG_LABEL_MAX_SIZE = 25;
private const val APPEND_ON = " On";
private const val APPEND_OFF = " Off"
const val CLASS_NAME_EXTRA_KEY = "className"
private fun createAndPushShortcut(context : Context, intent : Intent, id : String, shortLabel : String,
longLabel : String, drawable : Int ) {
val shortcut = ShortcutInfoCompat.Builder(context, id)
.setShortLabel(shortLabel)
.setLongLabel(longLabel)
.setIcon(IconCompat.createWithResource(context, drawable))
.setIntent(intent)
.build()
ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)
}
fun createTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
createAndPushShortcut(context,
createTunnelOnIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_ON.length)) + APPEND_ON,
R.drawable.vpn_on
)
createAndPushShortcut(context,
createTunnelOffIntent(context, mapOf(context.getString(R.string.tunnel_extras_key) to tunnelConfig.toString())),
tunnelConfig.id.toString() + APPEND_OFF,
tunnelConfig.name.take((SHORT_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
tunnelConfig.name.take((LONG_LABEL_MAX_SIZE - APPEND_OFF.length)) + APPEND_OFF,
R.drawable.vpn_off
)
}
fun removeTunnelShortcuts(context : Context, tunnelConfig : TunnelConfig) {
ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(tunnelConfig.id.toString() + APPEND_ON,
tunnelConfig.id.toString() + APPEND_OFF ))
}
private fun createTunnelOnIntent(context: Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.START.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
private fun createTunnelOffIntent(context : Context, extras : Map<String,String>) : Intent {
return Intent(context, ShortcutsActivity::class.java).also {
it.action = Action.STOP.name
it.putExtra(CLASS_NAME_EXTRA_KEY, WireGuardTunnelService::class.java.name)
extras.forEach {(k, v) ->
it.putExtra(k, v)
}
}
}
}
@@ -0,0 +1,142 @@
package net.nymtech.nymconnect.service.tile
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import com.wireguard.android.backend.Tunnel
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.repository.Repository
import net.nymtech.nymconnect.service.foreground.ServiceManager
import net.nymtech.nymconnect.service.tunnel.VpnService
import net.nymtech.nymconnect.service.tunnel.model.Settings
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@AndroidEntryPoint
class TunnelControlTile : TileService() {
@Inject
lateinit var settingsRepo : Repository<Settings>
@Inject
lateinit var configRepo : Repository<TunnelConfig>
@Inject
lateinit var vpnService : VpnService
private val scope = CoroutineScope(Dispatchers.Main);
private lateinit var job : Job
override fun onStartListening() {
job = scope.launch {
updateTileState()
}
super.onStartListening()
}
override fun onTileAdded() {
super.onTileAdded()
qsTile.contentDescription = this.resources.getString(R.string.toggle_vpn)
scope.launch {
updateTileState();
}
}
override fun onTileRemoved() {
super.onTileRemoved()
cancelJob()
}
override fun onClick() {
super.onClick()
unlockAndRun {
scope.launch {
try {
val tunnel = determineTileTunnel();
if(tunnel != null) {
attemptWatcherServiceToggle(tunnel.toString())
if(vpnService.getState() == Tunnel.State.UP) {
ServiceManager.stopVpnService(this@TunnelControlTile)
} else {
ServiceManager.startVpnService(this@TunnelControlTile, tunnel.toString())
}
}
} catch (e : Exception) {
Timber.e(e.message)
} finally {
cancel()
}
}
}
}
private suspend fun determineTileTunnel() : TunnelConfig? {
var tunnelConfig : TunnelConfig? = null;
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
tunnelConfig = if (setting.defaultTunnel != null) {
TunnelConfig.from(setting.defaultTunnel!!);
} else {
val config = configRepo.getAll()?.first();
config;
}
}
return tunnelConfig;
}
private fun attemptWatcherServiceToggle(tunnelConfig : String) {
scope.launch {
val settings = settingsRepo.getAll()
if (!settings.isNullOrEmpty()) {
val setting = settings.first()
if(setting.isAutoTunnelEnabled) {
ServiceManager.toggleWatcherService(this@TunnelControlTile, tunnelConfig)
}
}
}
}
private suspend fun updateTileState() {
vpnService.state.collect {
when(it) {
Tunnel.State.UP -> {
qsTile.state = Tile.STATE_ACTIVE
}
Tunnel.State.DOWN -> {
qsTile.state = Tile.STATE_INACTIVE;
}
else -> {
qsTile.state = Tile.STATE_UNAVAILABLE
}
}
val config = determineTileTunnel();
setTileDescription(config?.name ?: this.resources.getString(R.string.no_tunnel_available))
qsTile.updateTile()
}
}
private fun setTileDescription(description : String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
qsTile.subtitle = description
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
qsTile.stateDescription = description;
}
}
private fun cancelJob() {
if(this::job.isInitialized) {
job.cancel();
}
}
}
@@ -0,0 +1,14 @@
package net.nymtech.nymconnect.service.tunnel
enum class HandshakeStatus {
HEALTHY,
UNHEALTHY,
NEVER_CONNECTED,
NOT_STARTED;
companion object {
private const val WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC = 120
const val UNHEALTHY_TIME_LIMIT_SEC = WG_TYPICAL_HANDSHAKE_INTERVAL_WHEN_HEALTHY_SEC + 60
const val NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC = 30
}
}
@@ -0,0 +1,18 @@
package net.nymtech.nymconnect.service.tunnel
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import kotlinx.coroutines.flow.SharedFlow
interface VpnService : Tunnel {
suspend fun startTunnel(tunnelConfig : TunnelConfig) : Tunnel.State
suspend fun stopTunnel()
val state : SharedFlow<Tunnel.State>
val tunnelName : SharedFlow<String>
val statistics : SharedFlow<Statistics>
val lastHandshake : SharedFlow<Map<Key,Long>>
val handshakeStatus : SharedFlow<HandshakeStatus>
fun getState() : Tunnel.State
}
@@ -0,0 +1,131 @@
package net.nymtech.nymconnect.service.tunnel
import com.wireguard.android.backend.Backend
import com.wireguard.android.backend.BackendException
import com.wireguard.android.backend.Statistics
import com.wireguard.android.backend.Tunnel
import com.wireguard.crypto.Key
import net.nymtech.nymconnect.Constants
import net.nymtech.nymconnect.service.tunnel.model.TunnelConfig
import net.nymtech.nymconnect.util.NumberUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class WireGuardTunnel @Inject constructor(private val backend : Backend,
) : VpnService {
private val _tunnelName = MutableStateFlow("")
override val tunnelName get() = _tunnelName.asStateFlow()
private val _state = MutableSharedFlow<Tunnel.State>(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
replay = 1)
private val _handshakeStatus = MutableSharedFlow<HandshakeStatus>(replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST)
override val state get() = _state.asSharedFlow()
private val _statistics = MutableSharedFlow<Statistics>(replay = 1)
override val statistics get() = _statistics.asSharedFlow()
private val _lastHandshake = MutableSharedFlow<Map<Key, Long>>(replay = 1)
override val lastHandshake get() = _lastHandshake.asSharedFlow()
override val handshakeStatus: SharedFlow<HandshakeStatus>
get() = _handshakeStatus.asSharedFlow()
private lateinit var statsJob : Job
override suspend fun startTunnel(tunnelConfig: TunnelConfig) : Tunnel.State{
return try {
if(getState() == Tunnel.State.UP && _tunnelName.value != tunnelConfig.name) {
stopTunnel()
}
_tunnelName.emit(tunnelConfig.name)
val config = TunnelConfig.configFromQuick(tunnelConfig.wgQuick)
val state = backend.setState(
this, Tunnel.State.UP, config)
_state.emit(state)
state;
} catch (e : Exception) {
Timber.e("Failed to start tunnel with error: ${e.message}")
Tunnel.State.DOWN
}
}
override fun getName(): String {
return _tunnelName.value
}
override suspend fun stopTunnel() {
try {
if(getState() == Tunnel.State.UP) {
val state = backend.setState(this, Tunnel.State.DOWN, null)
_state.emit(state)
}
} catch (e : BackendException) {
Timber.e("Failed to stop tunnel with error: ${e.message}")
}
}
override fun getState(): Tunnel.State {
return backend.getState(this)
}
override fun onStateChange(state : Tunnel.State) {
val tunnel = this;
_state.tryEmit(state)
if(state == Tunnel.State.UP) {
statsJob = CoroutineScope(Dispatchers.IO).launch {
val handshakeMap = HashMap<Key, Long>()
var neverHadHandshakeCounter = 0
while (true) {
val statistics = backend.getStatistics(tunnel)
_statistics.emit(statistics)
statistics.peers().forEach {
val handshakeEpoch = statistics.peer(it)?.latestHandshakeEpochMillis ?: 0L
handshakeMap[it] = handshakeEpoch
if(handshakeEpoch == 0L) {
if(neverHadHandshakeCounter >= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
_handshakeStatus.emit(HandshakeStatus.NEVER_CONNECTED)
} else {
_handshakeStatus.emit(HandshakeStatus.NOT_STARTED)
}
if(neverHadHandshakeCounter <= HandshakeStatus.NEVER_CONNECTED_TO_UNHEALTHY_TIME_LIMIT_SEC) {
neverHadHandshakeCounter += 10
}
return@forEach
}
if(NumberUtils.getSecondsBetweenTimestampAndNow(handshakeEpoch) >= HandshakeStatus.UNHEALTHY_TIME_LIMIT_SEC) {
_handshakeStatus.emit(HandshakeStatus.UNHEALTHY)
} else {
_handshakeStatus.emit(HandshakeStatus.HEALTHY)
}
}
_lastHandshake.emit(handshakeMap)
delay(Constants.VPN_STATISTIC_CHECK_INTERVAL)
}
}
}
if(state == Tunnel.State.DOWN) {
if(this::statsJob.isInitialized) {
statsJob.cancel()
}
_handshakeStatus.tryEmit(HandshakeStatus.NOT_STARTED)
_lastHandshake.tryEmit(emptyMap())
}
}
}
@@ -0,0 +1,15 @@
package net.nymtech.nymconnect.service.tunnel.model
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
@Entity
data class Settings(
@Id
var id : Long = 0,
var isAutoTunnelEnabled : Boolean = false,
var isTunnelOnMobileDataEnabled : Boolean = false,
var trustedNetworkSSIDs : MutableList<String> = mutableListOf(),
var defaultTunnel : String? = null,
var isAlwaysOnVpnEnabled : Boolean = false,
)
@@ -0,0 +1,89 @@
package net.nymtech.nymconnect.service.tunnel.model
import com.wireguard.config.Config
import io.objectbox.annotation.ConflictStrategy
import io.objectbox.annotation.Entity
import io.objectbox.annotation.Id
import io.objectbox.annotation.Unique
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.io.InputStream
@Entity
@Serializable
data class TunnelConfig(
@Id
var id : Long = 0,
@Unique(onConflict = ConflictStrategy.REPLACE)
var name : String,
var wgQuick : String
) {
override fun toString(): String {
return Json.encodeToString(serializer(), this)
}
companion object {
private const val INCLUDED_APPLICATIONS = "IncludedApplications = "
private const val EXCLUDED_APPLICATIONS = "ExcludedApplications = "
private const val INTERFACE = "[Interface]"
private const val NEWLINE_CHAR = "\n"
private const val APP_CONFIG_SEPARATOR = ", "
private fun addApplicationsToConfig(appConfig : String, wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val interfaceIndex = configList.indexOf(INTERFACE)
configList.add(interfaceIndex + 1, appConfig)
return configList.joinToString(NEWLINE_CHAR)
}
fun clearAllApplicationsFromConfig(wgQuick : String) : String {
val configList = wgQuick.split(NEWLINE_CHAR).toMutableList()
val itr = configList.iterator()
while (itr.hasNext()) {
val next = itr.next()
if(next.contains(INCLUDED_APPLICATIONS) || next.contains(EXCLUDED_APPLICATIONS)) {
itr.remove()
}
}
return configList.joinToString(NEWLINE_CHAR)
}
fun setExcludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val excludeConfig = buildExcludedApplicationsString(packages)
return addApplicationsToConfig(excludeConfig, clearedWgQuick)
}
fun setIncludedApplicationsOnQuick(packages : List<String>, wgQuick: String) : String {
if(packages.isEmpty()) {
return wgQuick
}
val clearedWgQuick = clearAllApplicationsFromConfig(wgQuick)
val includeConfig = buildIncludedApplicationsString(packages)
return addApplicationsToConfig(includeConfig, clearedWgQuick)
}
private fun buildExcludedApplicationsString(packages : List<String>) : String {
return EXCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
private fun buildIncludedApplicationsString(packages : List<String>) : String {
return INCLUDED_APPLICATIONS + packages.joinToString(APP_CONFIG_SEPARATOR)
}
fun from(string : String) : TunnelConfig {
return Json.decodeFromString<TunnelConfig>(string)
}
fun configFromQuick(wgQuick: String): Config {
val inputStream: InputStream = wgQuick.byteInputStream()
val reader = inputStream.bufferedReader(Charsets.UTF_8)
return Config.parse(reader)
}
}
}
@@ -0,0 +1,201 @@
package net.nymtech.nymconnect.ui
import android.Manifest
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.view.KeyEvent
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInHorizontally
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.input.key.onKeyEvent
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
import com.wireguard.android.backend.GoBackend
import net.nymtech.nymconnect.R
import net.nymtech.nymconnect.ui.common.PermissionRequestFailedScreen
import net.nymtech.nymconnect.ui.common.navigation.BottomNavBar
import net.nymtech.nymconnect.ui.screens.config.ConfigScreen
import net.nymtech.nymconnect.ui.screens.detail.DetailScreen
import net.nymtech.nymconnect.ui.screens.main.MainScreen
import net.nymtech.nymconnect.ui.screens.settings.SettingsScreen
import net.nymtech.nymconnect.ui.screens.support.SupportScreen
import net.nymtech.nymconnect.ui.theme.TransparentSystemBars
import net.nymtech.nymconnect.ui.theme.WireguardAutoTunnelTheme
import dagger.hilt.android.AndroidEntryPoint
import timber.log.Timber
import java.lang.IllegalStateException
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@OptIn(ExperimentalAnimationApi::class,
ExperimentalPermissionsApi::class
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberAnimatedNavController()
val focusRequester = remember { FocusRequester() }
WireguardAutoTunnelTheme {
TransparentSystemBars()
val snackbarHostState = remember { SnackbarHostState() }
val notificationPermissionState =
rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS)
fun requestNotificationPermission() {
if (!notificationPermissionState.status.isGranted && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermissionState.launchPermissionRequest()
}
}
var vpnIntent by remember { mutableStateOf(GoBackend.VpnService.prepare(this)) }
val vpnActivityResultState = rememberLauncherForActivityResult(
ActivityResultContracts.StartActivityForResult(),
onResult = {
val accepted = (it.resultCode == RESULT_OK)
if (accepted) {
vpnIntent = null
}
})
LaunchedEffect(vpnIntent) {
if (vpnIntent != null) {
vpnActivityResultState.launch(vpnIntent)
} else requestNotificationPermission()
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState)},
modifier = Modifier.onKeyEvent {
if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) {
when (it.nativeKeyEvent.keyCode) {
KeyEvent.KEYCODE_DPAD_UP -> {
try {
focusRequester.requestFocus()
} catch(e : IllegalStateException) {
Timber.e("No D-Pad focus request modifier added to element on screen")
}
false
} else -> {
false;
}
}
} else {
false
}
},
bottomBar = if (vpnIntent == null && notificationPermissionState.status.isGranted) {
{ BottomNavBar(navController, Routes.navItems) }
} else {
{}
},
)
{ padding ->
if (vpnIntent != null) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = { vpnActivityResultState.launch(vpnIntent) },
message = getString(R.string.vpn_permission_required),
getString(R.string.retry)
)
return@Scaffold
}
if (!notificationPermissionState.status.isGranted) {
PermissionRequestFailedScreen(
padding = padding,
onRequestAgain = {
val intentSettings =
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
intentSettings.data =
Uri.fromParts("package", this.packageName, null)
startActivity(intentSettings);
},
message = getString(R.string.notification_permission_required),
getString(R.string.open_settings)
)
return@Scaffold
}
AnimatedNavHost(navController, startDestination = Routes.Main.name) {
composable(Routes.Main.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Support.name ->
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
)
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) {
MainScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController)
}
composable(Routes.Settings.name, enterTransition = {
when (initialState.destination.route) {
Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
)
Routes.Support.name -> {
slideInHorizontally(
initialOffsetX = { -1000 },
animationSpec = tween(500)
)
}
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) { SettingsScreen(padding = padding, snackbarHostState = snackbarHostState, navController = navController, focusRequester = focusRequester) }
composable(Routes.Support.name, enterTransition = {
when (initialState.destination.route) {
Routes.Settings.name, Routes.Main.name ->
slideInHorizontally(
initialOffsetX = { 1000 },
animationSpec = tween(500)
)
else -> {
fadeIn(animationSpec = tween(1000))
}
}
}) { SupportScreen(padding = padding, focusRequester) }
composable("${Routes.Config.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { ConfigScreen(padding = padding, navController = navController, id = it.arguments?.getString("id"), focusRequester = focusRequester)}
composable("${Routes.Detail.name}/{id}", enterTransition = {
fadeIn(animationSpec = tween(1000))
}) { DetailScreen(padding = padding, id = it.arguments?.getString("id")) }
}
}
}
}
}
}
@@ -0,0 +1,36 @@
package net.nymtech.nymconnect.ui
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.QuestionMark
import androidx.compose.material.icons.rounded.Settings
import net.nymtech.nymconnect.ui.common.navigation.BottomNavItem
enum class Routes {
Main,
Settings,
Support,
Config,
Detail;
companion object {
val navItems = listOf(
BottomNavItem(
name = "Tunnels",
route = Main.name,
icon = Icons.Rounded.Home,
),
BottomNavItem(
name = "Settings",
route = Settings.name,
icon = Icons.Rounded.Settings,
),
BottomNavItem(
name = "Support",
route = Support.name,
icon = Icons.Rounded.QuestionMark,
)
)
}
}

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