From bfbd509e4b832f07a04aefe670232bf3ad0e0795 Mon Sep 17 00:00:00 2001 From: Fouad Date: Thu, 10 Nov 2022 16:22:09 +0000 Subject: [PATCH 01/75] Update/last minute release updates (#1753) * fix vesting update bond settings * style and text updates * show tx fee when updating node settings * allow cost param update with vesting tokens --- .../src/components/Bonding/BondedMixnode.tsx | 2 +- nym-wallet/src/pages/balance/vesting.tsx | 11 +-- .../settings-pages/NodeUnbondPage.tsx | 9 ++- .../general-settings/InfoSettings.tsx | 78 +++++++++---------- .../general-settings/ParametersSettings.tsx | 53 ++++++++++--- 5 files changed, 90 insertions(+), 63 deletions(-) diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index a30e5557cb..00b3ea7196 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -42,7 +42,7 @@ const headers: Header[] = [ header: 'Operator rewards', id: 'operator-rewards', tooltipText: - 'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.', + 'This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch. You can redeem your rewards at any time.', }, { header: 'No. delegators', diff --git a/nym-wallet/src/pages/balance/vesting.tsx b/nym-wallet/src/pages/balance/vesting.tsx index a3a52a44b1..c58c1454dd 100644 --- a/nym-wallet/src/pages/balance/vesting.tsx +++ b/nym-wallet/src/pages/balance/vesting.tsx @@ -130,16 +130,10 @@ const TokenTransfer = () => { t.palette.nym.text.muted, mt: 2 }}> - Transferable tokens + Unlocked transferable tokens - + {userBalance.tokenAllocation?.spendable || 'n/a'} {clientDetails?.display_mix_denom.toUpperCase()} @@ -167,7 +161,6 @@ export const VestingCard = ({ onTransfer }: { onTransfer: () => Promise }) } Action={ { diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx index 4a6097b8da..f576afccd9 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx @@ -33,10 +33,17 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { + + + diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx index 7b85f1c0c2..31e88a0f31 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx @@ -4,15 +4,20 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { Button, Divider, Typography, TextField, Grid, CircularProgress, Box } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { isMixnode } from 'src/types'; -import { updateMixnodeConfig } from 'src/requests'; +import { simulateUpdateMixnodeConfig, simulateVestingUpdateMixnodeConfig, updateMixnodeConfig } from 'src/requests'; import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; +import { vestingUpdateMixnodeConfig } from 'src/requests/vesting'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { LoadingModal } from 'src/components/Modals/LoadingModal'; export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); + const { getFee, fee, resetFeeState } = useGetFee(); const theme = useTheme(); @@ -33,6 +38,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon verlocPort?: number; httpApiPort?: number; }) => { + resetFeeState(); const { host, version, mixPort, verlocPort, httpApiPort } = data; if (host && version && mixPort && verlocPort && httpApiPort) { const MixNodeConfigParams = { @@ -43,7 +49,11 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon version, }; try { - await updateMixnodeConfig(MixNodeConfigParams); + if (bondedNode.proxy) { + await vestingUpdateMixnodeConfig(MixNodeConfigParams); + } else { + await updateMixnodeConfig(MixNodeConfigParams); + } setOpenConfirmationModal(true); } catch (error) { Console.error(error); @@ -53,10 +63,22 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon return ( + {fee && ( + onSubmit(d))} + onPrev={resetFeeState} + onClose={resetFeeState} + /> + )} + {isSubmitting && } - Your changes will be ONLY saved on the display. Remember to change the values on your node’s config file too + Changing these values will ONLY change the data about your node on the blockchain. Remember to change your + node’s config file with the same values too } dismissable @@ -67,16 +89,6 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon Port - (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), - }} - > - Change profit margin of your node - @@ -120,16 +132,6 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon Host - (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), - }} - > - Lock wallet after certain time - @@ -151,16 +153,6 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon Version - (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), - }} - > - Lock wallet after certain time - @@ -182,18 +174,24 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon size="large" variant="contained" disabled={isSubmitting || !isDirty || !isValid} - onClick={handleSubmit((d) => onSubmit(d))} - type="submit" - sx={{ m: 3, width: '320px' }} - endIcon={isSubmitting && } + onClick={handleSubmit((data) => + getFee(bondedNode.proxy ? simulateVestingUpdateMixnodeConfig : simulateUpdateMixnodeConfig, { + host: data.host, + mix_port: data.mixPort, + verloc_port: data.verlocPort, + http_api_port: data.httpApiPort, + version: data.version, + }), + )} + sx={{ m: 3 }} > - Save all display changes + Submit changes to the blockchain diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx index ad9b3d03af..aebb21c8c1 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx @@ -14,17 +14,27 @@ import { } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { CurrencyDenom, MixNodeCostParams } from '@nymproject/types'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { add, format, fromUnixTime } from 'date-fns'; import { isMixnode } from 'src/types'; -import { getCurrentInterval, getPendingIntervalEvents, updateMixnodeCostParams } from 'src/requests'; -import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; +import { + getCurrentInterval, + getPendingIntervalEvents, + simulateUpdateMixnodeCostParams, + simulateVestingUpdateMixnodeCostParams, + updateMixnodeCostParams, + vestingUpdateMixnodeCostParams, +} from 'src/requests'; +import { TBondedMixnode } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { ChangeMixCostParams } from 'src/pages/bonding/types'; import { AppContext } from 'src/context'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { LoadingModal } from 'src/components/Modals/LoadingModal'; export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }): JSX.Element => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); @@ -34,6 +44,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode const { clientDetails } = useContext(AppContext); const theme = useTheme(); + const { fee, getFee, resetFeeState } = useGetFee(); + const defaultValues = { operatorCost: bondedNode.operatorCost, profitMargin: bondedNode.profitMargin, @@ -96,6 +108,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }, []); const onSubmit = async (data: { operatorCost: { amount: string; denom: CurrencyDenom }; profitMargin: string }) => { + resetFeeState(); if (data.operatorCost && data.profitMargin) { const MixNodeCostParams = { profit_margin_percent: (+data.profitMargin / 100).toString(), @@ -105,7 +118,11 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }, }; try { - await updateMixnodeCostParams(MixNodeCostParams); + if (bondedNode.proxy) { + await vestingUpdateMixnodeCostParams(MixNodeCostParams); + } else { + await updateMixnodeCostParams(MixNodeCostParams); + } await getPendingEvents(); reset(); setOpenConfirmationModal(true); @@ -117,6 +134,17 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode return ( + {fee && ( + onSubmit(d))} + onPrev={resetFeeState} + onClose={resetFeeState} + /> + )} + {isSubmitting && } @@ -129,7 +157,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode /> - + Profit Margin @@ -223,12 +251,16 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode size="large" variant="contained" disabled={isSubmitting || !isDirty || !isValid} - onClick={handleSubmit(onSubmit)} + onClick={handleSubmit((data) => { + getFee(bondedNode.proxy ? simulateVestingUpdateMixnodeCostParams : simulateUpdateMixnodeCostParams, { + profit_margin_percent: (+data.profitMargin / 100).toString(), + interval_operating_cost: data.operatorCost, + }); + })} type="submit" - sx={{ m: 3, width: '320px' }} - endIcon={isSubmitting && } + sx={{ m: 3 }} > - Save all display changes + Submit changes to the blockchain @@ -236,7 +268,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode open={openConfirmationModal} header="Your changes will take place in the next interval" - okLabel="close" + okLabel="Close" hideCloseIcon displayInfoIcon onOk={async () => { @@ -256,7 +288,6 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode textAlign: 'center', color: theme.palette.nym.nymWallet.text.blue, fontSize: 16, - textTransform: 'capitalize', }} subHeaderStyles={{ m: 0, From 8774b22d8403bc27d7db1992ec6221db8fc62d60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 15 Nov 2022 16:06:03 +0000 Subject: [PATCH 02/75] Added migration code for updating vesting contract address --- .../mixnet-contract/src/msg.rs | 4 +++- contracts/mixnet/src/contract.rs | 14 ++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index c6fb28063e..8950b10055 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -413,4 +413,6 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg {} +pub struct MigrateMsg { + pub vesting_contract_address: String, +} diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 1951c535de..7e4c8de786 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -467,10 +467,20 @@ pub fn query( #[entry_point] pub fn migrate( - _deps: DepsMut<'_>, + deps: DepsMut<'_>, _env: Env, - _msg: MigrateMsg, + msg: MigrateMsg, ) -> Result { + // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address + // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh + // environment, one of the contracts will HAVE TO go through a migration + if !msg.vesting_contract_address.is_empty() { + let mut current_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; + current_state.vesting_contract_address = + deps.api.addr_validate(&msg.vesting_contract_address)?; + mixnet_params_storage::CONTRACT_STATE.save(deps.storage, ¤t_state)?; + } + Ok(Default::default()) } From f46c0142e7160d747b88b410ff36b2a4e2a3e8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 15 Nov 2022 16:28:15 +0000 Subject: [PATCH 03/75] Updated changelog --- contracts/CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 3de60bd6f4..013be328af 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -1,3 +1,11 @@ +## Unreleased + +## Added + +- Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759]) + +[#1759]: https://github.com/nymtech/nym/pull/1759 + ## [nym-contracts-v1.1.0](https://github.com/nymtech/nym/tree/nym-contracts-v1.1.0) (2022-11-09) ### Changed From b7aa84cd5a5589b4921feb6a4633a4a2de530d14 Mon Sep 17 00:00:00 2001 From: benedettadavico Date: Tue, 15 Nov 2022 19:54:28 +0100 Subject: [PATCH 04/75] updating mainnet mixnet conract address --- common/network-defaults/src/mainnet.rs | 2 +- nym-wallet/nym-wallet-types/src/network.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index ed415e2622..9e93b9194b 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -10,7 +10,7 @@ pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = - "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"; + "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 1be3c80732..7a0e01c527 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -160,9 +160,9 @@ mod qa { pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = - "n1frq2hzkjtatsupc6jtyaz67ytydk9nya437q92qg76ny3y8fcnjsw806vg"; - pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"; + pub(crate) const VESTING_CONTRACT_ADDRESS: &str = + "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = @@ -177,8 +177,8 @@ mod qa { //pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0"; pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new( - "https://adv-epoch-qa-validator.qa.nymte.ch/", - Some("https://adv-epoch-qa-val-api.qa.nymte.ch/api"), + "https://qwerty-validator.qa.nymte.ch/", + Some("https://qwerty-validator-api.qa.nymte.ch/api"), )] } From 8f52f34bc4868296cc40a9355c8c3c0381a14cea Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Wed, 16 Nov 2022 11:43:17 +0000 Subject: [PATCH 05/75] GitHub Actions: add workflow to build network explorer api --- .github/workflows/network-explorer-api.yml | 53 ++++++++++++++++++++++ Makefile | 3 ++ 2 files changed, 56 insertions(+) create mode 100644 .github/workflows/network-explorer-api.yml diff --git a/.github/workflows/network-explorer-api.yml b/.github/workflows/network-explorer-api.yml new file mode 100644 index 0000000000..6464d32b96 --- /dev/null +++ b/.github/workflows/network-explorer-api.yml @@ -0,0 +1,53 @@ +name: CI for Network Explorer API + +on: + workflow_dispatch: + release: + types: [created] + +env: + NETWORK: mainnet + +jobs: + publish-nym: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest] + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v3 + + - name: Check the release tag starts with `nym-explorer-api-` + if: startsWith(github.ref, 'refs/tags/nym-explorer-api-') == false && github.event_name != 'workflow_dispatch' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Release tag did not start with nym-explorer-api-...') + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Build all explorer-api + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path explorer-api/Cargo.toml --workspace --release + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: my-artifact + path: | + target/release/explorer-api + retention-days: 30 + + - name: Upload to release based on tag name + uses: softprops/action-gh-release@v1 + if: github.event_name == 'release' + with: + files: | + target/release/explorer-api diff --git a/Makefile b/Makefile index 95d163a7ca..8f5aba2678 100644 --- a/Makefile +++ b/Makefile @@ -92,6 +92,9 @@ build-wallet: build-connect: cargo build --manifest-path nym-connect/Cargo.toml --workspace +build-explorer-api: + cargo build --manifest-path explorer-api/Cargo.toml --workspace + build-wasm-client: cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown From b84486c0f48fce65e6818689fccc51138412aca2 Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Wed, 16 Nov 2022 11:56:26 +0000 Subject: [PATCH 06/75] GitHub Actions: install packages needed for build --- .github/workflows/network-explorer-api.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/network-explorer-api.yml b/.github/workflows/network-explorer-api.yml index 6464d32b96..2ad184be72 100644 --- a/.github/workflows/network-explorer-api.yml +++ b/.github/workflows/network-explorer-api.yml @@ -19,6 +19,9 @@ jobs: steps: - uses: actions/checkout@v3 + - 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 libudev-dev squashfs-tools + - name: Check the release tag starts with `nym-explorer-api-` if: startsWith(github.ref, 'refs/tags/nym-explorer-api-') == false && github.event_name != 'workflow_dispatch' uses: actions/github-script@v3 From a803c7f25e8ccd6f4f101c8da7aae2c3495eaa70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Wed, 16 Nov 2022 16:06:03 +0100 Subject: [PATCH 07/75] Set output --- .github/workflows/nightly_build_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 0347d1f755..7723cb599a 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -25,7 +25,7 @@ jobs: - name: Set output variable to latest release branch id: step2 - run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1)" >> $GITHUB_OUTPUT + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/origin\///')" >> $GITHUB_OUTPUT build: needs: [get_release,matrix_prep] strategy: @@ -40,7 +40,7 @@ jobs: - name: Check out latest release branch uses: actions/checkout@v3 with: - ref: release/v1.1.1 + ref: ${{needs.get_release.outputs.output1}} - name: Install rust toolchain uses: actions-rs/toolchain@v1 From 8e928019294de7b4f013c00ba2cd6cdc2a3fe86b Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Wed, 16 Nov 2022 16:45:59 +0000 Subject: [PATCH 08/75] Reduce logging in explorer api --- explorer-api/src/country_statistics/geolocate.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 42fe9405be..c77a61a9a1 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -95,12 +95,12 @@ impl GeoLocateTask { .await; } }, - Err(e) => { - warn!( - "❌ Oh no! Location for {} failed. Error: {:#?}", - cache_item.mix_node().host, - e - ); + Err(_e) => { + // warn!( + // "❌ Oh no! Location for {} failed. Error: {:#?}", + // cache_item.mix_node().host, + // e + // ); } }; } From d108edb424ffbfccf58a3fa3486d6c89deacdb6f Mon Sep 17 00:00:00 2001 From: Pierre Dommerc Date: Thu, 17 Nov 2022 15:15:45 +0100 Subject: [PATCH 09/75] fix(nym-cli): typo in sign command json output (#1768) --- common/commands/src/validator/signature/sign.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs index d26302aeba..cd4c169ae1 100644 --- a/common/commands/src/validator/signature/sign.rs +++ b/common/commands/src/validator/signature/sign.rs @@ -13,7 +13,7 @@ use validator_client::nymd::wallet::DirectSecp256k1HdWallet; pub struct SignatureOutputJson { pub account_id: String, pub public_key: PublicKey, - pub signature: String, + pub signature_as_hex: String, } #[derive(Debug, Parser)] @@ -46,7 +46,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option) { let output = SignatureOutputJson { account_id: account.address().to_string(), public_key: account.public_key(), - signature: signature.to_string(), + signature_as_hex: signature.to_string(), }; println!("{}", json!(output)); } From ce14e40968fcc1ed3257d240f0ed55923e15f290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 15 Nov 2022 11:24:29 +0100 Subject: [PATCH 10/75] websocket-requests: fix deserialize length check --- clients/native/websocket-requests/src/requests.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index ea44059250..565a184df9 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -62,10 +62,10 @@ impl ClientRequest { .collect() } - // SEND_REQUEST_TAG || with_reply || recipient || data_len || data + // SEND_REQUEST_TAG || with_reply || recipient || conn_id || data_len || data fn deserialize_send(b: &[u8]) -> Result { - // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + sizeof bytes - if b.len() < 2 + Recipient::LEN + size_of::() { + // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + 2*sizeof bytes + if b.len() < 2 + Recipient::LEN + 2 * size_of::() { return Err(error::Error::new( ErrorKind::TooShortRequest, "not enough data provided to recover 'send'".to_string(), From 0d71ac5e75580a1c0acb557d35d51916b29b9e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 15 Nov 2022 11:24:55 +0100 Subject: [PATCH 11/75] websocket-requests: fix lints in file --- clients/native/src/client/mod.rs | 2 +- clients/native/src/websocket/handler.rs | 70 +++++++++---------- .../native/websocket-requests/src/requests.rs | 12 ++-- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index df77a7fb77..f2aee9829f 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -291,7 +291,7 @@ impl NymClient { msg_input, closed_connection_tx, buffer_requester, - self.as_mix_recipient(), + &self.as_mix_recipient(), ); websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler); diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index f43fbca4c8..9830b6fdd0 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -69,13 +69,13 @@ impl Handler { msg_input: InputMessageSender, closed_connection_tx: ClosedConnectionSender, buffer_requester: ReceivedBufferRequestSender, - self_full_address: Recipient, + self_full_address: &Recipient, ) -> Self { Handler { msg_input, closed_connection_tx, buffer_requester, - self_full_address, + self_full_address: *self_full_address, socket: None, received_response_type: Default::default(), } @@ -83,14 +83,14 @@ impl Handler { fn handle_send( &mut self, - recipient: Recipient, + recipient: &Recipient, message: Vec, with_reply_surb: bool, connection_id: u64, ) -> Option { // the ack control is now responsible for chunking, etc. let lane = TransmissionLane::ConnectionId(connection_id); - let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); + let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); self.msg_input.unbounded_send(input_msg).unwrap(); None @@ -125,7 +125,7 @@ impl Handler { message, with_reply_surb, connection_id, - } => self.handle_send(recipient, message, with_reply_surb, connection_id), + } => self.handle_send(&recipient, message, with_reply_surb, connection_id), ClientRequest::Reply { message, reply_surb, @@ -150,7 +150,7 @@ impl Handler { response.map(|resp| WsMessage::text(resp.into_text())) } - fn handle_binary_message(&mut self, msg: Vec) -> Option { + fn handle_binary_message(&mut self, msg: &[u8]) -> Option { debug!("Handling binary message request"); self.received_response_type = ReceivedResponseType::Binary; @@ -170,37 +170,11 @@ impl Handler { // old version of this file. match raw_request { WsMessage::Text(text_message) => self.handle_text_message(text_message), - WsMessage::Binary(binary_message) => self.handle_binary_message(binary_message), + WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message), _ => None, } } - // I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but - // let's just play along for now - fn prepare_reconstructed_binary( - &self, - reconstructed_messages: Vec, - ) -> Vec> { - reconstructed_messages - .into_iter() - .map(ServerResponse::Received) - .map(|resp| Ok(WsMessage::Binary(resp.into_binary()))) - .collect() - } - - // I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but - // let's just play along for now - fn prepare_reconstructed_text( - &self, - reconstructed_messages: Vec, - ) -> Vec> { - reconstructed_messages - .into_iter() - .map(ServerResponse::Received) - .map(|resp| Ok(WsMessage::Text(resp.into_text()))) - .collect() - } - async fn push_websocket_received_plaintexts( &mut self, reconstructed_messages: Vec, @@ -209,10 +183,8 @@ impl Handler { // if it's text or binary, but for time being we use the naive assumption that if // client is sending Message::Text it expects text back. Same for Message::Binary let response_messages = match self.received_response_type { - ReceivedResponseType::Binary => { - self.prepare_reconstructed_binary(reconstructed_messages) - } - ReceivedResponseType::Text => self.prepare_reconstructed_text(reconstructed_messages), + ReceivedResponseType::Binary => prepare_reconstructed_binary(reconstructed_messages), + ReceivedResponseType::Text => prepare_reconstructed_text(reconstructed_messages), }; let mut send_stream = futures::stream::iter(response_messages); @@ -307,3 +279,27 @@ impl Handler { self.listen_for_requests(reconstructed_receiver).await; } } + +// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but +// let's just play along for now +fn prepare_reconstructed_binary( + reconstructed_messages: Vec, +) -> Vec> { + reconstructed_messages + .into_iter() + .map(ServerResponse::Received) + .map(|resp| Ok(WsMessage::Binary(resp.into_binary()))) + .collect() +} + +// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but +// let's just play along for now +fn prepare_reconstructed_text( + reconstructed_messages: Vec, +) -> Vec> { + reconstructed_messages + .into_iter() + .map(ServerResponse::Received) + .map(|resp| Ok(WsMessage::Text(resp.into_text()))) + .collect() +} diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index 565a184df9..f681b4264c 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -127,7 +127,7 @@ impl ClientRequest { } // REPLY_REQUEST_TAG || surb_len || surb || message_len || message - fn serialize_reply(message: Vec, reply_surb: ReplySurb) -> Vec { + fn serialize_reply(message: Vec, reply_surb: &ReplySurb) -> Vec { let reply_surb_bytes = reply_surb.to_bytes(); let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); let message_len_bytes = (message.len() as u64).to_be_bytes(); @@ -225,7 +225,7 @@ impl ClientRequest { fn serialize_closed_connection(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); std::iter::once(CLOSED_CONNECTION_REQUEST_TAG) - .chain(conn_id_bytes.iter().cloned()) + .chain(conn_id_bytes.iter().copied()) .collect() } @@ -253,7 +253,7 @@ impl ClientRequest { ClientRequest::Reply { message, reply_surb, - } => Self::serialize_reply(message, reply_surb), + } => Self::serialize_reply(message, &reply_surb), ClientRequest::SelfAddress => Self::serialize_self_address(), @@ -290,13 +290,13 @@ impl ClientRequest { CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)), n => Err(error::Error::new( ErrorKind::UnknownRequest, - format!("type {}", n), + format!("type {n}"), )), } } - pub fn try_from_binary(raw_req: Vec) -> Result { - Self::deserialize(&raw_req) + pub fn try_from_binary(raw_req: &[u8]) -> Result { + Self::deserialize(raw_req) } pub fn try_from_text(raw_req: String) -> Result { From 6946151b25bed336a6ad1ff4da2db718ca3354bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 17 Nov 2022 17:13:58 +0000 Subject: [PATCH 12/75] Made vesting_contract_address argument peroperly optional (#1769) * Made vesting_contract_address argument peroperly optional * Updated changelog --- common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs | 2 +- contracts/CHANGELOG.md | 3 ++- contracts/mixnet/src/contract.rs | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 8950b10055..1c00c64a9c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -414,5 +414,5 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct MigrateMsg { - pub vesting_contract_address: String, + pub vesting_contract_address: Option, } diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 013be328af..f57a633956 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -2,9 +2,10 @@ ## Added -- Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759]) +- Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759],[#1769]) [#1759]: https://github.com/nymtech/nym/pull/1759 +[#1769]: https://github.com/nymtech/nym/pull/1769 ## [nym-contracts-v1.1.0](https://github.com/nymtech/nym/tree/nym-contracts-v1.1.0) (2022-11-09) diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 7e4c8de786..e07b7ba32a 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -474,10 +474,10 @@ pub fn migrate( // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh // environment, one of the contracts will HAVE TO go through a migration - if !msg.vesting_contract_address.is_empty() { + if let Some(vesting_contract_address) = msg.vesting_contract_address { let mut current_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; current_state.vesting_contract_address = - deps.api.addr_validate(&msg.vesting_contract_address)?; + deps.api.addr_validate(&vesting_contract_address)?; mixnet_params_storage::CONTRACT_STATE.save(deps.storage, ¤t_state)?; } From 09b51226c2cb02a22f604954789ff6497038656b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 18 Nov 2022 10:24:28 +0100 Subject: [PATCH 13/75] client: add LaneQueueLength to ServerResponse (#1772) * client: add LaneQueueLength to ServerResponse * fix test compilation * changlog: add note --- CHANGELOG.md | 1 + .../websocket-requests/src/responses.rs | 47 +++++++++++++++++++ clients/native/websocket-requests/src/text.rs | 7 +++ 3 files changed, 55 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 369c290aab..d21a49d7fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### Added - binaries: add `-c` shortform for `--config-env-file` +- websocket-requests: add server response signalling current packet queue length in the client ### Changed diff --git a/clients/native/websocket-requests/src/responses.rs b/clients/native/websocket-requests/src/responses.rs index 603650f0b8..7a32645957 100644 --- a/clients/native/websocket-requests/src/responses.rs +++ b/clients/native/websocket-requests/src/responses.rs @@ -23,10 +23,14 @@ pub const RECEIVED_RESPONSE_TAG: u8 = 0x01; /// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02; +/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`] +pub const LANE_QUEUE_LENGTH_RESPONSE_TAG: u8 = 0x03; + #[derive(Debug)] pub enum ServerResponse { Received(ReconstructedMessage), SelfAddress(Recipient), + LaneQueueLength(u64, usize), Error(error::Error), } @@ -193,6 +197,31 @@ impl ServerResponse { Ok(ServerResponse::SelfAddress(recipient)) } + // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length + fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec { + std::iter::once(LANE_QUEUE_LENGTH_RESPONSE_TAG) + .chain(lane.to_be_bytes().iter().cloned()) + .chain(queue_length.to_be_bytes().iter().cloned()) + .collect() + } + + // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length + fn deserialize_lane_queue_length(b: &[u8]) -> Result { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], LANE_QUEUE_LENGTH_RESPONSE_TAG); + + let mut lane_bytes = [0u8; size_of::()]; + lane_bytes.copy_from_slice(&b[1..=size_of::()]); + let lane = u64::from_be_bytes(lane_bytes); + + let mut queue_length_bytes = [0u8; size_of::()]; + queue_length_bytes + .copy_from_slice(&b[1 + size_of::()..1 + size_of::() + size_of::()]); + let queue_length = usize::from_be_bytes(queue_length_bytes); + + Ok(ServerResponse::LaneQueueLength(lane, queue_length)) + } + // ERROR_RESPONSE_TAG || err_code || msg_len || msg fn serialize_error(error: error::Error) -> Vec { let message_len_bytes = (error.message.len() as u64).to_be_bytes(); @@ -272,6 +301,9 @@ impl ServerResponse { Self::serialize_received(reconstructed_message) } ServerResponse::SelfAddress(address) => Self::serialize_self_address(address), + ServerResponse::LaneQueueLength(lane, queue_length) => { + Self::serialize_lane_queue_length(lane, queue_length) + } ServerResponse::Error(err) => Self::serialize_error(err), } } @@ -302,6 +334,7 @@ impl ServerResponse { match response_tag { RECEIVED_RESPONSE_TAG => Self::deserialize_received(b), SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b), + LANE_QUEUE_LENGTH_RESPONSE_TAG => Self::deserialize_lane_queue_length(b), ERROR_RESPONSE_TAG => Self::deserialize_error(b), n => Err(error::Error::new( ErrorKind::UnknownResponse, @@ -378,6 +411,20 @@ mod tests { } } + #[test] + fn lane_queue_length_response_serialization_works() { + let lane_queue_length_response = ServerResponse::LaneQueueLength(13, 42); + let bytes = lane_queue_length_response.serialize(); + let recovered = ServerResponse::deserialize(&bytes).unwrap(); + match recovered { + ServerResponse::LaneQueueLength(lane, queue_length) => { + assert_eq!(lane, 13); + assert_eq!(queue_length, 42) + } + _ => unreachable!(), + } + } + #[test] fn error_response_serialization_works() { let dummy_error = error::Error::new(ErrorKind::UnknownRequest, "foomp message".to_string()); diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs index 5a9f9dcf01..cca6569f4f 100644 --- a/clients/native/websocket-requests/src/text.rs +++ b/clients/native/websocket-requests/src/text.rs @@ -94,6 +94,10 @@ pub(super) enum ServerResponseText { SelfAddress { address: String, }, + LaneQueueLength { + lane: u64, + queue_length: usize, + }, Error { message: String, }, @@ -135,6 +139,9 @@ impl From for ServerResponseText { ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress { address: recipient.to_string(), }, + ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponseText::LaneQueueLength { lane, queue_length } + } ServerResponse::Error(err) => ServerResponseText::Error { message: err.to_string(), }, From 9f56796bf61c8f4a9b06756a8b452790bcedf49b Mon Sep 17 00:00:00 2001 From: Dave Hrycyszyn Date: Fri, 18 Nov 2022 12:28:21 +0000 Subject: [PATCH 14/75] Removing unused nym-chitchat experiment --- nym-chitchat/Cargo.lock | 1983 ----------------------------------- nym-chitchat/Cargo.toml | 28 - nym-chitchat/README.md | 15 - nym-chitchat/run-servers.sh | 15 - nym-chitchat/src/main.rs | 123 --- 5 files changed, 2164 deletions(-) delete mode 100644 nym-chitchat/Cargo.lock delete mode 100644 nym-chitchat/Cargo.toml delete mode 100644 nym-chitchat/README.md delete mode 100755 nym-chitchat/run-servers.sh delete mode 100644 nym-chitchat/src/main.rs diff --git a/nym-chitchat/Cargo.lock b/nym-chitchat/Cargo.lock deleted file mode 100644 index 4c396ed66d..0000000000 --- a/nym-chitchat/Cargo.lock +++ /dev/null @@ -1,1983 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "aead" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" -dependencies = [ - "generic-array", -] - -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", - "opaque-debug", -] - -[[package]] -name = "aes-gcm" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "anyhow" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" - -[[package]] -name = "assert_cmd" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" -dependencies = [ - "bstr", - "doc-comment", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-trait" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", -] - -[[package]] -name = "bumpalo" -version = "3.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - -[[package]] -name = "bytes" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chitchat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e16bc6492a6744d6e322e22562c3ab89404ba326aedb65a570799d2e042687" -dependencies = [ - "anyhow", - "async-trait", - "bytes 1.2.0", - "rand", - "serde", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "chitchat-test" -version = "0.3.0" -dependencies = [ - "anyhow", - "assert_cmd", - "chitchat", - "cool-id-generator", - "env_logger", - "once_cell", - "poem", - "poem-openapi", - "predicates", - "reqwest", - "serde", - "serde_json", - "structopt", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "time 0.1.44", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" -dependencies = [ - "aes-gcm", - "base64 0.13.0", - "hkdf", - "hmac", - "percent-encoding", - "rand", - "sha2", - "subtle", - "time 0.3.11", - "version_check", -] - -[[package]] -name = "cool-id-generator" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946cf88fb171128bc025588b9b408982814ace1a1fb31ffb9bfa63d955f0b43d" -dependencies = [ - "rand", -] - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" -dependencies = [ - "cipher", -] - -[[package]] -name = "darling" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "either" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" - -[[package]] -name = "encoding_rs" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "fastrand" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" - -[[package]] -name = "futures-io" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" - -[[package]] -name = "futures-macro" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" - -[[package]] -name = "futures-task" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-util" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "ghash" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "h2" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" -dependencies = [ - "bytes 1.2.0", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "headers" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" -dependencies = [ - "base64 0.13.0", - "bitflags", - "bytes 1.2.0", - "headers-core", - "http", - "httpdate", - "mime", - "sha-1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" -dependencies = [ - "bytes 1.2.0", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes 1.2.0", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" -dependencies = [ - "bytes 1.2.0", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "js-sys" -version = "0.3.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "lock_api" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[package]] -name = "mio" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "multer" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30ba6d97eb198c5e8a35d67d5779d6680cca35652a60ee90fc23dc431d4fde8" -dependencies = [ - "bytes 1.2.0", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin", - "tokio", - "version_check", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys", -] - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "poem" -version = "1.3.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e479ed477ea11939bea754109eaa024e55bf1639e19c1753fb250026351da8" -dependencies = [ - "async-trait", - "bytes 1.2.0", - "chrono", - "cookie", - "futures-util", - "headers", - "http", - "hyper", - "mime", - "multer", - "parking_lot", - "percent-encoding", - "pin-project-lite", - "poem-derive", - "regex", - "rfc7239", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "tempfile", - "thiserror", - "time 0.3.11", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "typed-headers", -] - -[[package]] -name = "poem-derive" -version = "1.3.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d454ac05b5299eb8e6261214fcb823f0c3c1b02f47167f8809c9dc2db0ee033" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "poem-openapi" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cd1c97d45a9552b0c80bab1d51f93a1050c3f7f91e9312239b3b87c336b0f" -dependencies = [ - "base64 0.13.0", - "bytes 1.2.0", - "derive_more", - "futures-util", - "mime", - "num-traits", - "poem", - "poem-openapi-derive", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "thiserror", - "tokio", -] - -[[package]] -name = "poem-openapi-derive" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5105e426a8a747fefc4be06ab7705cb63bc3ab1b2db373867e1d83b1d95877a" -dependencies = [ - "Inflector", - "darling", - "http", - "indexmap", - "mime", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn", - "thiserror", -] - -[[package]] -name = "polyval" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "predicates" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" -dependencies = [ - "difflib", - "float-cmp", - "itertools", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" - -[[package]] -name = "predicates-tree" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror", - "toml", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - -[[package]] -name = "regex-syntax" -version = "0.6.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "reqwest" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" -dependencies = [ - "base64 0.13.0", - "bytes 1.2.0", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rfc7239" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" -dependencies = [ - "uncased", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "semver" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" - -[[package]] -name = "serde" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" -dependencies = [ - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" -dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", -] - -[[package]] -name = "serde_yaml" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "slab" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" - -[[package]] -name = "socket2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" -dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "termtree" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" -dependencies = [ - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" -dependencies = [ - "itoa", - "libc", - "num_threads", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "tokio" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" -dependencies = [ - "autocfg", - "bytes 1.2.0", - "libc", - "memchr", - "mio", - "num_cpus", - "once_cell", - "pin-project-lite", - "socket2", - "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-macros" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-stream" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" -dependencies = [ - "bytes 1.2.0", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" -dependencies = [ - "serde", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" -dependencies = [ - "ansi_term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "typed-headers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3179a61e9eccceead5f1574fd173cf2e162ac42638b9bf214c6ad0baf7efa24a" -dependencies = [ - "base64 0.11.0", - "bytes 0.5.6", - "chrono", - "http", - "mime", -] - -[[package]] -name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "uncased" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" - -[[package]] -name = "unicode-ident" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" - -[[package]] -name = "unicode-normalization" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "universal-hash" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "url" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" -dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" - -[[package]] -name = "web-sys" -version = "0.3.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-sys" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/nym-chitchat/Cargo.toml b/nym-chitchat/Cargo.toml deleted file mode 100644 index eae29e5738..0000000000 --- a/nym-chitchat/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "chitchat-test" -version = "0.3.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -chitchat = "0.4" -poem = "1" -poem-openapi = {version="2", features = ["swagger-ui"] } -structopt = "0.3" -tokio = { version = "1.14.0", features = ["net", "sync", "rt-multi-thread", "macros", "time"] } -serde = { version="1", features=["derive"] } -serde_json = "1" -anyhow = "1" -once_cell = "1" -tracing = "0.1" -tracing-subscriber = "0.3" -cool-id-generator = "1" -env_logger = "0.9" - -[dev-dependencies] -assert_cmd = "2" -predicates = "2" -reqwest = { version = "0.11", default-features=false, features = ["blocking", "json"] } - -[workspace] diff --git a/nym-chitchat/README.md b/nym-chitchat/README.md deleted file mode 100644 index 2fd61cd169..0000000000 --- a/nym-chitchat/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Chitchat test - -Runs simple chitchat servers, mostly copied over from https://github.com/quickwit-oss/chitchat - -## Example - -```bash -# Starts 5 servers and joins them into a cluster on localhost ports 10000-10004 -# All servers print cluster state on `/` ie 127.0.0.1:10000 -# `/docs` endpoint has an open api with a key value setter, set it on one node and observe how the state propagates to the other nodes -# NodeState is a regular BTreeMap -./run-servers.sh - -# run killall chitchat-test after you're done, as the servers will continue to run forever in the background -``` diff --git a/nym-chitchat/run-servers.sh b/nym-chitchat/run-servers.sh deleted file mode 100755 index 9e08abac3c..0000000000 --- a/nym-chitchat/run-servers.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -killall chitchat-test - -cargo build --release - -for i in $(seq 10000 10004) -do - listen_addr="127.0.0.1:$i"; - echo ${listen_addr}; - cargo run --release -- --listen_addr ${listen_addr} --seed 127.0.0.1:10000 --node_id node_$i & -done; - -read -kill 0 diff --git a/nym-chitchat/src/main.rs b/nym-chitchat/src/main.rs deleted file mode 100644 index dc0899913d..0000000000 --- a/nym-chitchat/src/main.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use chitchat::transport::UdpTransport; -use chitchat::{spawn_chitchat, Chitchat, ChitchatConfig, FailureDetectorConfig, NodeId}; -use cool_id_generator::Size; -use poem::listener::TcpListener; -use poem::{Route, Server}; -use poem_openapi::param::Query; -use poem_openapi::payload::Json; -use poem_openapi::{OpenApi, OpenApiService}; -use structopt::StructOpt; -use tokio::sync::Mutex; - -use chitchat::ClusterStateSnapshot; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct ApiResponse { - pub cluster_id: String, - pub cluster_state: ClusterStateSnapshot, - pub live_nodes: Vec, - pub dead_nodes: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SetKeyValueResponse { - pub status: bool, -} - -struct Api { - chitchat: Arc>, -} - -#[OpenApi] -impl Api { - /// Chitchat state - #[oai(path = "/", method = "get")] - async fn index(&self) -> Json { - let chitchat_guard = self.chitchat.lock().await; - let response = ApiResponse { - cluster_id: chitchat_guard.cluster_id().to_string(), - cluster_state: chitchat_guard.state_snapshot(), - live_nodes: chitchat_guard.live_nodes().cloned().collect::>(), - dead_nodes: chitchat_guard.dead_nodes().cloned().collect::>(), - }; - Json(serde_json::to_value(&response).unwrap()) - } - - /// Set a key & value on this node (with no validation). - #[oai(path = "/set_kv/", method = "get")] - async fn set_kv(&self, key: Query, value: Query) -> Json { - let mut chitchat_guard = self.chitchat.lock().await; - - let cc_state = chitchat_guard.self_node_state(); - cc_state.set(key.as_str(), value.as_str()); - - Json(serde_json::to_value(&SetKeyValueResponse { status: true }).unwrap()) - } -} - -#[derive(Debug, StructOpt)] -#[structopt(name = "chitchat", about = "Chitchat test server.")] -struct Opt { - /// Defines the socket addr on which we should listen to. - #[structopt(long = "listen_addr", default_value = "127.0.0.1:10000")] - listen_addr: SocketAddr, - /// Defines the socket_address (host:port) other servers should use to - /// reach this server. - /// - /// It defaults to the listen address, but this is only valid - /// when all server are running on the same server. - #[structopt(long = "public_addr")] - public_addr: Option, - - /// Node id. Has to be unique. If None, the node_id will be generated from - /// the public_addr and a random suffix. - #[structopt(long = "node_id")] - node_id: Option, - - #[structopt(long = "seed")] - seeds: Vec, - - #[structopt(long = "interval_ms", default_value = "500")] - interval: u64, -} - -fn generate_server_id(public_addr: SocketAddr) -> String { - let cool_id = cool_id_generator::get_id(Size::Medium); - format!("server:{}-{}", public_addr, cool_id) -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt::init(); - let opt = Opt::from_args(); - println!("{:?}", opt); - let public_addr = opt.public_addr.unwrap_or(opt.listen_addr); - let node_id_str = opt - .node_id - .unwrap_or_else(|| generate_server_id(public_addr)); - let node_id = NodeId::new(node_id_str, public_addr); - let config = ChitchatConfig { - node_id, - cluster_id: "testing".to_string(), - gossip_interval: Duration::from_millis(opt.interval), - listen_addr: opt.listen_addr, - seed_nodes: opt.seeds.clone(), - failure_detector_config: FailureDetectorConfig::default(), - }; - let chitchat_handler = spawn_chitchat(config, Vec::new(), &UdpTransport).await?; - let chitchat = chitchat_handler.chitchat(); - let api = Api { chitchat }; - let api_service = OpenApiService::new(api, "Hello World", "1.0") - .server(&format!("http://{}/", opt.listen_addr)); - let docs = api_service.swagger_ui(); - let app = Route::new().nest("/", api_service).nest("/docs", docs); - Server::new(TcpListener::bind(&opt.listen_addr)) - .run(app) - .await?; - Ok(()) -} From 87fb4daeda69b6a04eeadced48da08eff0f751af Mon Sep 17 00:00:00 2001 From: benedetta davico <46782255+benedettadavico@users.noreply.github.com> Date: Fri, 18 Nov 2022 13:38:16 +0100 Subject: [PATCH 15/75] Release/v1.1.1 nym wallet (#1775) * fix undelegating with vesting tokens * update version number * update tauri conf version * fix(wallet): explorer links * refactor(explorer): rename mixnodeidentitykey to mixid * fix(wallet): broken explorer links Co-authored-by: fmtabbara Co-authored-by: pierre --- explorer/src/context/mixnode.tsx | 24 ++++----- explorer/src/pages/MixnodeDetail/index.tsx | 2 +- nym-wallet/Cargo.lock | 2 +- nym-wallet/package.json | 2 +- nym-wallet/src-tauri/Cargo.toml | 2 +- nym-wallet/src-tauri/tauri.conf.json | 2 +- .../src/components/Bonding/BondedGateway.tsx | 2 +- .../src/components/Bonding/BondedMixnode.tsx | 3 +- .../components/Delegation/DelegationItem.tsx | 2 +- .../Delegation/PendingDelegationItem.tsx | 2 +- nym-wallet/src/context/bonding.tsx | 2 + nym-wallet/src/context/delegations.tsx | 25 ++++++---- nym-wallet/src/context/mocks/bonding.tsx | 1 + nym-wallet/src/context/mocks/delegations.tsx | 49 ++++++++++++------- nym-wallet/src/pages/delegation/index.tsx | 24 ++++++--- 15 files changed, 90 insertions(+), 54 deletions(-) diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx index a17366645c..a710095811 100644 --- a/explorer/src/context/mixnode.tsx +++ b/explorer/src/context/mixnode.tsx @@ -36,16 +36,16 @@ export const useMixnodeContext = (): React.ContextType => React.useContext(MixnodeContext); interface MixnodeContextProviderProps { - mixNodeIdentityKey: string; + mixId: string; } /** * Provides a state context for a mixnode by identity - * @param mixNodeIdentityKey The identity key of the mixnode + * @param mixId The mixID of the mixnode */ -export const MixnodeContextProvider: React.FC = ({ mixNodeIdentityKey, children }) => { +export const MixnodeContextProvider: React.FC = ({ mixId, children }) => { const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchMixnodeByID, 'Failed to fetch mixnode by id', ); @@ -53,44 +53,44 @@ export const MixnodeContextProvider: React.FC = ({ const [mixNodeRow, setMixnodeRow] = React.useState(); const [delegations, fetchDelegations, clearDelegations] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchDelegationsById, 'Failed to fetch delegations for mixnode', ); const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchUniqDelegationsById, 'Failed to fetch delegations for mixnode', ); const [status, fetchStatus, clearStatus] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchStatusById, 'Failed to fetch mixnode status', ); const [stats, fetchStats, clearStats] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchStatsById, 'Failed to fetch mixnode stats', ); const [description, fetchDescription, clearDescription] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchMixnodeDescriptionById, 'Failed to fetch mixnode description', ); const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchMixnodeEconomicDynamicsStatsById, 'Failed to fetch mixnode dynamics stats by id', ); const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchUptimeStoryById, 'Failed to fetch mixnode uptime history', ); @@ -123,7 +123,7 @@ export const MixnodeContextProvider: React.FC = ({ fetchUptimeHistory(), ]); }); - }, [mixNodeIdentityKey]); + }, [mixId]); const state = React.useMemo( () => ({ diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx index 266c13be3a..1c38f69a53 100644 --- a/explorer/src/pages/MixnodeDetail/index.tsx +++ b/explorer/src/pages/MixnodeDetail/index.tsx @@ -223,7 +223,7 @@ export const PageMixnodeDetail: React.FC = () => { } return ( - + ); diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index a0086ebb09..1bc9e48663 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2877,7 +2877,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.1.0" +version = "1.1.1" dependencies = [ "aes-gcm", "argon2 0.3.4", diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 973fc67ded..a9a55d2d0b 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.0.0", + "version": "1.1.1", "main": "index.js", "license": "MIT", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 21dfa19030..835383188e 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.1.0" +version = "1.1.1" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 11af64d0a0..b7b616b2e8 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.1.0" + "version": "1.1.1" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 8b2776f8a9..51523ef6ca 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -91,7 +91,7 @@ export const BondedGateway = ({ {network && ( Check more stats of your gateway on the{' '} - + explorer diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index 00b3ea7196..5d63555ddb 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -66,6 +66,7 @@ export const BondedMixnode = ({ const navigate = useNavigate(); const { name, + mixId, stake, bond, stakeSaturation, @@ -165,7 +166,7 @@ export const BondedMixnode = ({ {network && ( Check more stats of your node on the{' '} - + explorer diff --git a/nym-wallet/src/components/Delegation/DelegationItem.tsx b/nym-wallet/src/components/Delegation/DelegationItem.tsx index 2906ed2abd..d800f9f487 100644 --- a/nym-wallet/src/components/Delegation/DelegationItem.tsx +++ b/nym-wallet/src/components/Delegation/DelegationItem.tsx @@ -45,7 +45,7 @@ export const DelegationItem = ({ ) : ( Promise; - undelegate: ( - mix_id: number, - usesVestingContractTokens: boolean, - fee?: FeeDetails, - ) => Promise; + undelegate: (mix_id: number, fee?: Fee) => Promise; + undelegateVesting: (mix_id: number) => Promise; }; export type TDelegationTransaction = { @@ -51,7 +54,10 @@ export const DelegationContext = createContext({ addDelegation: async () => { throw new Error('Not implemented'); }, - undelegate: async () => { + undelegate: () => { + throw new Error('Not implemented'); + }, + undelegateVesting: () => { throw new Error('Not implemented'); }, }); @@ -135,7 +141,8 @@ export const DelegationContextProvider: FC<{ totalRewards, refresh, addDelegation, - undelegate: undelegateAllFromMixnode, + undelegate: undelegateFromMixnode, + undelegateVesting: vestingUndelegateFromMixnode, }), [isLoading, error, delegations, pendingDelegations, totalDelegations], ); diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 5fde300d75..8d97deb090 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -8,6 +8,7 @@ const SLEEP_MS = 1000; const bondedMixnodeMock: TBondedMixnode = { name: 'Monster node', + mixId: 1, identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F', stake: { denom: 'nym', amount: '1234' }, bond: { denom: 'nym', amount: '1234' }, diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx index 3674dc1494..bc216fd7bf 100644 --- a/nym-wallet/src/context/mocks/delegations.tsx +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -1,5 +1,12 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { DelegationWithEverything, DecCoin, TransactionExecuteResult, FeeDetails } from '@nymproject/types'; +import { + DelegationWithEverything, + DecCoin, + TransactionExecuteResult, + FeeDetails, + Fee, + CurrencyDenom, +} from '@nymproject/types'; import { DelegationContext, TDelegationTransaction } from '../delegations'; import { mockSleep } from './utils'; @@ -154,11 +161,7 @@ export const MockDelegationContextProvider: FC<{}> = ({ children }) => { }; }; - const undelegate = async ( - mix_id: number, - _usesVestingContractTokens: boolean, - _fee?: FeeDetails, - ): Promise => { + const undelegate = async (mix_id: number, _fee?: Fee): Promise => { await mockSleep(SLEEP_MS); mockDelegations = mockDelegations.map((d) => { if (d.mix_id === mix_id) { @@ -175,18 +178,29 @@ export const MockDelegationContextProvider: FC<{}> = ({ children }) => { triggerStateUpdate(); }, 3000); - return [ - { - logs_json: '', - data_json: '', - transaction_hash: '', - gas_info: { - gas_wanted: { gas_units: BigInt(1) }, - gas_used: { gas_units: BigInt(1) }, - }, - fee: { amount: '1', denom: 'nym' }, + return { + logs_json: '', + data_json: '', + transaction_hash: '', + gas_info: { + gas_wanted: { gas_units: BigInt(1) }, + gas_used: { gas_units: BigInt(1) }, }, - ]; + fee: { amount: '1', denom: 'nym' as CurrencyDenom }, + }; + }; + + const undelegateVesting = async (mix_id: number, _fee?: FeeDetails) => { + return { + logs_json: '', + data_json: '', + transaction_hash: '', + gas_info: { + gas_wanted: { gas_units: BigInt(1) }, + gas_used: { gas_units: BigInt(1) }, + }, + fee: { amount: '1', denom: 'nym' as CurrencyDenom }, + }; }; const resetState = () => { @@ -226,6 +240,7 @@ export const MockDelegationContextProvider: FC<{}> = ({ children }) => { addDelegation, updateDelegation, undelegate, + undelegateVesting, }), [isLoading, error, delegations, totalDelegations, trigger], ); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 62418f0463..a70300388f 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -52,6 +52,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { isLoading, addDelegation, undelegate, + undelegateVesting, refresh: refreshDelegations, } = useDelegationContext(); @@ -206,7 +207,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const handleUndelegate = async ( mixId: number, - identityKey: string, + // identityKey is no longer used + _: string, usesVestingContractTokens: boolean, fee?: FeeDetails, ) => { @@ -216,19 +218,27 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { }); setShowUndelegateModal(false); setCurrentDelegationListActionItem(undefined); - + let tx; try { - const txs = await undelegate(mixId, usesVestingContractTokens, fee); + if (usesVestingContractTokens) { + tx = await undelegateVesting(mixId); + } else { + tx = await undelegate(mixId, fee?.fee); + } + + // const txs = await undelegate(mixId, usesVestingContractTokens, fee); const balances = await getAllBalances(); setConfirmationModalProps({ status: 'success', action: 'undelegate', ...balances, - transactions: txs.map((tx) => ({ - url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, - hash: tx.transaction_hash, - })), + transactions: [ + { + url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, + hash: tx.transaction_hash, + }, + ], }); } catch (e) { Console.error('Failed to undelegate', e); From e3cda93919c916cabfec77b397d2714abb17954e Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Fri, 18 Nov 2022 13:06:03 +0000 Subject: [PATCH 16/75] WASM client: update crate version so that the client works with mainnet --- clients/webassembly/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index b296e29959..708c2aeb1c 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nym-client-wasm" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] -version = "1.0.1" +version = "1.1.0" edition = "2021" keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"] license = "Apache-2.0" From 3ae16fbf1dd4bff8b7833053383637132929d653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 18 Nov 2022 15:22:33 +0100 Subject: [PATCH 17/75] network-requester: make arguments to run into flags (#1776) --- service-providers/network-requester/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index 07b7eb0f1b..50acee207e 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -21,15 +21,19 @@ const ENABLE_STATISTICS: &str = "enable-statistics"; #[derive(Args)] struct Run { /// Specifies whether this network requester should run in 'open-proxy' mode + #[clap(long)] open_proxy: bool, /// Websocket port to bind to + #[clap(long)] websocket_port: Option, /// Enable service anonymized statistics that get sent to a statistics aggregator server + #[clap(long)] enable_statistics: bool, /// Mixnet client address where a statistics aggregator is running. The default value is a Nym aggregator client + #[clap(long)] statistics_recipient: Option, } From e30fd270a166df0ad07642a104cddd7fbba51ddb Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Fri, 18 Nov 2022 15:32:57 +0000 Subject: [PATCH 18/75] Publish @nymproject/sdk v1.1.4 --- sdk/typescript/packages/sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/typescript/packages/sdk/package.json b/sdk/typescript/packages/sdk/package.json index c31270a7cf..e2e4c5b008 100644 --- a/sdk/typescript/packages/sdk/package.json +++ b/sdk/typescript/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/sdk", - "version": "1.1.2", + "version": "1.1.4", "license": "Apache-2.0", "author": "Nym Technologies SA", "main": "dist/index.js", From 1859ca0a302bffc04637fcda0ecf4e6ea33d2e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 21 Nov 2022 11:21:28 +0100 Subject: [PATCH 19/75] client-core: expose lane queue length state to other components (#1777) * client-core: publish lane queue lengths * client-connections: rename to LaneQueueLenghts plural * Fix clippy * Fix wasm build * rustfmt * clippy --- Cargo.lock | 1 + .../src/client/real_messages_control/mod.rs | 17 +++--- .../real_traffic_stream.rs | 34 ++++++++---- .../transmission_buffer.rs | 10 ++-- clients/native/src/client/mod.rs | 11 +++- clients/socks5/src/client/mod.rs | 9 +++- clients/webassembly/src/client/mod.rs | 9 +++- common/client-connections/Cargo.toml | 1 + common/client-connections/src/lib.rs | 52 +++++++++++++++++++ 9 files changed, 121 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2da304990a..6c382cce87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ name = "client-connections" version = "0.1.0" dependencies = [ "futures", + "log", ] [[package]] diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index a7c03dc1c0..a1e9290c64 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -8,13 +8,15 @@ use self::{ acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, }; -use crate::client::real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors; -use crate::client::{ - inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, - topology_control::TopologyAccessor, +use crate::{ + client::{ + inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, + real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors, + topology_control::TopologyAccessor, + }, + spawn_future, }; -use crate::spawn_future; -use client_connections::ClosedConnectionReceiver; +use client_connections::{ClosedConnectionReceiver, LaneQueueLengths}; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; @@ -104,6 +106,7 @@ where // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` impl RealMessagesController { + #[allow(clippy::too_many_arguments)] pub fn new( config: Config, ack_receiver: AcknowledgementReceiver, @@ -111,6 +114,7 @@ impl RealMessagesController { mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, + lane_queue_lengths: LaneQueueLengths, closed_connection_rx: ClosedConnectionReceiver, ) -> Self { let rng = OsRng; @@ -161,6 +165,7 @@ impl RealMessagesController { rng, config.self_recipient, topology_access, + lane_queue_lengths, closed_connection_rx, ); diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index a42c24892e..43be7524b3 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -4,7 +4,9 @@ use crate::client::mix_traffic::BatchMixMessageSender; use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender; use crate::client::topology_control::TopologyAccessor; -use client_connections::{ClosedConnectionReceiver, ConnectionId, TransmissionLane}; +use client_connections::{ + ClosedConnectionReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, +}; use futures::channel::mpsc; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; @@ -134,6 +136,9 @@ where /// Incoming channel for being notified of closed connections, so that we can close lanes /// corresponding to connections. To avoid sending traffic unnecessary closed_connection_rx: ClosedConnectionReceiver, + + /// Report queue lengths so that upstream can backoff sending data, and keep connections open. + lane_queue_lengths: LaneQueueLengths, } pub(crate) struct RealMessage { @@ -177,6 +182,7 @@ where rng: R, our_full_destination: Recipient, topology_access: TopologyAccessor, + lane_queue_lengths: LaneQueueLengths, closed_connection_rx: ClosedConnectionReceiver, ) -> Self { OutQueueControl { @@ -192,6 +198,7 @@ where topology_access, transmission_buffer: Default::default(), closed_connection_rx, + lane_queue_lengths, } } @@ -309,6 +316,17 @@ where } } + fn pop_next_message(&mut self) -> Option { + // Pop the next message from the transmission buffer + let (lane, real_next) = self.transmission_buffer.pop_next_message_at_random()?; + + // Update the published queue length + let lane_length = self.transmission_buffer.lane_length(&lane); + self.lane_queue_lengths.set(&lane, lane_length); + + Some(real_next) + } + fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll> { // The average delay could change depending on if backpressure in the downstream channel // (mix_tx) was detected. @@ -359,16 +377,13 @@ where log::trace!("handling real_messages: size: {}", real_messages.len()); self.transmission_buffer.store(&conn_id, real_messages); - let real_next = self - .transmission_buffer - .pop_next_message_at_random() - .expect("we just added one"); + let real_next = self.pop_next_message().expect("Just stored one"); Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } Poll::Pending => { - if let Some(real_next) = self.transmission_buffer.pop_next_message_at_random() { + if let Some(real_next) = self.pop_next_message() { Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } else { // otherwise construct a dummy one @@ -411,16 +426,13 @@ where // First store what we got for the given connection id self.transmission_buffer.store(&conn_id, real_messages); - let real_next = self - .transmission_buffer - .pop_next_message_at_random() - .expect("we just added one"); + let real_next = self.pop_next_message().expect("we just added one"); Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } Poll::Pending => { - if let Some(real_next) = self.transmission_buffer.pop_next_message_at_random() { + if let Some(real_next) = self.pop_next_message() { Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } else { Poll::Pending diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs index 2510a65e05..b47aaf470e 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs @@ -41,6 +41,10 @@ impl TransmissionBuffer { self.buffer.keys().count() } + pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option { + self.buffer.get(lane).map(LaneBufferEntry::len) + } + #[allow(unused)] pub(crate) fn connections(&self) -> HashSet { self.buffer @@ -127,7 +131,7 @@ impl TransmissionBuffer { Some(real_next) } - pub(crate) fn pop_next_message_at_random(&mut self) -> Option { + pub(crate) fn pop_next_message_at_random(&mut self) -> Option<(TransmissionLane, RealMessage)> { if self.buffer.is_empty() { return None; } @@ -142,8 +146,9 @@ impl TransmissionBuffer { *self.pick_random_lane()? }; + let msg = self.pop_front_from_lane(&lane)?; log::trace!("picking to send from lane: {:?}", lane); - self.pop_front_from_lane(&lane) + Some((lane, msg)) } pub(crate) fn prune_stale_connections(&mut self) { @@ -196,7 +201,6 @@ impl LaneBufferEntry { > Duration::from_secs(MSG_CONSIDERED_STALE_AFTER_SECS) } - #[cfg(not(target_arch = "wasm32"))] fn len(&self) -> usize { self.real_messages.len() } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index f2aee9829f..91fb13bcfa 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, TransmissionLane}; +use client_connections::{ + ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths, TransmissionLane, +}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -119,6 +121,7 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, + lane_queue_lengths: LaneQueueLengths, closed_connection_rx: ClosedConnectionReceiver, shutdown: ShutdownListener, ) { @@ -149,6 +152,7 @@ impl NymClient { mix_sender, topology_accessor, reply_key_storage, + lane_queue_lengths, closed_connection_rx, ) .start_with_shutdown(shutdown); @@ -417,12 +421,17 @@ impl NymClient { // controller that connections are closed. let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) + let shared_lane_queue_length = LaneQueueLengths::new(); + self.start_real_traffic_controller( shared_topology_accessor.clone(), reply_key_storage, ack_receiver, input_receiver, sphinx_message_sender.clone(), + shared_lane_queue_length, closed_connection_rx, shutdown.subscribe(), ); diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 5b8530e6d0..b35d7fd9fe 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -9,7 +9,7 @@ use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender}; +use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -120,6 +120,7 @@ impl NymClient { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, closed_connection_rx: ClosedConnectionReceiver, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let mut controller_config = client_core::client::real_messages_control::Config::new( @@ -149,6 +150,7 @@ impl NymClient { mix_sender, topology_accessor, reply_key_storage, + lane_queue_lengths, closed_connection_rx, ) .start_with_shutdown(shutdown); @@ -409,6 +411,10 @@ impl NymClient { // This will be forwarded to `OutQueueControl` let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); + self.start_real_traffic_controller( shared_topology_accessor.clone(), reply_key_storage, @@ -416,6 +422,7 @@ impl NymClient { input_receiver, sphinx_message_sender.clone(), closed_connection_rx, + shared_lane_queue_lengths, shutdown.subscribe(), ); diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 7aed6c9a3b..e49d6199db 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; -use client_connections::{ClosedConnectionReceiver, TransmissionLane}; +use client_connections::{ClosedConnectionReceiver, LaneQueueLengths, TransmissionLane}; use client_core::client::{ cover_traffic_stream::LoopCoverTrafficStream, inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, @@ -129,6 +129,7 @@ impl NymClient { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, closed_connection_rx: ClosedConnectionReceiver, + lane_queue_lengths: LaneQueueLengths, ) { let mut controller_config = real_messages_control::Config::new( self.key_manager.ack_key(), @@ -153,6 +154,7 @@ impl NymClient { input_receiver, mix_sender, topology_accessor, + lane_queue_lengths, closed_connection_rx, ) .start(); @@ -353,12 +355,17 @@ impl NymClient { // The MixTrafficController then sends the actual traffic let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client); + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); + self.start_real_traffic_controller( shared_topology_accessor.clone(), ack_receiver, input_receiver, sphinx_message_sender.clone(), closed_connection_rx, + shared_lane_queue_lengths, ); if !self.config.debug.disable_loop_cover_traffic_stream { diff --git a/common/client-connections/Cargo.toml b/common/client-connections/Cargo.toml index 067621c28f..7438be79ba 100644 --- a/common/client-connections/Cargo.toml +++ b/common/client-connections/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] futures = "0.3" +log = "0.4.17" diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index 2aabce78b0..265e21f9fd 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; + use futures::channel::mpsc; pub type ConnectionId = u64; @@ -19,3 +21,53 @@ pub enum TransmissionLane { /// they can forward this to the `OutQueueControl` (via `ClientRequest` for the network-requester) pub type ClosedConnectionSender = mpsc::UnboundedSender; pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver; + +// The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down +// if needed. +#[derive(Clone)] +pub struct LaneQueueLengths(std::sync::Arc>); + +impl LaneQueueLengths { + pub fn new() -> Self { + LaneQueueLengths(std::sync::Arc::new(std::sync::Mutex::new( + LaneQueueLengthsInner { + map: HashMap::new(), + }, + ))) + } + + pub fn set(&mut self, lane: &TransmissionLane, lane_length: Option) { + match self.0.lock() { + Ok(mut inner) => { + if let Some(length) = lane_length { + inner + .map + .entry(*lane) + .and_modify(|e| *e = length) + .or_insert(length); + } else { + inner.map.remove(lane); + } + } + Err(err) => log::warn!("Failed to set lane queue length: {err}"), + } + } +} + +impl Default for LaneQueueLengths { + fn default() -> Self { + Self::new() + } +} + +impl std::ops::Deref for LaneQueueLengths { + type Target = std::sync::Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub struct LaneQueueLengthsInner { + map: HashMap, +} From 279ba7034c252cf9ab4d9ba66b94486ee813478e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 14:53:30 +0100 Subject: [PATCH 20/75] Typo --- .github/workflows/nightly_build_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 7723cb599a..b3ea3b7cc6 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest needs: matrix_prep outputs: - output1: ${{ steps.step2.outputs.lastest_release }} + output1: ${{ steps.step2.outputs.latest_release }} steps: - name: Check out repository code uses: actions/checkout@v3 From 9337821712301d76f571cc2a1bc5af07d8d1119f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 15:07:50 +0100 Subject: [PATCH 21/75] Removed extra spaces in sed expression --- .github/workflows/nightly_build_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index b3ea3b7cc6..678eefc22f 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -25,7 +25,7 @@ jobs: - name: Set output variable to latest release branch id: step2 - run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/origin\///')" >> $GITHUB_OUTPUT + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT build: needs: [get_release,matrix_prep] strategy: From c4ee964557128ced0fe2c572e51d2e8d127359bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 21 Nov 2022 16:34:50 +0200 Subject: [PATCH 22/75] Setup with 1 epoch and full test that skips key update (#1647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Setup with 1 epoch and full test that skips key update * Remove a bunch of epoch code * Remove unnecessary map from one element vector * Remove tau, epoch and lambda_t * Removed lambda_t completely Co-authored-by: Jędrzej Stuczyński --- common/crypto/dkg/benches/benchmarks.rs | 67 +- common/crypto/dkg/src/bte/encryption.rs | 205 +------ common/crypto/dkg/src/bte/keys.rs | 780 +++--------------------- common/crypto/dkg/src/bte/mod.rs | 400 +----------- common/crypto/dkg/src/dealing.rs | 45 +- common/crypto/dkg/tests/integration.rs | 61 +- 6 files changed, 132 insertions(+), 1426 deletions(-) diff --git a/common/crypto/dkg/benches/benchmarks.rs b/common/crypto/dkg/benches/benchmarks.rs index 4f226144ab..7b85d70356 100644 --- a/common/crypto/dkg/benches/benchmarks.rs +++ b/common/crypto/dkg/benches/benchmarks.rs @@ -9,7 +9,7 @@ use dkg::bte::proof_discrete_log::ProofOfDiscreteLog; use dkg::bte::proof_sharing::ProofOfSecretSharing; use dkg::bte::{ decrypt_share, encrypt_shares, keygen, proof_chunking, proof_sharing, setup, DecryptionKey, - Epoch, PublicKey, + PublicKey, }; use dkg::interpolation::polynomial::Polynomial; use dkg::{Dealing, NodeIndex, Share}; @@ -54,7 +54,6 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 2; - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 3); @@ -66,7 +65,6 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) { ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ) @@ -80,7 +78,6 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 2; - let epoch = Epoch::new(2); let (receivers, mut dks) = prepare_keys(&mut rng, 3); let (dealing, _) = Dealing::create( @@ -88,22 +85,18 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ); let first_key = dks.get_mut(0).unwrap(); - first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); c.bench_function( "verifying single dealing made for 3 parties (threshold 2) and recovering share", |b| { b.iter(|| { - assert!(dealing - .verify(¶ms, epoch, threshold, &receivers, None) - .is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); }) }, ); @@ -114,7 +107,6 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 14; - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 20); @@ -128,7 +120,6 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) { ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ) @@ -143,7 +134,6 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 14; - let epoch = Epoch::new(2); let (receivers, mut dks) = prepare_keys(&mut rng, 20); let (dealing, _) = Dealing::create( @@ -151,22 +141,18 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ); let first_key = dks.get_mut(0).unwrap(); - first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); c.bench_function( "verifying single dealing made for 20 parties (threshold 14) and recovering share", |b| { b.iter(|| { - assert!(dealing - .verify(¶ms, epoch, threshold, &receivers, None) - .is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); }) }, ); @@ -177,7 +163,6 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 67; - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -191,7 +176,6 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) { ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ) @@ -206,7 +190,6 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 67; - let epoch = Epoch::new(2); let (receivers, mut dks) = prepare_keys(&mut rng, 100); let (dealing, _) = Dealing::create( @@ -214,22 +197,18 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ); let first_key = dks.get_mut(0).unwrap(); - first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); c.bench_function( "verifying single dealing made for 100 parties (threshold 67) and recovering share", |b| { b.iter(|| { - assert!(dealing - .verify(¶ms, epoch, threshold, &receivers, None) - .is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); }) }, ); @@ -266,7 +245,6 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -283,7 +261,7 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) { .collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); c.bench_function("creating proof of chunking for 100 parties", |b| { b.iter(|| { @@ -301,7 +279,6 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -318,7 +295,7 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) { .collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); let proof_of_chunking = @@ -338,7 +315,6 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -354,7 +330,7 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { .map(|(share, key)| (share, key)) .collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); let combined_ciphertexts = ciphertexts.combine_ciphertexts(); let combined_r = hazmat.combine_rs(); @@ -381,7 +357,6 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -397,7 +372,7 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { .map(|(share, key)| (share, key)) .collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); let combined_ciphertexts = ciphertexts.combine_ciphertexts(); let combined_r = hazmat.combine_rs(); @@ -430,7 +405,6 @@ pub fn single_share_encryption(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (_, pk) = keygen(¶ms, &mut rng); let polynomial = Polynomial::new_random(&mut rng, 3); @@ -440,7 +414,6 @@ pub fn single_share_encryption(c: &mut Criterion) { b.iter(|| { black_box(encrypt_shares( &[(&share, pk.public_key())], - epoch, ¶ms, &mut rng, )) @@ -452,7 +425,6 @@ pub fn share_encryption_100(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); let polynomial = Polynomial::new_random(&mut rng, 3); @@ -468,14 +440,7 @@ pub fn share_encryption_100(c: &mut Criterion) { .collect::>(); c.bench_function("100 shares encryption", |b| { - b.iter(|| { - black_box(encrypt_shares( - &remote_share_key_pairs, - epoch, - ¶ms, - &mut rng, - )) - }) + b.iter(|| black_box(encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng))) }); } @@ -483,16 +448,14 @@ pub fn share_decryption(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); - let (mut dk, pk) = keygen(¶ms, &mut rng); + let (dk, pk) = keygen(¶ms, &mut rng); let polynomial = Polynomial::new_random(&mut rng, 3); let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into(); - let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], epoch, ¶ms, &mut rng); - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng); c.bench_function("single share decryption", |b| { - b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, epoch, None))) + b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None))) }); } diff --git a/common/crypto/dkg/src/bte/encryption.rs b/common/crypto/dkg/src/bte/encryption.rs index 7d7f4d4819..22fd509677 100644 --- a/common/crypto/dkg/src/bte/encryption.rs +++ b/common/crypto/dkg/src/bte/encryption.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bte::keys::{DecryptionKey, PublicKey}; -use crate::bte::{Epoch, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE}; +use crate::bte::{evaluate_f, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE}; use crate::error::DkgError; use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2}; use crate::{Chunk, ChunkedShare, Share}; @@ -24,7 +24,7 @@ pub struct Ciphertexts { } impl Ciphertexts { - pub fn verify_integrity(&self, params: &Params, epoch: Epoch) -> bool { + pub fn verify_integrity(&self, params: &Params) -> bool { // if this checks fails it means the ciphertext is undefined as values // in `r`, `s` and `z` are meaningless since technically this ciphertext // has been created for 0 parties @@ -33,9 +33,7 @@ impl Ciphertexts { } let g1_neg = G1Affine::generator().neg(); - let f = epoch - .as_extended_tau(&self.rr, &self.ss, &self.ciphertext_chunks) - .evaluate_f(params); + let f = evaluate_f(params); // we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid), // so perform some precomputation on it @@ -192,7 +190,6 @@ impl HazmatRandomness { pub fn encrypt_shares( shares: &[(&Share, &PublicKey)], - epoch: Epoch, params: &Params, mut rng: impl RngCore, ) -> (Ciphertexts, HazmatRandomness) { @@ -242,7 +239,7 @@ pub fn encrypt_shares( let rr = rr.try_into().unwrap(); let ss = ss.try_into().unwrap(); - let f = epoch.as_extended_tau(&rr, &ss, &cc).evaluate_f(params); + let f = evaluate_f(params); let mut zz = Vec::with_capacity(NUM_CHUNKS); for i in 0..NUM_CHUNKS { @@ -269,35 +266,22 @@ pub fn decrypt_share( // in the case of multiple receivers, specifies which index of ciphertext chunks should be used i: usize, ciphertext: &Ciphertexts, - epoch: Epoch, lookup_table: Option<&BabyStepGiantStepLookup>, ) -> Result { let mut plaintext = ChunkedShare::default(); - let decryption_node = dk.try_get_compatible_node(epoch)?; - let extended_tau = epoch.as_extended_tau( - &ciphertext.rr, - &ciphertext.ss, - &ciphertext.ciphertext_chunks, - ); - if i >= ciphertext.ciphertext_chunks.len() { return Err(DkgError::UnavailableCiphertext(i)); } - let height = decryption_node.tau.height(); - let b_neg = decryption_node - .ds + let b_neg = dk + .dh .iter() - .chain(decryption_node.dh.iter()) - .zip(extended_tau.0.iter().by_vals().skip(height)) - .filter(|(_, i)| *i) - .map(|(d_i, _)| d_i) - .fold(decryption_node.b, |acc, d_i| acc + d_i) + .fold(dk.b, |acc, d_i| acc + d_i) .neg() .to_affine(); - let e_neg = decryption_node.e.neg().to_affine(); + let e_neg = dk.e.neg().to_affine(); for j in 0..NUM_CHUNKS { let rr_j = &ciphertext.rr[j]; @@ -308,7 +292,7 @@ pub fn decrypt_share( let miller = bls12_381::multi_miller_loop(&[ (&cc_ij.to_affine(), &G2_GENERATOR_PREPARED), (&rr_j.to_affine(), &G2Prepared::from(b_neg)), - (&decryption_node.a.to_affine(), &G2Prepared::from(zz_j)), + (&dk.a.to_affine(), &G2Prepared::from(zz_j)), (&ss_j.to_affine(), &G2Prepared::from(e_neg)), ]); let m = miller.final_exponentiation(); @@ -466,7 +450,6 @@ mod tests { let (decryption_key1, public_key1) = keygen(¶ms, &mut rng); let (decryption_key2, public_key2) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(0); let lookup_table = &DEFAULT_BSGS_TABLE; @@ -475,13 +458,13 @@ mod tests { let m2 = Share::random(&mut rng); let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)]; - let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng); + let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -494,15 +477,8 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let (mut decryption_key1, public_key1) = keygen(¶ms, &mut rng); - let (mut decryption_key2, public_key2) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(12345); - decryption_key1 - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); - decryption_key2 - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); + let (decryption_key1, public_key1) = keygen(¶ms, &mut rng); + let (decryption_key2, public_key2) = keygen(¶ms, &mut rng); let lookup_table = &DEFAULT_BSGS_TABLE; @@ -511,121 +487,18 @@ mod tests { let m2 = Share::random(&mut rng); let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)]; - let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng); + let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } } - #[test] - fn decryption_with_root_key() { - let dummy_seed = [42u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = setup(); - - let (root_key, public_key) = keygen(¶ms, &mut rng); - - let share = Share::random(&mut rng); - - let epoch0 = Epoch::new(0); - let epoch42 = Epoch::new(42); - let epoch_big = Epoch::new(3292547435); - - let (ciphertext1, hazmat1) = - encrypt_shares(&[(&share, &public_key.key)], epoch0, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext1, &hazmat1); - - let (ciphertext2, hazmat2) = - encrypt_shares(&[(&share, &public_key.key)], epoch42, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext2, &hazmat2); - - let (ciphertext3, hazmat3) = - encrypt_shares(&[(&share, &public_key.key)], epoch_big, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext3, &hazmat3); - - let recovered1 = decrypt_share(&root_key, 0, &ciphertext1, epoch0, None).unwrap(); - let recovered2 = decrypt_share(&root_key, 0, &ciphertext2, epoch42, None).unwrap(); - let recovered3 = decrypt_share(&root_key, 0, &ciphertext3, epoch_big, None).unwrap(); - - assert_eq!(share, recovered1); - assert_eq!(share, recovered2); - assert_eq!(share, recovered3); - } - - #[test] - #[ignore] // expensive test - fn update_and_decrypt_10() { - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = setup(); - - let (mut decryption_key, public_key) = keygen(¶ms, &mut rng); - - for epoch_value in 0..10 { - let epoch = Epoch::new(epoch_value); - let share = Share::random(&mut rng); - decryption_key - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); - - let (ciphertext, hazmat) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext, &hazmat); - - let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap(); - assert_eq!(share, recovered); - } - } - - #[test] - #[ignore] // expensive test - fn reblinding_node_doesnt_affect_decryption() { - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = setup(); - - let (mut decryption_key, public_key) = keygen(¶ms, &mut rng); - - let epoch = Epoch::new(12345); - decryption_key - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); - for node in decryption_key.nodes.iter_mut() { - node.reblind(¶ms, &mut rng); - } - let share = Share::random(&mut rng); - - let (ciphertext, hazmat) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext, &hazmat); - - let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap(); - assert_eq!(share, recovered); - - // attempt to update the key again so we have to derive fresh nodes using previous reblinded results - let epoch2 = Epoch::new(67890); - decryption_key - .try_update_to(epoch2, ¶ms, &mut rng) - .unwrap(); - for node in decryption_key.nodes.iter_mut() { - node.reblind(¶ms, &mut rng); - } - let share2 = Share::random(&mut rng); - - let (ciphertext, hazmat) = - encrypt_shares(&[(&share2, &public_key.key)], epoch2, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext, &hazmat); - - let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch2, None).unwrap(); - assert_eq!(share2, recovered); - } - #[test] #[ignore] // expensive test fn ciphertext_integrity_check_passes_for_valid_data() { @@ -634,14 +507,11 @@ mod tests { let dummy_seed = [1u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let (mut dk, public_key) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(1); + let (_, public_key) = keygen(¶ms, &mut rng); - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); let share = Share::random(&mut rng); - let (ciphertext, _) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - assert!(ciphertext.verify_integrity(¶ms, epoch)) + let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng); + assert!(ciphertext.verify_integrity(¶ms)) } #[test] @@ -652,45 +522,22 @@ mod tests { let dummy_seed = [1u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let (mut dk, public_key) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(1); + let (_, public_key) = keygen(¶ms, &mut rng); - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); let share = Share::random(&mut rng); - let (ciphertext, _) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng); let mut bad_cipher1 = ciphertext.clone(); bad_cipher1.rr[4] = G1Projective::generator(); - assert!(!bad_cipher1.verify_integrity(¶ms, epoch)); + assert!(!bad_cipher1.verify_integrity(¶ms)); let mut bad_cipher2 = ciphertext.clone(); bad_cipher2.ss[4] = G1Projective::generator(); - assert!(!bad_cipher2.verify_integrity(¶ms, epoch)); + assert!(!bad_cipher2.verify_integrity(¶ms)); let mut bad_cipher3 = ciphertext; bad_cipher3.zz[4] = G2Projective::generator(); - assert!(!bad_cipher3.verify_integrity(¶ms, epoch)); - } - - #[test] - #[ignore] // expensive test - fn ciphertext_integrity_check_passes_fails_for_wrong_epoch() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, public_key) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(1); - - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let share = Share::random(&mut rng); - let (ciphertext, _) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - - let another_epoch = Epoch::new(2); - assert!(!ciphertext.verify_integrity(¶ms, another_epoch)) + assert!(!bad_cipher3.verify_integrity(¶ms)); } #[test] @@ -711,7 +558,7 @@ mod tests { } let refs = shares.iter().zip(public_keys.iter()).collect::>(); - let (ciphertext, hazmat) = encrypt_shares(&refs, Epoch::new(42), ¶ms, &mut rng); + let (ciphertext, hazmat) = encrypt_shares(&refs, ¶ms, &mut rng); let combined_r = combine_scalar_chunks(hazmat.r()); let combined_rr = ciphertext.combine_rs(); diff --git a/common/crypto/dkg/src/bte/keys.rs b/common/crypto/dkg/src/bte/keys.rs index 857b01c5b0..c2c0e91a33 100644 --- a/common/crypto/dkg/src/bte/keys.rs +++ b/common/crypto/dkg/src/bte/keys.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bte::proof_discrete_log::ProofOfDiscreteLog; -use crate::bte::{Epoch, Params, Tau}; +use crate::bte::Params; use crate::error::DkgError; use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar}; use bls12_381::{G1Projective, G2Projective, Scalar}; @@ -11,297 +11,6 @@ use group::GroupEncoding; use rand_core::RngCore; use zeroize::Zeroize; -#[derive(Debug, Zeroize)] -#[zeroize(drop)] -#[cfg_attr(test, derive(Clone, PartialEq))] -pub(crate) struct Node { - pub(crate) tau: Tau, - - // g1^rho - pub(crate) a: G1Projective, - - // g2^x - pub(crate) b: G2Projective, - - // f_i^rho, up to lambda_t elements - pub(crate) ds: Vec, - - // fh_i^rho, always lambda_h elements - pub(crate) dh: Vec, - - // h^rho - pub(crate) e: G2Projective, -} - -impl Node { - fn new_root( - a: G1Projective, - b: G2Projective, - ds: Vec, - dh: Vec, - e: G2Projective, - ) -> Self { - Node { - tau: Tau::new_root(), - a, - b, - ds, - dh, - e, - } - } - - fn is_root(&self) -> bool { - self.tau.0.is_empty() - } - - pub(crate) fn reblind(&mut self, params: &Params, mut rng: impl RngCore) { - let delta = Scalar::random(&mut rng); - self.a += G1Projective::generator() * delta; - - // TODO: or do we have to do full tau evaluation here? - self.b += self.tau.evaluate_partial_f(params) * delta; - self.ds - .iter_mut() - .zip(params.fs.iter().skip(self.tau.height())) - .for_each(|(d_i, f_i)| *d_i += f_i * delta); - self.dh - .iter_mut() - .zip(params.fh.iter()) - .for_each(|(d_i, f_i)| *d_i += f_i * delta); - - self.e += params.h * delta; - } - - // note: it's unsafe to use this method outside `try_update_to` as - // we have guaranteed there that `self` is parent of the target - // and that `self.tau != target_tau` - /// Given `self` with `Tau1` and `target_tau` with `Tau2`, such that `Tau1` prefixes `Tau2`, - /// i.e. `Tau2 == Tau1 || SUFFIX`, and `Tau2` is a leaf node, derive all required crypto material - /// for its construction. - fn derive_target_child_with_partials( - &self, - params: &Params, - target_tau: Tau, - partial_b: &G2Projective, - partial_f: &G2Projective, - mut rng: impl RngCore, - ) -> Self { - debug_assert!(self.tau.is_parent_of(&target_tau)); - debug_assert_ne!(self.tau, target_tau); - - let delta = Scalar::random(&mut rng); - let a = self.a + G1Projective::generator() * delta; - let b = partial_b + partial_f * delta; - let ds = self - .ds - .iter() - .zip(params.fs.iter()) - .skip(target_tau.height()) - .map(|(d_i, f_i)| d_i + f_i * delta) - .collect(); - let dh = self - .dh - .iter() - .zip(params.fh.iter()) - .map(|(dh_i, fh_i)| dh_i + fh_i * delta) - .collect(); - let e = self.e + params.h * delta; - - Node { - tau: target_tau, - a, - b, - ds, - dh, - e, - } - } - - // note: it's unsafe to use this method outside `try_update_to` as - // we have guaranteed there that `self` is parent of the target - // and that `self.tau != target_tau` - /// Given `self` with `Tau1` and `most_direct_parent` with `Tau2`, such that `Tau1` prefixes `Tau2`, - /// i.e. `Tau2 == Tau1 || SUFFIX`, derive node with `Tau3 = Tau2 || 1` - fn derive_right_nonfinal_child_of_with_partials( - &self, - params: &Params, - most_direct_parent: Tau, - partial_b: &G2Projective, - partial_f: &G2Projective, - mut rng: impl RngCore, - ) -> Self { - let right_branch = most_direct_parent.right_child(); - - debug_assert!(self.tau.is_parent_of(&most_direct_parent)); - debug_assert!(self.tau.is_parent_of(&right_branch)); - debug_assert_ne!(self.tau, right_branch); - - // n is height difference between self and the child - let n = right_branch.height() - self.tau.height(); - - // i is the index of the last bit we just added - let i = right_branch.height() - 1; - - let delta = Scalar::random(&mut rng); - let a = self.a + G1Projective::generator() * delta; - let d0 = self.ds[n - 1]; - let b = partial_b + d0 + (partial_f + params.fs[i]) * delta; - let ds = self - .ds - .iter() - .skip(n) - .zip(params.fs.iter().skip(right_branch.height())) - .map(|(d_i, f_i)| d_i + f_i * delta) - .collect(); - let dh = self - .dh - .iter() - .zip(params.fh.iter()) - .map(|(dh_i, fh_i)| dh_i + fh_i * delta) - .collect(); - - let e = self.e + params.h * delta; - - Node { - tau: right_branch, - a, - b, - ds, - dh, - e, - } - } - - // tau_bytes_len || tau || a || b || len_ds || ds || len_dh || dh || e - pub(crate) fn to_bytes(&self) -> Vec { - let g1_elements = 1; - let g2_elements = self.ds.len() + self.dh.len() + 2; - - let tau_bytes = self.tau.to_bytes(); - - // the extra 12 comes from the triple u32 we use for encoding lengths of tau, ds and dh - let mut bytes = - Vec::with_capacity(tau_bytes.len() + g1_elements * 48 + g2_elements * 96 + 12); - - bytes.extend_from_slice(&((tau_bytes.len() as u32).to_be_bytes())); - bytes.extend_from_slice(&tau_bytes); - bytes.extend_from_slice(self.a.to_bytes().as_ref()); - bytes.extend_from_slice(self.b.to_bytes().as_ref()); - bytes.extend_from_slice(&((self.ds.len() as u32).to_be_bytes())); - for d_i in &self.ds { - bytes.extend_from_slice(d_i.to_bytes().as_ref()); - } - bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes())); - for dh_i in &self.dh { - bytes.extend_from_slice(dh_i.to_bytes().as_ref()); - } - bytes.extend_from_slice(self.e.to_bytes().as_ref()); - - bytes - } - - pub fn try_from_bytes(bytes: &[u8]) -> Result { - // at the very least we require bytes for: - // - tau_len ( 4 ) - // - tau ( could be 0 for root node ) - // - a ( 48 ) - // - b ( 96 ) - // - length indication of ds ( 4 ) - // - length indication of dh ( 4 ) - // - e ( 96 ) - if bytes.len() < 4 + 48 + 96 + 4 + 4 + 96 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided", - )); - } - - let tau_len = u32::from_be_bytes((&bytes[..4]).try_into().unwrap()) as usize; - let mut i = 4; - - let tau = Tau::try_from_bytes(&bytes[i..i + tau_len])?; - i += tau_len; - - // perform another length check to account for bytes consumed by tau - if bytes[i..].len() < 48 + 96 + 4 + 4 + 96 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided", - )); - } - - let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { - DkgError::new_deserialization_failure("Node.a", "invalid curve point") - })?; - i += 48; - - let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { - DkgError::new_deserialization_failure("Node.b", "invalid curve point") - })?; - i += 96; - - let ds_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; - i += 4; - - if bytes[i..].len() < ds_len * 96 + 4 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided (ds)", - )); - } - - let mut ds = Vec::with_capacity(ds_len); - for j in 0..ds_len { - let d_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { - DkgError::new_deserialization_failure( - format!("Node.ds_{}", j), - "invalid curve point", - ) - })?; - - ds.push(d_i); - i += 96; - } - - let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; - i += 4; - - if bytes[i..].len() != (dh_len + 1) * 96 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided (dh)", - )); - } - - let mut dh = Vec::with_capacity(dh_len); - for j in 0..dh_len { - let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { - DkgError::new_deserialization_failure( - format!("Node.dh_{}", j), - "invalid curve point", - ) - })?; - - dh.push(dh_i); - i += 96; - } - - let e = deserialize_g2(&bytes[i..]).ok_or_else(|| { - DkgError::new_deserialization_failure("Node.h", "invalid curve point") - })?; - - Ok(Node { - tau, - a, - b, - ds, - dh, - e, - }) - } -} - // produces public key and a decryption key for the root of the tree pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) { let g1 = G1Projective::generator(); @@ -317,11 +26,10 @@ pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicK let a = g1 * rho; let b = g2 * x + params.f0 * rho; - let ds = params.fs.iter().map(|f_i| f_i * rho).collect(); let dh = params.fh.iter().map(|fh_i| fh_i * rho).collect(); let e = params.h * rho; - let dk = DecryptionKey::new_root(Node::new_root(a, b, ds, dh, e)); + let dk = DecryptionKey::new_root(a, b, dh, e); let public_key = PublicKey(y); let key_with_proof = PublicKeyWithProof { @@ -414,231 +122,94 @@ impl PublicKeyWithProof { #[zeroize(drop)] #[cfg_attr(test, derive(PartialEq))] pub struct DecryptionKey { - // note that the nodes are ordered from "right" to "left" - pub(crate) nodes: Vec, + // g1^rho + pub(crate) a: G1Projective, + + // g2^x * f0^rho + pub(crate) b: G2Projective, + + // fh_i^rho, always lambda_h elements + pub(crate) dh: Vec, + + // h^rho + pub(crate) e: G2Projective, } impl DecryptionKey { - fn new_root(root_node: Node) -> Self { - DecryptionKey { - nodes: vec![root_node], - } - } - - fn current(&self) -> Result<&Node, DkgError> { - // we must have at least a single node, otherwise we have a malformed key - self.nodes.last().ok_or(DkgError::MalformedDecryptionKey) - } - - pub fn current_epoch(&self, params: &Params) -> Result, DkgError> { - let current_node = self.current()?; - if current_node.is_root() { - Ok(None) - } else { - Epoch::try_from_tau(¤t_node.tau, params).map(Option::Some) - } - } - - pub(crate) fn try_get_compatible_node(&self, epoch: Epoch) -> Result<&Node, DkgError> { - let tau = epoch.as_tau(); - self.nodes - .iter() - .rev() - .find(|node| node.tau.is_parent_of(&tau)) - .ok_or(DkgError::ExpiredKey) - } - - pub fn try_update_to_next_epoch( - &mut self, - params: &Params, - mut rng: impl RngCore, - ) -> Result<(), DkgError> { - if self.nodes.is_empty() { - return Err(DkgError::MalformedDecryptionKey); - } - - let mut target_epoch = Epoch::new(0); - if self.nodes.len() == 1 && self.nodes[0].is_root() { - return self.try_update_to(target_epoch, params, &mut rng); - } - - // unwrap is fine as we have asserted self.nodes is not empty - self.nodes.pop().unwrap(); - - if let Some(tail) = self.nodes.last() { - target_epoch = tail.tau.lowest_valid_epoch_child(params)?; - } else { - // essentially our key consisted of only a single node and it wasn't a root, - // so either it was malformed or we somehow reached the final epoch and wanted to update - // beyond that. Either way, update to l + 1 is impossible - return Err(DkgError::MalformedDecryptionKey); - } - - self.try_update_to(target_epoch, params, &mut rng) - } - - /// Attempts to update `self` to the provided `epoch`. If the update is not possible, - /// because the target was in the past or the key is malformed, an error is returned. - /// - /// Note that this method mutates the key in place and if the original key was malformed, - /// there are no guarantees about its internal state post-call. - pub fn try_update_to( - &mut self, - target_epoch: Epoch, - params: &Params, - mut rng: impl RngCore, - ) -> Result<(), DkgError> { - if self.nodes.is_empty() { - // somehow we have an empty decryption key - return Err(DkgError::MalformedDecryptionKey); - } - - // makes it easier to work with since we will be generating non-leaf nodes - let target_tau = target_epoch.as_tau(); - let current_tau = &self.current()?.tau; - - if current_tau == &target_tau { - // our key is already updated to the target - return Ok(()); - } - - if current_tau > &target_tau { - // we cannot derive keys for past epochs - return Err(DkgError::TargetEpochUpdateInThePast); - } - - // drop the nodes that are no longer required and get the most direct parent for the target epoch available - let mut parent = loop { - // if pop() fails the key is malformed since we checked that the target_epoch > current_epoch, - // hence the update should have been possible - let tail = self.nodes.pop().ok_or(DkgError::MalformedDecryptionKey)?; - if tail.tau.is_parent_of(&target_tau) { - break tail; - } - }; - - // essentially the case of updating epoch n to n + 1, where n is even; - // in that case the last two nodes are [..., epoch_{n+1}, epoch_n] - // so we just have to reblind the n+1 node and we're done - if parent.tau == target_tau { - parent.reblind(params, &mut rng); - self.nodes.push(parent); - return Ok(()); - } - - // accumulators, note that the previous elements have already been included by the parent, - // i.e. for example for parent at height l <= n, b = g2^x * f0^rho * d1^{tau_1} * ... * dl^{tau_l} - // new_b_accumulator = b * d1^{tau_1} * d2^{tau_2} * ... * dn^{tau_n} - // new_f_accumulator = f0 * f1^{tau_1} * f2^{tau_2} * ... * fn^{tau_n} (up to lambda_t) - let mut new_b_accumulator = parent.b; - let mut new_f_accumulator = parent.tau.evaluate_partial_f(params); - - let parent_height = parent.tau.height(); - - // path from the parent to the child - for (n, bit) in target_tau - .0 - .iter() - .by_vals() - .skip(parent.tau.height()) - .enumerate() - { - // ith bit of the [child] epoch - // note that n represents height difference between parent and the current bit - let i = n + parent_height; - - // if the bit is NOT set, push the right '1' subtree (for future keys) - // so for example if given parent with some `PREFIX` tau and target_epoch being `PREFIX || 010`, - // in the first loop iteration we're going to look at bit `0` and - // derive child node `PREFIX || 1` so that in the future we could derive keys for all other epochs starting with `PREFIX || 1` - // in the next loop iteration we're going to look at bit `1` and simply update the accumulators, - // as we don't need to generate any "left" nodes as all of them would have constructed epochs that are already in the past - // finally, in the last iteration, we look at the bit `0` and derive node `PREFIX || 011`, - // i.e. the one that FOLLOWS the target node. - if !bit { - let direct_parent = target_tau.try_get_parent_at_height(i)?; - - self.nodes - .push(parent.derive_right_nonfinal_child_of_with_partials( - params, - direct_parent, - &new_b_accumulator, - &new_f_accumulator, - &mut rng, - )); - } else { - // only update the accumulators when the bit is set, as d^0 == identity, so there's - // no point in doing anything else; - // note that we don't have to generate any new nodes when going into the right branch - // of the tree as everything on the left would have been in the past, so we don't care about them - new_b_accumulator += parent.ds[n]; // add d0 - new_f_accumulator += params.fs[i]; // f_i - } - } - - self.nodes.push(parent.derive_target_child_with_partials( - params, - target_epoch.as_tau(), - &new_b_accumulator, - &new_f_accumulator, - &mut rng, - )); - - Ok(()) + fn new_root(a: G1Projective, b: G2Projective, dh: Vec, e: G2Projective) -> Self { + DecryptionKey { a, b, dh, e } } pub fn to_bytes(&self) -> Vec { - let num_nodes = self.nodes.len() as u32; + let g1_elements = 1; + let g2_elements = self.dh.len() + 2; - // unfortunately we're not going to know the expected capacity - let mut bytes = Vec::new(); - bytes.extend_from_slice(&num_nodes.to_be_bytes()); + // the extra 8 comes from the triple u32 we use for encoding lengths of ds and dh + let mut bytes = Vec::with_capacity(g1_elements * 48 + g2_elements * 96 + 8); - for node in &self.nodes { - let mut node_bytes = node.to_bytes(); - bytes.extend_from_slice(&((node_bytes.len() as u32).to_be_bytes())); - bytes.append(&mut node_bytes) + bytes.extend_from_slice(self.a.to_bytes().as_ref()); + bytes.extend_from_slice(self.b.to_bytes().as_ref()); + bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes())); + for dh_i in &self.dh { + bytes.extend_from_slice(dh_i.to_bytes().as_ref()); } + bytes.extend_from_slice(self.e.to_bytes().as_ref()); bytes } - pub fn try_from_bytes(b: &[u8]) -> Result { - // we have to be able to read the length of nodes - if b.len() < 4 { + pub fn try_from_bytes(bytes: &[u8]) -> Result { + // at the very least we require bytes for: + // - a ( 48 ) + // - b ( 96 ) + // - length indication of dh ( 4 ) + // - e ( 96 ) + if bytes.len() < 48 + 96 + 4 + 96 { return Err(DkgError::new_deserialization_failure( - "DecryptionKey", + "Node", "insufficient number of bytes provided", )); } - let nodes_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; - let mut nodes = Vec::with_capacity(nodes_len); - let mut i = 4; - for _ in 0..nodes_len { - // check if we can actually read the length... - if b[i..].len() < 4 { - return Err(DkgError::new_deserialization_failure( - "DecryptionKey.Node", - "insufficient number of bytes provided for BTE Node recovery", - )); - } + let mut i = 0; + let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.a", "invalid curve point") + })?; + i += 48; - let node_bytes = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize; - if b[i + 4..].len() < node_bytes { - return Err(DkgError::new_deserialization_failure( - "DecryptionKey.Node", - "insufficient number of bytes provided for BTE Node recovery", - )); - } - i += 4; + let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.b", "invalid curve point") + })?; + i += 96; - let node = Node::try_from_bytes(&b[i..i + node_bytes])?; - nodes.push(node); - i += node_bytes; + let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + if bytes[i..].len() != (dh_len + 1) * 96 { + return Err(DkgError::new_deserialization_failure( + "Node", + "insufficient number of bytes provided (dh)", + )); } - Ok(DecryptionKey { nodes }) + let mut dh = Vec::with_capacity(dh_len); + for j in 0..dh_len { + let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + format!("Node.dh_{}", j), + "invalid curve point", + ) + })?; + + dh.push(dh_i); + i += 96; + } + + let e = deserialize_g2(&bytes[i..]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.h", "invalid curve point") + })?; + + Ok(Self { a, b, dh, e }) } } @@ -646,163 +217,8 @@ impl DecryptionKey { mod tests { use super::*; use crate::bte::setup; - use bitvec::bitvec; - use bitvec::order::Msb0; use rand_core::SeedableRng; - #[test] - #[ignore] // expensive test - fn basic_coverage_nodes() { - // it's some basic test I've been performing when writing the update function, but figured - // might as well put it into a unit test. note that it doesn't check the entire structure, - // but just the few last nodes of low height - - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - let root_node_copy = dk.nodes.clone(); - - // this is a root node - assert_eq!(dk.nodes.len(), 1); - assert!(dk.nodes[0].is_root()); - - // we have to have a node for right branch on each height (1, 01, 001, ... etc) - // plus an additional one for the two left-most leaves (epochs "0" and "1") - dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap(); - assert_eq!(dk.nodes.len(), 33); - - let expected_last = Tau::new(0); - // (and yes, I had to look up those names in a thesaurus) - let expected_penultimate = Tau::new(1); - // note that this value is 31bit long - let expected_antepenultimate = Tau(bitvec![u32, Msb0; - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1 - ]); - - let mut nodes_iter = dk.nodes.iter().rev(); - assert_eq!(expected_last, nodes_iter.next().unwrap().tau); - assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau); - assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau); - - let mut epoch_zero_nodes = dk.nodes.clone(); - - // nodes for epoch1 should be identical for those for epoch0 minus the 00..00 leaf - dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap(); - assert_eq!(dk.nodes.len(), 32); - epoch_zero_nodes.pop().unwrap(); - assert_eq!( - epoch_zero_nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>(), - dk.nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>() - ); - - dk.try_update_to(Epoch::new(2), ¶ms, &mut rng).unwrap(); - dk.try_update_to(Epoch::new(3), ¶ms, &mut rng).unwrap(); - dk.try_update_to(Epoch::new(4), ¶ms, &mut rng).unwrap(); - - let expected_last = Tau::new(4); - let expected_penultimate = Tau::new(5); - let expected_antepenultimate = Tau(bitvec![u32, Msb0; - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 - ]); - let expected_preantepenultimate = Tau(bitvec![u32, Msb0; - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 - ]); - assert_eq!(dk.nodes.len(), 32); - let mut nodes_iter = dk.nodes.iter().rev(); - assert_eq!(expected_last, nodes_iter.next().unwrap().tau); - assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau); - assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau); - assert_eq!(expected_preantepenultimate, nodes_iter.next().unwrap().tau); - - // the result should be the same of regardless if we update incrementally or go to the target immediately - let mut new_root = DecryptionKey { - nodes: root_node_copy, - }; - new_root - .try_update_to(Epoch::new(4), ¶ms, &mut rng) - .unwrap(); - assert_eq!( - dk.nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>(), - new_root - .nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>() - ); - - // getting expected nodes for those epochs is non-trivial for test purposes, but the last node - // should ALWAYS be equal to the target epoch - dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); - assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(42)); - dk.try_update_to(Epoch::new(123456), ¶ms, &mut rng) - .unwrap(); - assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(123456)); - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(3292547435)); - - // trying to go to past epochs fails - assert!(dk - .try_update_to(Epoch::new(531), ¶ms, &mut rng) - .is_err()) - } - - #[test] - #[ignore] // expensive test - fn updating_to_next_epoch() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - // for root node current epoch is `None` - assert_eq!(None, dk.current_epoch(¶ms).unwrap()); - - // for root node it should result in epoch 0 - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(0)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(1)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(2)), dk.current_epoch(¶ms).unwrap()); - - // if we start from some non-root epoch, it should result in l + 1 - dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(43)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to(Epoch::new(12345), ¶ms, &mut rng) - .unwrap(); - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(12346)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!( - Some(Epoch::new(3292547436)), - dk.current_epoch(¶ms).unwrap() - ); - } - #[test] fn public_key_with_proof_roundtrip() { let params = setup(); @@ -816,64 +232,4 @@ mod tests { assert_eq!(pk, recovered) } - - #[test] - #[ignore] // expensive test - fn bte_node_roundtrip() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - let root_node = dk.nodes[0].clone(); - let bytes = root_node.to_bytes(); - let recovered = Node::try_from_bytes(&bytes).unwrap(); - assert_eq!(root_node, recovered); - - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - for node in &dk.nodes { - let bytes = node.to_bytes(); - let recovered = Node::try_from_bytes(&bytes).unwrap(); - assert_eq!(node, &recovered); - } - } - - #[test] - #[ignore] // expensive test - fn decryption_key_node_roundtrip() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - } } diff --git a/common/crypto/dkg/src/bte/mod.rs b/common/crypto/dkg/src/bte/mod.rs index cb3dd07fa4..2eca744cf1 100644 --- a/common/crypto/dkg/src/bte/mod.rs +++ b/common/crypto/dkg/src/bte/mod.rs @@ -1,17 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::DkgError; -use crate::utils::{hash_g2, RandomOracleBuilder}; +use crate::utils::hash_g2; use crate::{Chunk, Share}; -use bitvec::field::BitField; -use bitvec::order::Msb0; -use bitvec::vec::BitVec; -use bitvec::view::BitView; -use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt}; +use bls12_381::{G1Affine, G2Affine, G2Prepared, G2Projective, Gt}; use group::Curve; use lazy_static::lazy_static; -use zeroize::Zeroize; pub mod encryption; pub mod keys; @@ -35,10 +29,6 @@ lazy_static! { // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1 const SETUP_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381G2_XMD:SHA-256_SSWU_RO_SETUP"; -// this particular domain is not for curve hashing, but might as well also follow the same naming pattern -const TREE_TAU_EXTENSION_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_TREE_EXTENSION"; - -const MAX_EPOCHS_EXP: usize = 32; const HASH_SECURITY_PARAM: usize = 256; // note: CHUNK_BYTES * NUM_CHUNKS must equal to SCALAR_SIZE @@ -49,253 +39,17 @@ pub const SCALAR_SIZE: usize = 32; /// In paper B; number of distinct chunks pub const CHUNK_SIZE: usize = 1 << (CHUNK_BYTES << 3); -pub(crate) type EpochStore = u32; - -#[derive(Clone, Debug, PartialEq, PartialOrd)] -// None empty bitvec implies this is a root node -pub(crate) struct Tau(BitVec); - -impl Tau { - pub fn new_root() -> Self { - Tau(BitVec::new()) - } - - // TODO: perhaps this should be explicitly moved to some test module - #[cfg(test)] - pub(crate) fn new(epoch: EpochStore) -> Self { - Tau(epoch.view_bits().to_bitvec()) - } - - #[allow(unused)] - pub fn left_child(&self) -> Self { - let mut child = self.0.clone(); - child.push(false); - Tau(child) - } - - pub fn right_child(&self) -> Self { - let mut child = self.0.clone(); - child.push(true); - Tau(child) - } - - pub fn is_leaf(&self, params: &Params) -> bool { - self.height() == params.lambda_t - } - - pub fn try_get_parent_at_height(&self, height: usize) -> Result { - if height > self.0.len() { - return Err(DkgError::NotAValidParent); - } - - Ok(Tau(self.0[..height].to_bitvec())) - } - - // essentially is this tau prefixing the other - pub fn is_parent_of(&self, other: &Tau) -> bool { - if self.0.len() > other.0.len() { - return false; - } - - for (i, b) in self.0.iter().enumerate() { - if b != other.0[i] { - return false; - } - } - - true - } - - pub fn lowest_valid_epoch_child(&self, params: &Params) -> Result { - if self.0.len() > params.lambda_t { - // this node is already BELOW a valid leaf-epoch node. it can only happen - // if either some invariant was broken or additional data was pushed to `tau` - // in order compute some intermediate results, but in that case this method should have - // never been called anyway. tl;dr: if this is called, the underlying key is malformed - return Err(DkgError::NotAValidParent); - } - let mut child = self.0.clone(); - for _ in 0..(params.lambda_t - self.0.len()) { - child.push(false) - } - - // the unwrap here is fine as we ensure we have exactly `params.tree_height` bits here - // (we could just propagate the error instead of unwraping and putting it behind an `Ok` anyway - // but I'd prefer to just blow up since this would be a serious error - Ok(Epoch::try_from_tau(&Tau(child), params).unwrap()) - } - - pub fn height(&self) -> usize { - self.0.len() - } - - fn extend( - &self, - rr: &[G1Projective; NUM_CHUNKS], - ss: &[G1Projective; NUM_CHUNKS], - cc: &[[G1Projective; NUM_CHUNKS]], - ) -> Self { - let mut random_oracle_builder = RandomOracleBuilder::new(TREE_TAU_EXTENSION_DOMAIN); - random_oracle_builder.update_with_g1_elements(rr.iter()); - random_oracle_builder.update_with_g1_elements(ss.iter()); - for ciphertext_chunks in cc { - random_oracle_builder.update_with_g1_elements(ciphertext_chunks.iter()); - } - - let tau_mem = self.0.as_raw_slice(); - assert_eq!(tau_mem.len(), 1, "tau length invariant was broken"); - random_oracle_builder.update(tau_mem[0].to_be_bytes()); - - let oracle_output = random_oracle_builder.finalize(); - debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM); - - let mut extended_tau = self.clone(); - for byte in oracle_output { - extended_tau - .0 - .extend_from_bitslice(byte.view_bits::()) - } - - extended_tau - } - - // considers all lambda_t + lambda_h bits - fn evaluate_f(&self, params: &Params) -> G2Projective { - self.0 - .iter() - .by_vals() - .zip(params.fs.iter().chain(params.fh.iter())) - .filter(|(i, _)| *i) - .map(|(_, f_i)| f_i) - .fold(params.f0, |acc, f_i| acc + f_i) - } - - // only considers up to lambda_t bits - fn evaluate_partial_f(&self, params: &Params) -> G2Projective { - self.0 - .iter() - .by_vals() - .zip(params.fs.iter()) - .filter(|(i, _)| *i) - .map(|(_, f_i)| f_i) - .fold(params.f0, |acc, f_i| acc + f_i) - } - - pub(crate) fn to_bytes(&self) -> Vec { - let len_bytes = (self.0.len() as u32).to_be_bytes(); - len_bytes - .into_iter() - .chain(self.0.chunks(8).map(BitField::load_be)) - .collect() - } - - pub(crate) fn try_from_bytes(b: &[u8]) -> Result { - if b.len() < 4 { - return Err(DkgError::new_deserialization_failure( - "Tau", - "insufficient number of bytes provided", - )); - } - let tau_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; - - // maximum theoretical length - if tau_len > MAX_EPOCHS_EXP + HASH_SECURITY_PARAM { - return Err(DkgError::new_deserialization_failure( - "Tau", - format!( - "malformed length {} is greater than maximum {}", - tau_len, - MAX_EPOCHS_EXP + HASH_SECURITY_PARAM - ), - )); - } - - if tau_len == 0 { - if b.len() != 4 { - Err(DkgError::new_deserialization_failure( - "Tau", - "malformed bytes", - )) - } else { - Ok(Tau::new_root()) - } - } else if b.len() == 4 { - Err(DkgError::new_deserialization_failure( - "Tau", - "insufficient number of bytes provided", - )) - } else { - let mut inner = BitVec::repeat(false, tau_len); - for (slot, &byte) in inner.chunks_mut(8).zip(b[4..].iter()) { - slot.store_be(byte); - } - - Ok(Tau(inner)) - } - } -} - -impl Zeroize for Tau { - fn zeroize(&mut self) { - for v in self.0.as_raw_mut_slice() { - v.zeroize() - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)] -pub struct Epoch(EpochStore); - -impl Epoch { - pub fn new(value: EpochStore) -> Self { - Epoch(value) - } - - pub(crate) fn as_tau(&self) -> Tau { - (*self).into() - } - - pub(crate) fn as_extended_tau( - &self, - rr: &[G1Projective; NUM_CHUNKS], - ss: &[G1Projective; NUM_CHUNKS], - cc: &[[G1Projective; NUM_CHUNKS]], - ) -> Tau { - self.as_tau().extend(rr, ss, cc) - } - - pub(crate) fn try_from_tau(tau: &Tau, params: &Params) -> Result { - if !tau.is_leaf(params) { - Err(DkgError::MalformedEpoch) - } else { - Ok(Epoch(tau.0.load_be())) - } - } -} - -impl From for Tau { - fn from(epoch: Epoch) -> Self { - Tau(epoch.0.view_bits().to_bitvec()) - } -} - -impl From for Epoch { - fn from(epoch: EpochStore) -> Self { - Epoch(epoch) - } +// considers all lambda_h bits +pub fn evaluate_f(params: &Params) -> G2Projective { + params.fh.iter().fold(params.f0, |acc, f_i| acc + f_i) } pub struct Params { - /// Maximum size of an epoch, in bits. - pub lambda_t: usize, - /// Security parameter of our $H_{\Lamda_H}$ hash function pub lambda_h: usize, - // keeping f0 separate from the rest of the curve points makes it easier to work with tau f0: G2Projective, - fs: Vec, // f_1, f_2, .... f_{lambda_t} in the paper - fh: Vec, // f_{lambda_t+1}, f_{lambda_t+1}, .... f_{lambda_t+lambda_h} in the paper + fh: Vec, // f_{lambda_h}, f_{lambda_h+1}, .... f_{lambda_h} in the paper h: G2Projective, /// Precomputed `h` used for the miller loop @@ -305,10 +59,6 @@ pub struct Params { pub fn setup() -> Params { let f0 = hash_g2(b"f0", SETUP_DOMAIN); - let fs = (1..=MAX_EPOCHS_EXP) - .map(|i| hash_g2(format!("f{}", i), SETUP_DOMAIN)) - .collect(); - let fh = (0..HASH_SECURITY_PARAM) .map(|i| hash_g2(format!("fh{}", i), SETUP_DOMAIN)) .collect(); @@ -316,148 +66,10 @@ pub fn setup() -> Params { let h = hash_g2(b"h", SETUP_DOMAIN); Params { - lambda_t: MAX_EPOCHS_EXP, lambda_h: HASH_SECURITY_PARAM, f0, - fs, fh, h, _h_prepared: G2Prepared::from(h.to_affine()), } } - -#[cfg(test)] -mod tests { - use super::*; - use bitvec::bitvec; - use bitvec::order::Msb0; - - #[test] - fn creating_tau_from_epoch() { - assert!(Tau::new_root().0.is_empty()); - - let zero = Tau::new(0); - assert!(zero.0.iter().by_vals().all(|b| !b)); - - let one = Tau::new(1); - let mut iter = one.0.iter().by_vals(); - // first 31 bits are 0, the last one is 1 - for _ in 0..31 { - assert!(!iter.next().unwrap()) - } - assert!(iter.next().unwrap()); - - // 101010 in binary - let forty_two = Tau::new(42); - // first 26 bits are not set - let mut iter = forty_two.0.iter().by_vals(); - for _ in 0..26 { - assert!(!iter.next().unwrap()) - } - assert!(iter.next().unwrap()); - assert!(!iter.next().unwrap()); - assert!(iter.next().unwrap()); - assert!(!iter.next().unwrap()); - assert!(iter.next().unwrap()); - assert!(!iter.next().unwrap()); - - // value that requires an actual u32 (i.e. takes 4 bytes to represent) - // 11000100_01000000_01001001_01101011 in binary - let big_val = Tau::new(3292547435); - let expected = bitvec![u32, Msb0; - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, - 0, 1, 1 - ]; - assert_eq!(expected, big_val.0) - } - - #[test] - fn getting_parent_at_height() { - let tau = Tau(bitvec![u32, Msb0; 1,0,1,1,0,0,1]); - - let expected_0 = Tau(BitVec::new()); - let expected_1 = Tau(bitvec![u32, Msb0; 1]); - let expected_5 = Tau(bitvec![u32, Msb0; 1,0,1,1,0]); - - assert_eq!(expected_0, tau.try_get_parent_at_height(0).unwrap()); - assert_eq!(expected_1, tau.try_get_parent_at_height(1).unwrap()); - assert_eq!(expected_5, tau.try_get_parent_at_height(5).unwrap()); - assert_eq!(tau, tau.try_get_parent_at_height(7).unwrap()); - assert!(tau.try_get_parent_at_height(8).is_err()) - } - - #[test] - fn converting_tau_to_epoch() { - let params = setup(); - - let tau0: Tau = Epoch::new(0).into(); - let tau1: Tau = Epoch::new(1).into(); - let tau42: Tau = Epoch::new(42).into(); - let tau_big: Tau = Epoch::new(3292547435).into(); - - assert_eq!(Epoch::new(0), Epoch::try_from_tau(&tau0, ¶ms).unwrap()); - assert_eq!(Epoch::new(1), Epoch::try_from_tau(&tau1, ¶ms).unwrap()); - assert_eq!( - Epoch::new(42), - Epoch::try_from_tau(&tau42, ¶ms).unwrap() - ); - assert_eq!( - Epoch::new(3292547435), - Epoch::try_from_tau(&tau_big, ¶ms).unwrap() - ); - - assert!(Epoch::try_from_tau(&Tau(BitVec::new()), ¶ms).is_err()); - assert!(Epoch::try_from_tau(&Tau(bitvec![u32, Msb0; 1,0,1,1,0]), ¶ms).is_err()); - let _31bit_tau = Tau(bitvec![u32, Msb0; - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, - 0, 1 - ]); - assert!(Epoch::try_from_tau(&_31bit_tau, ¶ms).is_err()); - - let _33bit_tau = Tau(bitvec![u32, Msb0; - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, - 0, 1, 1, 0 - ]); - assert!(Epoch::try_from_tau(&_33bit_tau, ¶ms).is_err()); - } - - #[test] - fn tau_roundtrip() { - let good_taus = vec![ - Tau::new_root(), - Tau::new(0), - Tau::new(1), - Tau::new(2), - Tau::new(42), - Tau::new(123456), - Tau::new(3292547435), - Tau::new(u32::MAX), - ]; - - for tau in good_taus { - let bytes = tau.to_bytes(); - let recovered = Tau::try_from_bytes(&bytes).unwrap(); - assert_eq!(tau, recovered); - } - - // more valid variants - let mut another_tau = Tau::new(u32::MAX); - another_tau.0.push(true); - another_tau.0.push(false); - another_tau.0.push(true); - - let bytes = another_tau.to_bytes(); - let recovered = Tau::try_from_bytes(&bytes).unwrap(); - assert_eq!(another_tau, recovered); - - // ensure there are no panics - let big_length_bytes = [255, 255, 255, 255, 42]; - assert!(Tau::try_from_bytes(&big_length_bytes).is_err()); - - assert!(Tau::try_from_bytes(&[]).is_err()); - assert!(Tau::try_from_bytes(&[1, 1, 1, 1]).is_err()); - assert!(Tau::try_from_bytes(&[0, 0, 0, 1]).is_err()); - assert!(Tau::try_from_bytes(&[1, 0, 0, 0]).is_err()); - assert!(Tau::try_from_bytes(&[1, 0, 0]).is_err()); - } -} diff --git a/common/crypto/dkg/src/dealing.rs b/common/crypto/dkg/src/dealing.rs index bf471d7e37..ead51b7f80 100644 --- a/common/crypto/dkg/src/dealing.rs +++ b/common/crypto/dkg/src/dealing.rs @@ -3,9 +3,7 @@ use crate::bte::proof_chunking::ProofOfChunking; use crate::bte::proof_sharing::ProofOfSecretSharing; -use crate::bte::{ - encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Epoch, Params, PublicKey, -}; +use crate::bte::{encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Params, PublicKey}; use crate::error::DkgError; use crate::interpolation::polynomial::{Polynomial, PublicCoefficients}; use crate::interpolation::{ @@ -34,7 +32,6 @@ impl Dealing { params: &Params, dealer_index: NodeIndex, threshold: Threshold, - epoch: Epoch, // BTreeMap ensures the keys are sorted by their indices receivers: &BTreeMap, prior_resharing_secret: Option, @@ -58,8 +55,7 @@ impl Dealing { .collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); - let (ciphertexts, hazmat) = - encrypt_shares(&remote_share_key_pairs, epoch, params, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, params, &mut rng); // create proofs of knowledge let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); @@ -108,7 +104,6 @@ impl Dealing { pub fn verify( &self, params: &Params, - epoch: Epoch, threshold: Threshold, receivers: &BTreeMap, prior_resharing_public: Option, @@ -134,7 +129,7 @@ impl Dealing { }); } - if !self.ciphertexts.verify_integrity(params, epoch) { + if !self.ciphertexts.verify_integrity(params) { return Err(DkgError::FailedCiphertextIntegrityCheck); } @@ -369,32 +364,18 @@ mod tests { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - let dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let shares = dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); derived_secrets.push( combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(), @@ -437,22 +418,10 @@ mod tests { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - let dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); @@ -478,7 +447,6 @@ mod tests { let parties = 5; let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold; let node_indices = (1..=parties).collect::>(); - let epoch = Epoch::new(2); let mut receivers = BTreeMap::new(); for index in &node_indices { @@ -491,7 +459,6 @@ mod tests { ¶ms, node_indices[0], threshold, - epoch, &receivers, None, ); diff --git a/common/crypto/dkg/tests/integration.rs b/common/crypto/dkg/tests/integration.rs index 8e6c001385..24518dd789 100644 --- a/common/crypto/dkg/tests/integration.rs +++ b/common/crypto/dkg/tests/integration.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use bls12_381::{G2Projective, Scalar}; -use dkg::bte::{decrypt_share, keygen, setup, Epoch}; +use dkg::bte::{decrypt_share, keygen, setup}; use dkg::interpolation::perform_lagrangian_interpolation_at_origin; use dkg::{combine_shares, try_recover_verification_keys, Dealing}; use rand_core::SeedableRng; @@ -32,9 +32,6 @@ fn single_sender() { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - // TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET // verify remote proofs of key possession for key in full_keys.iter() { @@ -46,23 +43,20 @@ fn single_sender() { ¶ms, node_indices[0], threshold, - epoch, &receivers, None, ); dealing - .verify(¶ms, epoch, threshold, &receivers, None) + .verify(¶ms, threshold, &receivers, None) .unwrap(); // make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check) for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap(); + let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap(); } // and for good measure, check that the dealer's share matches decryption result - let recovered_dealer = - decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, epoch, None).unwrap(); + let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); assert_eq!(recovered_dealer, dealer_share.unwrap()); } @@ -87,9 +81,6 @@ fn full_threshold_secret_sharing() { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - // TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET // verify remote proofs of key possession for key in full_keys.iter() { @@ -99,21 +90,12 @@ fn full_threshold_secret_sharing() { let dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); for dealing in dealings.iter() { dealing - .verify(¶ms, epoch, threshold, &receivers, None) + .verify(¶ms, threshold, &receivers, None) .unwrap(); } @@ -125,11 +107,9 @@ fn full_threshold_secret_sharing() { let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let shares = dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); // we know dealer_share matches, but it would be inconvenient to try to put them in here, @@ -183,22 +163,10 @@ fn full_threshold_secret_resharing() { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - let first_dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); @@ -208,11 +176,9 @@ fn full_threshold_secret_resharing() { let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let shares = first_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -227,8 +193,6 @@ fn full_threshold_secret_resharing() { ]) .unwrap(); - let next_epoch = Epoch::new(3); - // attempt to create resharing dealings! let resharing_dealings = node_indices .iter() @@ -239,7 +203,6 @@ fn full_threshold_secret_resharing() { ¶ms, dealer_index, threshold, - next_epoch, &receivers, Some(*prior_secret), ) @@ -249,7 +212,7 @@ fn full_threshold_secret_resharing() { for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) { reshared_dealing - .verify(¶ms, next_epoch, threshold, &receivers, Some(*prior_vk)) + .verify(¶ms, threshold, &receivers, Some(*prior_vk)) .unwrap(); } @@ -259,11 +222,9 @@ fn full_threshold_secret_resharing() { let mut reshared_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(next_epoch, ¶ms, &mut rng).unwrap(); - let shares = resharing_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, next_epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = From 6dde8ecd0ac93df65ca43912d759e679c90a0017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 15:49:42 +0100 Subject: [PATCH 23/75] Debugging --- .github/workflows/nightly_build_release.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 678eefc22f..601fbabf1e 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -26,6 +26,9 @@ jobs: - name: Set output variable to latest release branch id: step2 run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT + - name: Echo + id: step3 + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" build: needs: [get_release,matrix_prep] strategy: @@ -37,6 +40,9 @@ jobs: run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools if: matrix.os == 'ubuntu-latest' + - name: Echo latest release + run: echo "latest_release:" ${{needs.get_release.outputs.output1}} + - name: Check out latest release branch uses: actions/checkout@v3 with: From ce17196d48c381fded48b0eb41773d9c2117e3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 16:04:12 +0100 Subject: [PATCH 24/75] Debugging workflow --- .github/workflows/nightly_build_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 601fbabf1e..3f327dcda1 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -28,7 +28,7 @@ jobs: run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT - name: Echo id: step3 - run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" + run: echo "latest_release=$(git branch -r)" build: needs: [get_release,matrix_prep] strategy: From f568673fbcf0af55c5c8904b02810039d3ed86c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 16:13:37 +0100 Subject: [PATCH 25/75] Debugging workflow --- .github/workflows/nightly_build_release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 3f327dcda1..ee54aae0f7 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -22,7 +22,8 @@ jobs: steps: - name: Check out repository code uses: actions/checkout@v3 - + - name: Fetch all branches + run: git fetch --all - name: Set output variable to latest release branch id: step2 run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT From 3f5bfcc6963b512e890c13656a62a0b84c82f945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 16:20:44 +0100 Subject: [PATCH 26/75] Cleaned --- .github/workflows/nightly_build_release.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index ee54aae0f7..2ea9d18a4e 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -27,9 +27,6 @@ jobs: - name: Set output variable to latest release branch id: step2 run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT - - name: Echo - id: step3 - run: echo "latest_release=$(git branch -r)" build: needs: [get_release,matrix_prep] strategy: @@ -41,9 +38,6 @@ jobs: run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools if: matrix.os == 'ubuntu-latest' - - name: Echo latest release - run: echo "latest_release:" ${{needs.get_release.outputs.output1}} - - name: Check out latest release branch uses: actions/checkout@v3 with: From 23e97e9643ff197435cb7eb918db1fd26f438420 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 21 Nov 2022 16:23:36 +0100 Subject: [PATCH 27/75] Set schedule for nightly build on release --- .github/workflows/nightly_build_release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 2ea9d18a4e..9a1ba43dc5 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -1,6 +1,8 @@ name: Nightly builds on latest release -on: workflow_dispatch +on: + schedule: + - cron: '14 2 * * *' jobs: matrix_prep: runs-on: ubuntu-latest From fea6f44a57c8935c92d57588c4b608a1caf04d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 21 Nov 2022 17:28:26 +0200 Subject: [PATCH 28/75] Feature/dkg (#1678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Port code without epoch and blacklisting Co-authored-by: Jędrzej Stuczyński * Add dkg contract to validator client Co-authored-by: Jędrzej Stuczyński * Introduce publisher Co-authored-by: Jędrzej Stuczyński * Fix mock testing client * Apply fmt to contract * Get data from attributes * Minor fixes * Fix wasm client * Add pem files for dkg keys * Save/load dkg keys in/from pem files * Get dealer old or fresh dealer index Co-authored-by: Jędrzej Stuczyński --- Cargo.lock | 21 ++ Cargo.toml | 1 + .../client-libs/validator-client/Cargo.toml | 4 +- .../validator-client/src/client.rs | 84 +++++++- .../src/nymd/cosmwasm_client/logs.rs | 3 + .../validator-client/src/nymd/mod.rs | 13 ++ .../src/nymd/traits/dkg_query_client.rs | 104 ++++++++++ .../src/nymd/traits/dkg_signing_client.rs | 73 +++++++ .../validator-client/src/nymd/traits/mod.rs | 4 + .../src/event_attributes.rs | 4 + .../coconut-bandwidth-contract/src/lib.rs | 1 + .../coconut-dkg/Cargo.toml | 13 ++ .../coconut-dkg/src/dealer.rs | 102 ++++++++++ .../coconut-dkg/src/event_attributes.rs | 4 + .../coconut-dkg/src/lib.rs | 4 + .../coconut-dkg/src/msg.rs | 55 ++++++ .../coconut-dkg/src/types.rs | 98 +++++++++ .../contracts-common/Cargo.toml | 9 +- .../contracts-common/src/commitment.rs | 186 ++++++++++++++++++ .../contracts-common/src/lib.rs | 1 + common/crypto/dkg/Cargo.toml | 2 + common/crypto/dkg/src/bte/keys.rs | 78 ++++++++ common/network-defaults/src/lib.rs | 13 ++ common/network-defaults/src/mainnet.rs | 1 + common/network-defaults/src/var_names.rs | 1 + contracts/Cargo.lock | 26 +++ contracts/Cargo.toml | 2 +- contracts/coconut-dkg/Cargo.toml | 22 +++ contracts/coconut-dkg/src/constants.rs | 7 + contracts/coconut-dkg/src/dealers/mod.rs | 6 + contracts/coconut-dkg/src/dealers/queries.rs | 67 +++++++ contracts/coconut-dkg/src/dealers/storage.rs | 49 +++++ .../coconut-dkg/src/dealers/transactions.rs | 101 ++++++++++ contracts/coconut-dkg/src/dealings/mod.rs | 6 + contracts/coconut-dkg/src/dealings/queries.rs | 42 ++++ contracts/coconut-dkg/src/dealings/storage.rs | 25 +++ .../coconut-dkg/src/dealings/transactions.rs | 32 +++ contracts/coconut-dkg/src/error.rs | 53 +++++ contracts/coconut-dkg/src/lib.rs | 156 +++++++++++++++ contracts/coconut-dkg/src/state/mod.rs | 16 ++ envs/mainnet.env | 1 + envs/qa.env | 1 + .../websocket/connection_handler/coconut.rs | 4 +- nym-connect/Cargo.lock | 12 ++ nym-wallet/Cargo.lock | 12 ++ nym-wallet/nym-wallet-types/src/network.rs | 6 + validator-api/Cargo.toml | 10 +- validator-api/src/coconut/client.rs | 11 ++ validator-api/src/coconut/dkg/mod.rs | 71 +++++++ validator-api/src/coconut/dkg/publisher.rs | 60 ++++++ validator-api/src/coconut/error.rs | 3 + validator-api/src/coconut/mod.rs | 1 + validator-api/src/coconut/tests.rs | 22 +++ validator-api/src/config/mod.rs | 57 +++++- validator-api/src/config/template.rs | 6 + validator-api/src/main.rs | 25 ++- validator-api/src/nymd_client.rs | 51 ++++- 57 files changed, 1824 insertions(+), 18 deletions(-) create mode 100644 common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs create mode 100644 common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs create mode 100644 common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs create mode 100644 contracts/coconut-dkg/Cargo.toml create mode 100644 contracts/coconut-dkg/src/constants.rs create mode 100644 contracts/coconut-dkg/src/dealers/mod.rs create mode 100644 contracts/coconut-dkg/src/dealers/queries.rs create mode 100644 contracts/coconut-dkg/src/dealers/storage.rs create mode 100644 contracts/coconut-dkg/src/dealers/transactions.rs create mode 100644 contracts/coconut-dkg/src/dealings/mod.rs create mode 100644 contracts/coconut-dkg/src/dealings/queries.rs create mode 100644 contracts/coconut-dkg/src/dealings/storage.rs create mode 100644 contracts/coconut-dkg/src/dealings/transactions.rs create mode 100644 contracts/coconut-dkg/src/error.rs create mode 100644 contracts/coconut-dkg/src/lib.rs create mode 100644 contracts/coconut-dkg/src/state/mod.rs create mode 100644 validator-api/src/coconut/dkg/mod.rs create mode 100644 validator-api/src/coconut/dkg/publisher.rs diff --git a/Cargo.lock b/Cargo.lock index 6c382cce87..e079c021e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -636,6 +636,16 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" @@ -745,7 +755,11 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "contracts-common" version = "0.1.0" dependencies = [ + "blake3", + "bs58", "cosmwasm-std", + "digest 0.10.3", + "generic-array 0.14.5", "schemars", "serde", "serde_json", @@ -1411,6 +1425,7 @@ dependencies = [ "ff 0.11.0", "group 0.11.0", "lazy_static", + "pemstore", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.3", @@ -1971,6 +1986,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ + "serde", "typenum", "version_check", ] @@ -3377,9 +3393,11 @@ version = "1.1.0" dependencies = [ "anyhow", "async-trait", + "bs58", "cfg-if 1.0.0", "clap 3.2.8", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "config", "console-subscriber", @@ -3391,6 +3409,7 @@ dependencies = [ "cw-utils", "cw3", "dirs", + "dkg", "dotenv", "futures", "gateway-client", @@ -3404,6 +3423,7 @@ dependencies = [ "nymcoconut", "nymsphinx", "okapi", + "pemstore", "pin-project", "pretty_env_logger", "rand 0.7.3", @@ -6420,6 +6440,7 @@ dependencies = [ "base64", "bip39", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "colored", "config", diff --git a/Cargo.toml b/Cargo.toml index 4ab19fc70e..e03bd46aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", + "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/multisig-contract", diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 2b45232238..bd9bb6ef80 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -11,9 +11,11 @@ rust-version = "1.56" base64 = "0.13" colored = "2.0" cw3 = "0.13.1" + +coconut-dkg-common = { path = "../../cosmwasm-smart-contracts/coconut-dkg" } +contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" } vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" } -contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } vesting-contract = { path = "../../../contracts/vesting" } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index a4d0a22f05..e9e5fafc79 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{validator_api, ValidatorClientError}; +use coconut_dkg_common::dealer::ContractDealingCommitment; +use coconut_dkg_common::types::DealerDetails; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::MixId; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; @@ -16,7 +18,7 @@ use validator_api_requests::models::{ }; #[cfg(feature = "nymd-client")] -use crate::nymd::traits::MixnetQueryClient; +use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient}; #[cfg(feature = "nymd-client")] use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] @@ -43,6 +45,7 @@ pub struct Config { gateway_page_limit: Option, mixnode_delegations_page_limit: Option, rewarded_set_page_limit: Option, + dealers_page_limit: Option, } #[cfg(feature = "nymd-client")] @@ -72,6 +75,7 @@ impl Config { gateway_page_limit: None, mixnode_delegations_page_limit: None, rewarded_set_page_limit: None, + dealers_page_limit: None, }) } @@ -119,6 +123,7 @@ pub struct Client { gateway_page_limit: Option, mixnode_delegations_page_limit: Option, rewarded_set_page_limit: Option, + dealers_page_limit: Option, // ideally they would have been read-only, but unfortunately rust doesn't have such features pub validator_api: validator_api::Client, @@ -145,6 +150,7 @@ impl Client { gateway_page_limit: config.gateway_page_limit, mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, rewarded_set_page_limit: config.rewarded_set_page_limit, + dealers_page_limit: config.dealers_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -178,6 +184,7 @@ impl Client { gateway_page_limit: config.gateway_page_limit, mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, rewarded_set_page_limit: config.rewarded_set_page_limit, + dealers_page_limit: config.dealers_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -519,6 +526,81 @@ impl Client { Ok(events) } + + pub async fn get_all_nymd_current_dealers( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut dealers = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_current_dealers_paged(start_after.take(), self.dealers_page_limit) + .await?; + dealers.append(&mut paged_response.dealers); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(dealers) + } + + pub async fn get_all_nymd_past_dealers( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut dealers = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_past_dealers_paged(start_after.take(), self.dealers_page_limit) + .await?; + dealers.append(&mut paged_response.dealers); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(dealers) + } + + pub async fn get_all_nymd_epoch_dealings_commitments( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut commitments = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_dealings_commitments_paged(start_after.take(), self.dealers_page_limit) + .await?; + commitments.append(&mut paged_response.commitments); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(commitments) + } } // validator-api wrappers diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs index 7ddf3ef1dd..6c68c306f6 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs @@ -6,6 +6,9 @@ use cosmrs::tendermint::abci; use itertools::Itertools; use serde::{Deserialize, Serialize}; +pub use coconut_bandwidth_contract_common::event_attributes::*; +pub use coconut_dkg_common::event_attributes::*; + // it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response) // as theirs logs #[derive(Debug, Serialize, Deserialize)] diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 481cb021be..dabe4b0819 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -65,6 +65,7 @@ pub struct Config { pub(crate) bandwidth_claim_contract_address: Option, pub(crate) coconut_bandwidth_contract_address: Option, pub(crate) multisig_contract_address: Option, + pub(crate) coconut_dkg_contract_address: Option, // TODO: add this in later commits // pub(crate) gas_price: GasPrice, } @@ -118,6 +119,10 @@ impl Config { details.contracts.multisig_contract_address.as_ref(), prefix, )?, + coconut_dkg_contract_address: Self::parse_optional_account( + details.contracts.coconut_dkg_contract_address.as_ref(), + prefix, + )?, }) } } @@ -275,6 +280,14 @@ impl NymdClient { self.config.multisig_contract_address.as_ref().unwrap() } + // TODO: this should get changed into Result<&AccountId, NymdError> (or Option<&AccountId> in future commits + // note: what unwrap is doing here is just moving a failure that would have normally + // occurred in `connect` when attempting to parse an empty address, + // so it's not introducing new source of failure (just moves it) + pub fn coconut_dkg_contract_address(&self) -> &AccountId { + self.config.coconut_dkg_contract_address.as_ref().unwrap() + } + pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) { self.simulated_gas_multiplier = multiplier; } diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs new file mode 100644 index 0000000000..67efdec5cb --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs @@ -0,0 +1,104 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::error::NymdError; +use crate::nymd::{CosmWasmClient, NymdClient}; +use async_trait::async_trait; +use coconut_dkg_common::dealer::{ + DealerDetailsResponse, PagedCommitmentsResponse, PagedDealerResponse, +}; +use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; +use coconut_dkg_common::types::MinimumDepositResponse; +use cosmrs::AccountId; + +#[async_trait] +pub trait DkgQueryClient { + async fn get_dealer_details( + &self, + address: &AccountId, + ) -> Result; + async fn get_current_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; + async fn get_past_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; + + async fn get_deposit_amount(&self) -> Result; + async fn get_dealings_commitments_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; +} + +#[async_trait] +impl DkgQueryClient for NymdClient +where + C: CosmWasmClient + Send + Sync, +{ + async fn get_dealer_details( + &self, + address: &AccountId, + ) -> Result { + let request = DkgQueryMsg::GetDealerDetails { + dealer_address: address.to_string(), + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_current_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetCurrentDealers { + start_after, + limit: page_limit, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_past_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetPastDealers { + start_after, + limit: page_limit, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_deposit_amount(&self) -> Result { + let request = DkgQueryMsg::GetDepositAmount {}; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_dealings_commitments_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetDealingsCommitments { + limit: page_limit, + start_after, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs new file mode 100644 index 0000000000..9c9346d901 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs @@ -0,0 +1,73 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::cosmwasm_client::types::ExecuteResult; +use crate::nymd::error::NymdError; +use crate::nymd::traits::dkg_query_client::DkgQueryClient; +use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; +use async_trait::async_trait; +use coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg; +use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +use contracts_common::commitment::ContractSafeCommitment; + +#[async_trait] +pub trait DkgSigningClient { + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + fee: Option, + ) -> Result; + + async fn submit_dealing_commitment( + &self, + commitment: ContractSafeCommitment, + fee: Option, + ) -> Result; +} + +#[async_trait] +impl DkgSigningClient for NymdClient +where + C: SigningCosmWasmClient + Send + Sync, +{ + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + fee: Option, + ) -> Result { + let req = DkgExecuteMsg::RegisterDealer { + bte_key_with_proof: bte_key, + }; + let deposit = self.get_deposit_amount().await?; + + self.client + .execute( + self.address(), + self.coconut_dkg_contract_address(), + &req, + fee.unwrap_or_default(), + format!("registering {} as a dealer", self.address()), + vec![deposit.amount.into()], + ) + .await + } + + async fn submit_dealing_commitment( + &self, + commitment: ContractSafeCommitment, + fee: Option, + ) -> Result { + let req = DkgExecuteMsg::CommitDealing { commitment }; + + self.client + .execute( + self.address(), + self.coconut_dkg_contract_address(), + &req, + fee.unwrap_or_default(), + "dealing commitment", + vec![], + ) + .await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/mod.rs b/common/client-libs/validator-client/src/nymd/traits/mod.rs index 61a562a5b3..11e70cd484 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mod.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mod.rs @@ -3,6 +3,8 @@ mod coconut_bandwidth_query_client; mod coconut_bandwidth_signing_client; +mod dkg_query_client; +mod dkg_signing_client; mod mixnet_query_client; mod mixnet_signing_client; mod multisig_query_client; @@ -12,6 +14,8 @@ mod vesting_signing_client; pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient; pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; +pub use dkg_query_client::DkgQueryClient; +pub use dkg_signing_client::DkgSigningClient; pub use mixnet_query_client::MixnetQueryClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_query_client::MultisigQueryClient; diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs new file mode 100644 index 0000000000..431caa226c --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs index 34b5556745..56cb54e81d 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs @@ -1,4 +1,5 @@ pub mod deposit; +pub mod event_attributes; pub mod events; pub mod msg; pub mod spend_credential; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml b/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml new file mode 100644 index 0000000000..1ba84dadbc --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "coconut-dkg-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = "1.0.0" +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } + +contracts-common = { path = "../contracts-common" } \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs new file mode 100644 index 0000000000..31ecce7269 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -0,0 +1,102 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ContractSafeCommitment, EncodedBTEPublicKeyWithProof, NodeIndex}; +use cosmwasm_std::{Addr, Coin}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct DealerDetails { + pub address: Addr, + pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, + pub assigned_index: NodeIndex, + pub deposit: Coin, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DealerType { + Current, + Past, + Unknown, +} + +impl DealerType { + pub fn is_current(&self) -> bool { + matches!(&self, DealerType::Current) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct DealerDetailsResponse { + pub details: Option, + pub dealer_type: DealerType, +} + +impl DealerDetailsResponse { + pub fn new(details: Option, dealer_type: DealerType) -> Self { + DealerDetailsResponse { + details, + dealer_type, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PagedDealerResponse { + pub dealers: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedDealerResponse { + pub fn new( + dealers: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedDealerResponse { + dealers, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ContractDealingCommitment { + pub commitment: ContractSafeCommitment, + pub dealer: Addr, +} + +impl ContractDealingCommitment { + pub fn new(commitment: ContractSafeCommitment, dealer: Addr) -> Self { + ContractDealingCommitment { commitment, dealer } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PagedCommitmentsResponse { + pub commitments: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedCommitmentsResponse { + pub fn new( + commitments: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedCommitmentsResponse { + commitments, + per_page, + start_next_after, + } + } +} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs new file mode 100644 index 0000000000..ed28d113ce --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const NODE_INDEX: &str = "node_index"; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs new file mode 100644 index 0000000000..993fc3ae41 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs @@ -0,0 +1,4 @@ +pub mod dealer; +pub mod event_attributes; +pub mod msg; +pub mod types; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs new file mode 100644 index 0000000000..1e6741a66c --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -0,0 +1,55 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ContractSafeCommitment, EncodedBTEPublicKeyWithProof}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg { + pub group_addr: String, + pub mix_denom: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + RegisterDealer { + bte_key_with_proof: EncodedBTEPublicKeyWithProof, + }, + + CommitDealing { + commitment: ContractSafeCommitment, + }, + + // DEBUG ONLY TXs. THEY SHALL BE REMOVED BEFORE FINALISING THE CODE + // only exists for debugging purposes on local network to reset the entire state of the contract + DebugUnsafeResetAll { + init_msg: InstantiateMsg, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetDealerDetails { + dealer_address: String, + }, + GetCurrentDealers { + limit: Option, + start_after: Option, + }, + GetPastDealers { + limit: Option, + start_after: Option, + }, + GetDepositAmount {}, + GetDealingsCommitments { + limit: Option, + start_after: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs new file mode 100644 index 0000000000..c5b26483b4 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -0,0 +1,98 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +pub use crate::dealer::{DealerDetails, PagedDealerResponse}; +pub use contracts_common::commitment::ContractSafeCommitment; +pub use cosmwasm_std::{Addr, Coin}; + +pub type EncodedBTEPublicKeyWithProof = String; +pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str; +pub type NodeIndex = u64; + +// currently (it is still extremely likely to change, we might be able to get rid of verification key-related complaints), +// the epoch can be in the following states (in order): +// 1. PublicKeySubmission -> potential dealers are submitting their BTE and ed25519 public keys to participate in dealing exchange +// 2. DealingExchange -> the actual (off-chain) dealing exchange is happening +// 3. ComplaintSubmission -> receivers submitting evidence of other dealers sending malformed data +// 4. ComplaintVoting -> (if any complaints were submitted) receivers voting on the validity of the evidence provided +// 5. VerificationKeySubmission -> receivers submitting their partial (and master) verification keys +// 6. VerificationKeyMismatchSubmission -> receivers / watchers raising issue that the submitted VK are mismatched with their local derivations +// 7. VerificationKeyMismatchVoting -> (if any complaints were submitted) receivers voting on received mismatches +// 8. InProgress -> all receivers have all their secrets derived and all is good +// +// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd` +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] +#[serde(rename_all = "snake_case")] +pub enum EpochState { + PublicKeySubmission, + DealingExchange, + ComplaintSubmission, + ComplaintVoting, + VerificationKeySubmission, + VerificationKeyMismatchSubmission, + VerificationKeyMismatchVoting, + InProgress, +} + +impl Display for EpochState { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EpochState::PublicKeySubmission => write!(f, "PublicKeySubmission"), + EpochState::DealingExchange => write!(f, "DealingExchange"), + EpochState::ComplaintSubmission => write!(f, "ComplaintSubmission"), + EpochState::ComplaintVoting => write!(f, "ComplaintVoting"), + EpochState::VerificationKeySubmission => write!(f, "VerificationKeySubmission"), + EpochState::VerificationKeyMismatchSubmission => { + write!(f, "VerificationKeyMismatchSubmission") + } + EpochState::VerificationKeyMismatchVoting => { + write!(f, "VerificationKeyMismatchVoting") + } + EpochState::InProgress => write!(f, "InProgress"), + } + } +} + +impl EpochState { + pub fn next(self) -> Option { + match self { + EpochState::PublicKeySubmission => Some(EpochState::DealingExchange), + EpochState::DealingExchange => Some(EpochState::ComplaintSubmission), + EpochState::ComplaintSubmission => Some(EpochState::ComplaintVoting), + EpochState::ComplaintVoting => Some(EpochState::VerificationKeySubmission), + EpochState::VerificationKeySubmission => { + Some(EpochState::VerificationKeyMismatchSubmission) + } + EpochState::VerificationKeyMismatchSubmission => { + Some(EpochState::VerificationKeyMismatchVoting) + } + EpochState::VerificationKeyMismatchVoting => Some(EpochState::InProgress), + EpochState::InProgress => None, + } + } + + pub fn all_until(&self, end: Self) -> Vec { + let mut states = vec![*self]; + while states.last().unwrap() != &end { + let next_state = states.last().unwrap().next().expect("somehow reached the end of state diff -> this should be impossible under any circumstances!"); + states.push(next_state); + } + + states + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct MinimumDepositResponse { + pub amount: Coin, +} + +impl MinimumDepositResponse { + pub fn new(amount: Coin) -> Self { + MinimumDepositResponse { amount } + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 4f98451100..9c34b5da8f 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -7,10 +7,17 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +blake3 = { version = "1.3.1", features = ["traits-preview"], optional = true } +bs58 = "0.4.0" cosmwasm-std = "1.0.0" -serde = { version = "1.0", features = ["derive"] } +digest = { version = "0.10.3", optional = true } +generic-array = { version = "0.14.5", features = ["serde"], optional = true } schemars = "0.8" +serde = { version = "1.0", features = ["derive"] } thiserror = "1" [dev-dependencies] serde_json = "1.0.0" + +[features] +committable_trait = ["blake3", "digest", "generic-array"] diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs b/common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs new file mode 100644 index 0000000000..e104090257 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs @@ -0,0 +1,186 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "committable_trait")] +pub use digest::{Digest, Output}; +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{Display, Formatter}; +#[cfg(feature = "committable_trait")] +use std::marker::PhantomData; +use std::ops::Deref; + +// some sane upper-bound size on commitment sizes +// currently set to 1024bits +pub const MAX_COMMITMENT_SIZE: usize = 128; + +// TODO: if we are to use commitments for different types, it might make sense to introduce something like +// CommitmentTypeId field on the below for distinguishing different ones. it would somehow become part of the trait +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)] +pub struct ContractSafeCommitment(Vec); + +impl Deref for ContractSafeCommitment { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for ContractSafeCommitment { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !self.0.is_empty() { + write!(f, "0x")?; + } + for byte in self.0.iter().take(MAX_COMMITMENT_SIZE) { + write!(f, "{:02X}", byte)?; + } + // just some sanity safeguards + if self.0.len() > MAX_COMMITMENT_SIZE { + write!(f, "...")?; + } + Ok(()) + } +} + +// since cosmwasm stores everything with byte representation of stringified json, it's actually more efficient +// to serialize this as a string as opposed to keeping it as vector of bytes. +// for example vec![255,255] would have string representation of "[255,255]" and will be serialized to +// [91, 50, 53, 53, 44, 50, 53, 53, 93]. the equivalent base58 encoded string `"LUv"` will be serialized to +// [34, 76, 85, 118, 34] +// +// the difference between base58 and base64 is rather minimal and I've gone with base58 for consistency sake +impl Serialize for ContractSafeCommitment { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&bs58::encode(&self.0).into_string()) + } +} + +impl<'de> Deserialize<'de> for ContractSafeCommitment { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + let bytes = bs58::decode(&s) + .into_vec() + .map_err(serde::de::Error::custom)?; + Ok(ContractSafeCommitment(bytes)) + } +} + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct UnsupportedCommitmentSize; + +#[cfg(feature = "committable_trait")] +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub struct InconsistentCommitmentSize; + +#[cfg(feature = "committable_trait")] +pub type DefaultHasher = blake3::Hasher; + +#[cfg(feature = "committable_trait")] +pub trait Committable { + type DigestAlgorithm: Digest; + + fn commitment_size() -> usize { + ::output_size() + } + + fn to_bytes(&self) -> Vec; + + fn produce_commitment(&self) -> MessageCommitment { + MessageCommitment { + commitment: Self::DigestAlgorithm::digest(self.to_bytes()), + _message_type: Default::default(), + } + } + + fn verify_commitment(&self, commitment: &MessageCommitment) -> bool { + let recomputed = self.produce_commitment(); + recomputed.commitment == commitment.commitment + } +} + +#[cfg(feature = "committable_trait")] +#[derive(Debug, Serialize, Deserialize)] +pub struct MessageCommitment +where + T: ?Sized + Committable, +{ + commitment: Output, + + #[serde(skip)] + _message_type: PhantomData, +} + +#[cfg(feature = "committable_trait")] +impl Clone for MessageCommitment +where + T: ?Sized + Committable, +{ + fn clone(&self) -> Self { + MessageCommitment { + commitment: self.commitment.clone(), + _message_type: Default::default(), + } + } +} + +#[cfg(feature = "committable_trait")] +impl MessageCommitment +where + T: ?Sized + Committable, +{ + pub fn value(&self) -> &[u8] { + self.commitment.as_ref() + } + + pub fn unchecked_set_value(value: &[u8]) -> Self { + MessageCommitment { + commitment: Output::::clone_from_slice(value), + _message_type: Default::default(), + } + } + + pub fn new(message: &T) -> MessageCommitment { + message.produce_commitment() + } + + pub fn contract_safe_commitment(&self) -> ContractSafeCommitment { + self.into() + } + + pub fn is_same_as(&self, other: &ContractSafeCommitment) -> bool { + self.commitment.as_slice() == other.0 + } +} + +#[cfg(feature = "committable_trait")] +impl<'a, T> From<&'a MessageCommitment> for ContractSafeCommitment +where + T: ?Sized + Committable, +{ + fn from(commitment: &'a MessageCommitment) -> Self { + ContractSafeCommitment(commitment.value().to_vec()) + } +} + +#[cfg(feature = "committable_trait")] +impl<'a, T> TryFrom<&'a ContractSafeCommitment> for MessageCommitment +where + T: ?Sized + Committable, +{ + type Error = InconsistentCommitmentSize; + + fn try_from(value: &'a ContractSafeCommitment) -> Result { + if value.len() != ::output_size() { + Err(InconsistentCommitmentSize) + } else { + Ok(MessageCommitment::unchecked_set_value(value)) + } + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index f4d3906558..31fd5a4a19 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -4,6 +4,7 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +pub mod commitment; pub mod events; pub mod types; diff --git a/common/crypto/dkg/Cargo.toml b/common/crypto/dkg/Cargo.toml index c6e3edefd4..ea20508321 100644 --- a/common/crypto/dkg/Cargo.toml +++ b/common/crypto/dkg/Cargo.toml @@ -25,6 +25,8 @@ serde_derive = "1.0" thiserror = "1.0" zeroize = { version = "1.4", features = ["zeroize_derive"] } +pemstore = { path = "../../pemstore" } + [dependencies.group] version = "0.11" default-features = false diff --git a/common/crypto/dkg/src/bte/keys.rs b/common/crypto/dkg/src/bte/keys.rs index c2c0e91a33..a7962c2797 100644 --- a/common/crypto/dkg/src/bte/keys.rs +++ b/common/crypto/dkg/src/bte/keys.rs @@ -8,6 +8,7 @@ use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar}; use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use rand_core::RngCore; use zeroize::Zeroize; @@ -59,6 +60,20 @@ pub struct PublicKeyWithProof { pub(crate) proof: ProofOfDiscreteLog, } +impl PemStorableKey for PublicKeyWithProof { + type Error = DkgError; + + fn pem_type() -> &'static str { + "DKG PUBLIC KEY WITH PROOF" + } + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + fn from_bytes(bytes: &[u8]) -> Result { + Self::try_from_bytes(bytes) + } +} + impl PublicKeyWithProof { pub fn verify(&self) -> bool { self.key.verify(&self.proof) @@ -135,6 +150,20 @@ pub struct DecryptionKey { pub(crate) e: G2Projective, } +impl PemStorableKey for DecryptionKey { + type Error = DkgError; + + fn pem_type() -> &'static str { + "DKG DECRYPTION KEY" + } + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + fn from_bytes(bytes: &[u8]) -> Result { + Self::try_from_bytes(bytes) + } +} + impl DecryptionKey { fn new_root(a: G1Projective, b: G2Projective, dh: Vec, e: G2Projective) -> Self { DecryptionKey { a, b, dh, e } @@ -213,6 +242,55 @@ impl DecryptionKey { } } +pub struct KeyPair { + pub(crate) private_key: DecryptionKey, + pub(crate) public_key: PublicKeyWithProof, +} + +impl KeyPair { + pub fn new(params: &Params, rng: impl RngCore) -> Self { + let (dk, pk) = keygen(params, rng); + Self { + private_key: dk, + public_key: pk, + } + } + pub fn private_key(&self) -> &DecryptionKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKeyWithProof { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Ok(KeyPair { + private_key: DecryptionKey::try_from_bytes(priv_bytes)?, + public_key: PublicKeyWithProof::try_from_bytes(pub_bytes)?, + }) + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = DecryptionKey; + type PublicPemKey = PublicKeyWithProof; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_keys(private_key: Self::PrivatePemKey, public_key: Self::PublicPemKey) -> Self { + KeyPair { + private_key, + public_key, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index c600199140..d7b01622b1 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -36,6 +36,7 @@ pub struct NymContracts { pub bandwidth_claim_contract_address: Option, pub coconut_bandwidth_contract_address: Option, pub multisig_contract_address: Option, + pub coconut_dkg_contract_address: Option, } // I wanted to use the simpler `NetworkDetails` name, but there's a clash @@ -96,6 +97,9 @@ impl NymNetworkDetails { .with_multisig_contract(Some( var(var_names::MULTISIG_CONTRACT_ADDRESS).expect("multisig contract not set"), )) + .with_coconut_dkg_contract(Some( + var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"), + )) } pub fn new_mainnet() -> Self { @@ -121,6 +125,9 @@ impl NymNetworkDetails { mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str( + mainnet::COCONUT_DKG_CONTRACT_ADDRESS, + ), }, } } @@ -190,6 +197,12 @@ impl NymNetworkDetails { self.contracts.multisig_contract_address = contract.map(Into::into); self } + + #[must_use] + pub fn with_coconut_dkg_contract>(mut self, contract: Option) -> Self { + self.contracts.coconut_dkg_contract_address = contract.map(Into::into); + self + } } #[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 9e93b9194b..c2e16dee00 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -18,6 +18,7 @@ pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index d1f3cf2736..4e62e444cc 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -15,6 +15,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS"; pub const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "BANDWIDTH_CLAIM_CONTRACT_ADDRESS"; pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT_ADDRESS"; pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; +pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS"; pub const NYMD_VALIDATOR: &str = "NYMD_VALIDATOR"; diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 1671df0a3a..810da4ac89 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -189,6 +189,31 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg" +version = "0.1.0" +dependencies = [ + "coconut-dkg-common", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus", + "cw4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "coconut-test" version = "0.1.0" @@ -220,6 +245,7 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", "schemars", "serde", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 1787d0ca52..de21d0fc92 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group", "coconut-test"] +members = ["coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group", "coconut-test", "coconut-dkg"] [profile.release] opt-level = 3 diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml new file mode 100644 index 0000000000..479187c370 --- /dev/null +++ b/contracts/coconut-dkg/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "coconut-dkg" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" } + +cosmwasm-std = "1.0.0" +cosmwasm-storage = "1.0.0" +cw-storage-plus = "0.13.4" +cw-controllers = "0.13.4" +cw4 = { version = "0.13.4" } + +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = "1.0.23" \ No newline at end of file diff --git a/contracts/coconut-dkg/src/constants.rs b/contracts/coconut-dkg/src/constants.rs new file mode 100644 index 0000000000..e250c889c0 --- /dev/null +++ b/contracts/coconut-dkg/src/constants.rs @@ -0,0 +1,7 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Uint128; + +// to be determined whether those should be constants or exist as contract state +pub(crate) const MINIMUM_DEPOSIT: Uint128 = Uint128::new(1_000_000_000); diff --git a/contracts/coconut-dkg/src/dealers/mod.rs b/contracts/coconut-dkg/src/dealers/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/coconut-dkg/src/dealers/queries.rs b/contracts/coconut-dkg/src/dealers/queries.rs new file mode 100644 index 0000000000..5221994dc0 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/queries.rs @@ -0,0 +1,67 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::storage::{self, IndexedDealersMap}; +use coconut_dkg_common::dealer::{DealerDetailsResponse, DealerType, PagedDealerResponse}; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +fn query_dealers( + deps: Deps<'_>, + start_after: Option, + limit: Option, + underlying_map: IndexedDealersMap<'_>, +) -> StdResult { + let limit = limit + .unwrap_or(storage::DEALERS_PAGE_DEFAULT_LIMIT) + .min(storage::DEALERS_PAGE_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let dealers = underlying_map + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = dealers.last().map(|dealer| dealer.address.clone()); + + Ok(PagedDealerResponse::new(dealers, limit, start_next_after)) +} + +pub fn query_dealer_details( + deps: Deps<'_>, + dealer_address: String, +) -> StdResult { + let addr = deps.api.addr_validate(&dealer_address)?; + if let Some(current) = storage::current_dealers().may_load(deps.storage, &addr)? { + return Ok(DealerDetailsResponse::new( + Some(current), + DealerType::Current, + )); + } + if let Some(past) = storage::past_dealers().may_load(deps.storage, &addr)? { + return Ok(DealerDetailsResponse::new(Some(past), DealerType::Past)); + } + Ok(DealerDetailsResponse::new(None, DealerType::Unknown)) +} + +pub fn query_current_dealers_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + query_dealers(deps, start_after, limit, storage::current_dealers()) +} + +pub fn query_past_dealers_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + query_dealers(deps, start_after, limit, storage::past_dealers()) +} diff --git a/contracts/coconut-dkg/src/dealers/storage.rs b/contracts/coconut-dkg/src/dealers/storage.rs new file mode 100644 index 0000000000..398193de23 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/storage.rs @@ -0,0 +1,49 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::types::{DealerDetails, NodeIndex}; +use cosmwasm_std::{Addr, StdResult, Storage}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, UniqueIndex}; + +const CURRENT_DEALERS_PK: &str = "crd"; +const PAST_DEALERS_PK: &str = "ptd"; +const DEALERS_NODE_INDEX_IDX_NAMESPACE: &str = "dni"; + +pub(crate) const DEALERS_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const DEALERS_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); + +pub(crate) type IndexedDealersMap<'a> = IndexedMap<'a, &'a Addr, DealerDetails, DealersIndex<'a>>; + +pub(crate) struct DealersIndex<'a> { + pub(crate) node_index: UniqueIndex<'a, NodeIndex, DealerDetails>, +} + +impl<'a> IndexList for DealersIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.node_index]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn current_dealers<'a>() -> IndexedDealersMap<'a> { + let indexes = DealersIndex { + node_index: UniqueIndex::new(|d| d.assigned_index, DEALERS_NODE_INDEX_IDX_NAMESPACE), + }; + IndexedMap::new(CURRENT_DEALERS_PK, indexes) +} + +pub(crate) fn past_dealers<'a>() -> IndexedDealersMap<'a> { + let indexes = DealersIndex { + node_index: UniqueIndex::new(|d| d.assigned_index, DEALERS_NODE_INDEX_IDX_NAMESPACE), + }; + IndexedMap::new(PAST_DEALERS_PK, indexes) +} + +pub(crate) fn next_node_index(store: &mut dyn Storage) -> StdResult { + // make sure we don't start from 0, otherwise all the crypto breaks (kinda) + let id: NodeIndex = NODE_INDEX_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NODE_INDEX_COUNTER.save(store, &id)?; + Ok(id) +} diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs new file mode 100644 index 0000000000..c921caaeb5 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -0,0 +1,101 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::MINIMUM_DEPOSIT; +use crate::dealers::storage as dealers_storage; +use crate::{ContractError, State, STATE}; +use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof}; +use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response}; + +// currently we only require that +// a) it's part of the signer group +// b) it isn't already a dealer +fn verify_dealer(deps: DepsMut<'_>, state: &State, dealer: &Addr) -> Result<(), ContractError> { + if dealers_storage::current_dealers() + .may_load(deps.storage, dealer)? + .is_some() + { + return Err(ContractError::AlreadyADealer); + } + + state + .group_addr + .is_voting_member(&deps.querier, dealer, None)? + .ok_or(ContractError::Unauthorized {})?; + + Ok(()) +} + +fn validate_dealer_deposit(state: &State, mut deposit: Vec) -> Result { + // check if anything was put as deposit + if deposit.is_empty() { + return Err(ContractError::NoDepositFound { + denom: state.mix_denom.clone(), + }); + } + + if deposit.len() > 1 { + return Err(ContractError::MultipleDenoms); + } + + // check that the denomination is correct + if deposit[0].denom != state.mix_denom { + return Err(ContractError::WrongDenom { + denom: state.mix_denom.clone(), + }); + } + + // check that we have at least MINIMUM_DEPOSIT coins in our deposit + if deposit[0].amount < MINIMUM_DEPOSIT { + return Err(ContractError::InsufficientDeposit { + received: deposit[0].amount.into(), + minimum: MINIMUM_DEPOSIT.into(), + }); + } + + // the unwrap would have been safe here under all circumstances, since we checked whether the vector is empty + // but in case something did change, change option into an error + deposit.pop().ok_or(ContractError::NoDepositFound { + denom: state.mix_denom.clone(), + }) +} + +pub fn try_add_dealer( + mut deps: DepsMut<'_>, + info: MessageInfo, + bte_key_with_proof: EncodedBTEPublicKeyWithProof, +) -> Result { + let state = STATE.load(deps.storage)?; + + verify_dealer(deps.branch(), &state, &info.sender)?; + + // validate and extract sent deposit + let deposit = validate_dealer_deposit(&state, info.funds)?; + + // if it was already a dealer in the past, assign the same node index + let node_index = if let Some(prior_details) = + dealers_storage::past_dealers().may_load(deps.storage, &info.sender)? + { + // since this dealer is going to become active now, remove it from the past dealers + dealers_storage::past_dealers().replace( + deps.storage, + &info.sender, + None, + Some(&prior_details), + )?; + prior_details.assigned_index + } else { + dealers_storage::next_node_index(deps.storage)? + }; + + // save the dealer into the storage + let dealer_details = DealerDetails { + address: info.sender.clone(), + bte_public_key_with_proof: bte_key_with_proof, + assigned_index: node_index, + deposit, + }; + dealers_storage::current_dealers().save(deps.storage, &info.sender, &dealer_details)?; + + Ok(Response::new().add_attribute("node_index", node_index.to_string())) +} diff --git a/contracts/coconut-dkg/src/dealings/mod.rs b/contracts/coconut-dkg/src/dealings/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs new file mode 100644 index 0000000000..7d6abd9d73 --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -0,0 +1,42 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealings::storage; +use crate::dealings::storage::DEALING_COMMITMENTS; +use coconut_dkg_common::dealer::{ContractDealingCommitment, PagedCommitmentsResponse}; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +pub fn query_epoch_dealings_commitments_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(storage::COMMITMENTS_PAGE_DEFAULT_LIMIT) + .min(storage::COMMITMENTS_PAGE_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let commitments = DEALING_COMMITMENTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|(dealer, commitment)| ContractDealingCommitment::new(commitment, dealer)) + }) + .collect::>>()?; + + let start_next_after = commitments + .last() + .map(|commitment| commitment.dealer.clone()); + + Ok(PagedCommitmentsResponse::new( + commitments, + limit, + start_next_after, + )) +} diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs new file mode 100644 index 0000000000..bcad1b95ee --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::types::ContractSafeCommitment; +use cosmwasm_std::Addr; +use cw_storage_plus::Map; + +pub(crate) const COMMITMENTS_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const COMMITMENTS_PAGE_DEFAULT_LIMIT: u32 = 50; + +type CommitmentKey<'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. +pub(crate) const DEALING_COMMITMENTS: Map<'_, CommitmentKey<'_>, ContractSafeCommitment> = + Map::new("dcmt"); diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs new file mode 100644 index 0000000000..7c3160b8b6 --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -0,0 +1,32 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::storage as dealers_storage; +use crate::dealings::storage::DEALING_COMMITMENTS; +use crate::ContractError; +use coconut_dkg_common::types::ContractSafeCommitment; +use cosmwasm_std::{DepsMut, MessageInfo, Response}; + +pub fn try_commit_dealing( + deps: DepsMut<'_>, + info: MessageInfo, + commitment: ContractSafeCommitment, +) -> Result { + // ensure the sender is a dealer for the current epoch + if dealers_storage::current_dealers() + .may_load(deps.storage, &info.sender)? + .is_none() + { + return Err(ContractError::NotADealer); + } + + // check if this dealer has already committed to a dealing + // (we don't want to allow overwriting it as some receivers might already be using the commitment) + if DEALING_COMMITMENTS.has(deps.storage, &info.sender) { + return Err(ContractError::AlreadyCommitted); + } + + DEALING_COMMITMENTS.save(deps.storage, &info.sender, &commitment)?; + + Ok(Response::new()) +} diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs new file mode 100644 index 0000000000..4d187c7a8d --- /dev/null +++ b/contracts/coconut-dkg/src/error.rs @@ -0,0 +1,53 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Addr, StdError, VerificationError}; +use thiserror::Error; + +/// Custom errors for contract failure conditions. +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Group contract invalid address '{addr}'")] + InvalidGroup { addr: String }, + + #[error("No coin was sent for the deposit, you must send {denom}")] + NoDepositFound { denom: String }, + + #[error("Received multiple coin types")] + MultipleDenoms, + + #[error("Wrong coin denomination, you must send {denom}")] + WrongDenom { denom: String }, + + #[error("Not enough funds sent for deposit. (received {received}, minimum {minimum})")] + InsufficientDeposit { received: u128, minimum: u128 }, + + #[error("Failed to perform ed25519 signature verification - {0}. This dealer will be temporarily blacklisted now.")] + Ed25519VerificationError(#[from] VerificationError), + + #[error("Provided ed25519 signature did not verify correctly. This dealer will be temporarily blacklisted now.")] + InvalidEd25519Signature, + + #[error("This potential dealer is not in the coconut signer group")] + Unauthorized, + + #[error("This sender is already a dealer for the epoch")] + AlreadyADealer, + + #[error("Epoch hasn't been correctly initialised!")] + EpochNotInitialised, + + // we should never ever see this error (famous last words in programming), therefore, I'd want to + // explicitly declare it so that when we ultimate do see it, it's gonna be more informative over "normal" panic + #[error("Somehow our validated address {address} is not using correct bech32 encoding")] + InvalidValidatedAddress { address: Addr }, + + #[error("This sender is not a dealer for the current epoch")] + NotADealer, + + #[error("This dealer has already commited dealing for this epoch")] + AlreadyCommitted, +} diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs new file mode 100644 index 0000000000..78be98bdd8 --- /dev/null +++ b/contracts/coconut-dkg/src/lib.rs @@ -0,0 +1,156 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::MINIMUM_DEPOSIT; +use crate::dealers::queries::{ + query_current_dealers_paged, query_dealer_details, query_past_dealers_paged, +}; +use crate::dealings::queries::query_epoch_dealings_commitments_paged; +use crate::error::ContractError; +use crate::state::{State, STATE}; +use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use coconut_dkg_common::types::MinimumDepositResponse; +use cosmwasm_std::{ + entry_point, to_binary, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, +}; +use cw4::Cw4Contract; + +mod constants; +mod dealers; +mod dealings; +mod error; +mod state; + +/// Instantiate the contract. +/// +/// `deps` contains Storage, API and Querier +/// `env` contains block, message and contract info +/// `msg` is the contract initialization message, sort of like a constructor call. +#[entry_point] +pub fn instantiate( + deps: DepsMut<'_>, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { + ContractError::InvalidGroup { + addr: msg.group_addr.clone(), + } + })?); + + let state = State { + group_addr, + mix_denom: msg.mix_denom, + }; + STATE.save(deps.storage, &state)?; + + Ok(Response::default()) +} + +/// Handle an incoming message +#[entry_point] +pub fn execute( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::RegisterDealer { bte_key_with_proof } => { + dealers::transactions::try_add_dealer(deps, info, bte_key_with_proof) + } + ExecuteMsg::CommitDealing { commitment } => { + dealings::transactions::try_commit_dealing(deps, info, commitment) + } + ExecuteMsg::DebugUnsafeResetAll { init_msg } => { + reset_contract_state(deps, env, info, init_msg) + } + } +} + +fn reset_contract_state( + mut deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + init_msg: InstantiateMsg, +) -> Result { + // this resets the epoch + instantiate(deps.branch(), env, info, init_msg)?; + + // clear all dealings, public keys, etc + let current = dealers::storage::current_dealers() + .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::, _>>()?; + let past = dealers::storage::past_dealers() + .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::, _>>()?; + let commitments = crate::dealings::storage::DEALING_COMMITMENTS + .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) + .collect::, _>>()?; + + for dealer in current { + dealers::storage::current_dealers().remove(deps.storage, &dealer)?; + } + + for dealer in past { + dealers::storage::past_dealers().remove(deps.storage, &dealer)?; + } + + for addr in commitments { + dealings::storage::DEALING_COMMITMENTS.remove(deps.storage, &addr); + } + + dealers::storage::NODE_INDEX_COUNTER.save(deps.storage, &0u64)?; + + Ok(Response::default()) +} + +#[entry_point] +pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { + let response = match msg { + QueryMsg::GetDealerDetails { dealer_address } => { + to_binary(&query_dealer_details(deps, dealer_address)?)? + } + QueryMsg::GetCurrentDealers { limit, start_after } => { + to_binary(&query_current_dealers_paged(deps, start_after, limit)?)? + } + QueryMsg::GetPastDealers { limit, start_after } => { + to_binary(&query_past_dealers_paged(deps, start_after, limit)?)? + } + QueryMsg::GetDepositAmount {} => to_binary(&MinimumDepositResponse::new(Coin::new( + MINIMUM_DEPOSIT.u128(), + STATE.load(deps.storage)?.mix_denom, + )))?, + QueryMsg::GetDealingsCommitments { limit, start_after } => to_binary( + &query_epoch_dealings_commitments_paged(deps, start_after, limit)?, + )?, + }; + + Ok(response) +} + +#[entry_point] +pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + + #[test] + fn initialize_contract() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = InstantiateMsg { + group_addr: "group_addr".to_string(), + mix_denom: "nym".to_string(), + }; + let info = mock_info("creator", &[]); + + let res = instantiate(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()) + } +} diff --git a/contracts/coconut-dkg/src/state/mod.rs b/contracts/coconut-dkg/src/state/mod.rs new file mode 100644 index 0000000000..c08f9e49cb --- /dev/null +++ b/contracts/coconut-dkg/src/state/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cw4::Cw4Contract; +use cw_storage_plus::Item; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct State { + pub mix_denom: String, + pub group_addr: Cw4Contract, +} + +// unique items +pub const STATE: Item = Item::new("state"); diff --git a/envs/mainnet.env b/envs/mainnet.env index a8dc60dd44..d53b7f4b8b 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -14,6 +14,7 @@ VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq7 BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" diff --git a/envs/qa.env b/envs/qa.env index 0e791db172..64511fa922 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -14,6 +14,7 @@ VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sj BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs +COCONUT_DKG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" NYMD_VALIDATOR="https://qa-validator.nymtech.net" diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs index 8f0ffcd098..19c8e426b4 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -8,7 +8,7 @@ use log::*; use coconut_interface::{Credential, VerificationKey}; use validator_client::{ nymd::{ - cosmwasm_client::logs::find_attribute, + cosmwasm_client::logs::{find_attribute, PROPOSAL_ID}, traits::{CoconutBandwidthSigningClient, MultisigQueryClient, MultisigSigningClient}, Coin, Fee, NymdClient, SigningNymdClient, }, @@ -69,7 +69,7 @@ impl CoconutVerifier { None, ) .await?; - let proposal_id = find_attribute(&res.logs, "wasm", "proposal_id") + let proposal_id = find_attribute(&res.logs, "wasm", PROPOSAL_ID) .ok_or(RequestHandlingError::ProposalIdError { reason: String::from("proposal id not found"), })? diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 40bcd330e4..918cc02baf 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -683,6 +683,16 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" @@ -759,6 +769,7 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", "schemars", "serde", @@ -6096,6 +6107,7 @@ dependencies = [ "base64", "bip39", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "colored", "config", diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 1bc9e48663..b8e93eb289 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -595,6 +595,16 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" @@ -667,6 +677,7 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", "schemars", "serde", @@ -5327,6 +5338,7 @@ dependencies = [ "base64", "bip39", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "colored 2.0.0", "config", diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 7a0e01c527..3a39bd6733 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -111,6 +111,8 @@ mod sandbox { "nymt1nz0r0au8aj6dc00wmm3ufy4g4k86rjzlgq608r"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "nymt1k8re7jwz6rnnwrktnejdwkwnncte7ek7kk6fvg"; + pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = + "nymt1k8re7jwz6rnnwrktnejdwkwnncte7ek7kk6fvg"; pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("8e0DcFF7F3085235C32E845f3667aEB3f1e83133"); pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = @@ -144,6 +146,7 @@ mod sandbox { COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, } } @@ -168,6 +171,8 @@ mod qa { pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs"; + pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = + "n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs"; pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = @@ -200,6 +205,7 @@ mod qa { COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, } } diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 9b07010edc..4ab65b6e4a 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -16,6 +16,7 @@ rust-version = "1.56" [dependencies] async-trait = "0.1.52" +bs58 = {version = "0.4.0", optional = true } cfg-if = "1.0" clap = { version = "3.2", features = ["cargo"] } console-subscriber = { version = "0.1.1", optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" @@ -63,6 +64,7 @@ schemars = { version = "0.8", features = ["preserve_order"] } ## internal coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg", optional = true } coconut-interface = { path = "../common/coconut-interface", optional = true } config = { path = "../common/config" } cosmwasm-std = "1.0.0" @@ -70,13 +72,15 @@ credential-storage = { path = "../common/credential-storage" } credentials = { path = "../common/credentials", optional = true } crypto = { path = "../common/crypto" } logging = { path = "../common/logging"} +dkg = { path = "../common/crypto/dkg", optional = true } gateway-client = { path = "../common/client-libs/gateway-client" } inclusion-probability = { path = "../common/inclusion-probability" } mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } -contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["committable_trait"] } multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nymcoconut = { path = "../common/nymcoconut", optional = true } nymsphinx = { path = "../common/nymsphinx" } +pemstore = { path = "../common/pemstore", optional = true } task = { path = "../common/task" } topology = { path = "../common/topology" } validator-api-requests = { path = "validator-api-requests" } @@ -93,6 +97,10 @@ coconut = [ "credentials/coconut", "validator-api-requests/coconut", "nymcoconut", + "coconut-dkg-common", + "dkg", + "bs58", + "pemstore", ] no-reward = [] generate-ts = ["ts-rs"] diff --git a/validator-api/src/coconut/client.rs b/validator-api/src/coconut/client.rs index 626a4b775b..7df918c67d 100644 --- a/validator-api/src/coconut/client.rs +++ b/validator-api/src/coconut/client.rs @@ -3,7 +3,11 @@ use crate::coconut::error::Result; use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; +use coconut_dkg_common::dealer::DealerDetailsResponse; +use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +use contracts_common::commitment::ContractSafeCommitment; use multisig_contract_common::msg::ProposalResponse; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; use validator_client::nymd::{AccountId, Fee, TxResponse}; #[async_trait] @@ -15,6 +19,13 @@ pub trait Client { &self, blinded_serial_number: String, ) -> Result; + async fn get_self_registered_dealer_details(&self) -> Result; async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; + async fn register_dealer(&self, bte_key: EncodedBTEPublicKeyWithProof) + -> Result; + async fn submit_dealing_commitment( + &self, + commitment: ContractSafeCommitment, + ) -> Result; } diff --git a/validator-api/src/coconut/dkg/mod.rs b/validator-api/src/coconut/dkg/mod.rs new file mode 100644 index 0000000000..aba57513d5 --- /dev/null +++ b/validator-api/src/coconut/dkg/mod.rs @@ -0,0 +1,71 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::client::Client; +use crate::coconut::dkg::publisher::Publisher; +use crate::Config; +use anyhow::Result; +use dkg::bte::keys::KeyPair; +use rand::rngs::OsRng; +use task::ShutdownListener; + +pub(crate) mod publisher; + +pub(crate) fn init_keypair(config: &Config) -> Result<()> { + let mut rng = OsRng; + let dkg_params = dkg::bte::setup(); + let kp = KeyPair::new(&dkg_params, &mut rng); + pemstore::store_keypair( + &kp, + &pemstore::KeyPairPath::new( + config.decryption_key_path(), + config.public_key_with_proof_path(), + ), + )?; + Ok(()) +} + +pub(crate) struct DkgController { + publisher: Publisher, + keypair: KeyPair, +} + +impl DkgController { + pub(crate) fn new(config: &Config, nymd_client: C) -> Result + where + C: Client + Send + Sync + 'static, + { + let publisher = Publisher::new(nymd_client); + let keypair = pemstore::load_keypair(&pemstore::KeyPairPath::new( + config.decryption_key_path(), + config.public_key_with_proof_path(), + ))?; + Ok(DkgController { publisher, keypair }) + } + + pub(crate) async fn run(&self, mut shutdown: ShutdownListener) { + let bte_key = bs58::encode(&self.keypair.public_key().to_bytes()).into_string(); + let index = if let Some(details) = self + .publisher + .get_self_registered_dealer_details() + .await + .expect("Could not query for dealer details") + .details + { + details.assigned_index + } else { + self.publisher + .register_dealer(bte_key) + .await + .expect("Could not register dealer in dkg protocol") + }; + info!("Starting dkg protocol with index {}", index); + while !shutdown.is_shutdown() { + tokio::select! { + _ = shutdown.recv() => { + trace!("DkgController: Received shutdown"); + } + } + } + } +} diff --git a/validator-api/src/coconut/dkg/publisher.rs b/validator-api/src/coconut/dkg/publisher.rs new file mode 100644 index 0000000000..d3312faa2b --- /dev/null +++ b/validator-api/src/coconut/dkg/publisher.rs @@ -0,0 +1,60 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::client::Client; +use crate::coconut::error::CoconutError; +use coconut_dkg_common::dealer::DealerDetailsResponse; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, NodeIndex}; +use contracts_common::commitment::ContractSafeCommitment; +use validator_client::nymd::cosmwasm_client::logs::{find_attribute, NODE_INDEX}; +use validator_client::nymd::AccountId; + +pub(crate) struct Publisher { + client: Box, +} + +impl Publisher { + pub(crate) fn new(nymd_client: C) -> Self + where + C: Client + Send + Sync + 'static, + { + let client = Box::new(nymd_client); + Publisher { client } + } + + pub(crate) async fn _get_address(&self) -> AccountId { + self.client.address().await + } + + pub(crate) async fn get_self_registered_dealer_details( + &self, + ) -> Result { + self.client.get_self_registered_dealer_details().await + } + + pub(crate) async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + ) -> Result { + let res = self.client.register_dealer(bte_key).await?; + let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) + .ok_or(CoconutError::NodeIndexRecoveryError { + reason: String::from("node index not found"), + })? + .value + .parse::() + .map_err(|_| CoconutError::NodeIndexRecoveryError { + reason: String::from("node index could not be parsed"), + })?; + + Ok(node_index) + } + + pub(crate) async fn _submit_dealing_commitment( + &self, + commitment: ContractSafeCommitment, + ) -> Result<(), CoconutError> { + self.client.submit_dealing_commitment(commitment).await?; + Ok(()) + } +} diff --git a/validator-api/src/coconut/error.rs b/validator-api/src/coconut/error.rs index 7dfdb843c2..1dcad8224d 100644 --- a/validator-api/src/coconut/error.rs +++ b/validator-api/src/coconut/error.rs @@ -77,6 +77,9 @@ pub enum CoconutError { #[error("Invalid status of credential: {status}")] InvalidCredentialStatus { status: String }, + + #[error("Failed to recover assigned node index: {reason}")] + NodeIndexRecoveryError { reason: String }, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index 45de17a25d..6e0552cace 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod client; pub(crate) mod comm; mod deposit; +pub(crate) mod dkg; pub(crate) mod error; #[cfg(test)] mod tests; diff --git a/validator-api/src/coconut/tests.rs b/validator-api/src/coconut/tests.rs index 757eb9aa4a..afbfe2c889 100644 --- a/validator-api/src/coconut/tests.rs +++ b/validator-api/src/coconut/tests.rs @@ -39,6 +39,9 @@ use validator_client::validator_api::routes::{ use crate::coconut::State; use crate::ValidatorApiStorage; use async_trait::async_trait; +use coconut_dkg_common::dealer::DealerDetailsResponse; +use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +use contracts_common::commitment::ContractSafeCommitment; use crypto::asymmetric::{encryption, identity}; use rand_07::rngs::OsRng; use rocket::http::Status; @@ -46,6 +49,7 @@ use rocket::local::asynchronous::Client; use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, RwLock}; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; const TEST_COIN_DENOM: &str = "unym"; const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; @@ -117,6 +121,10 @@ impl super::client::Client for DummyClient { }) } + async fn get_self_registered_dealer_details(&self) -> Result { + todo!() + } + async fn vote_proposal( &self, proposal_id: u64, @@ -132,6 +140,20 @@ impl super::client::Client for DummyClient { } Ok(()) } + + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + ) -> Result { + todo!() + } + + async fn submit_dealing_commitment( + &self, + commitment: ContractSafeCommitment, + ) -> Result { + todo!() + } } #[derive(Clone, Debug)] diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 3676e5ae3f..2eb19d351c 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -262,7 +262,7 @@ impl Default for Rewarding { } } -#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] pub struct CoconutSigner { /// Specifies whether rewarding service is enabled in this process. @@ -271,12 +271,43 @@ pub struct CoconutSigner { /// Path to the signing keypair keypair_path: PathBuf, + /// Path to the dkg dealer decryption key. + decryption_key_path: PathBuf, + + /// Path to the dkg dealer public key with proof. + public_key_with_proof_path: PathBuf, + /// Specifies list of all validators on the network issuing coconut credentials. /// A special care must be taken to ensure they are in correct order. /// The list must also contain THIS validator that is running the test all_validator_apis: Vec, } +impl CoconutSigner { + pub const DKG_DECRYPTION_KEY_FILE: &'static str = "dkg_decryption_key.pem"; + pub const DKG_PUBLIC_KEY_WITH_PROOF_FILE: &'static str = "dkg_public_key_with_proof.pem"; + + fn default_dkg_decryption_key_path() -> PathBuf { + Config::default_data_directory(None).join(Self::DKG_DECRYPTION_KEY_FILE) + } + + fn default_dkg_public_key_with_proof_path() -> PathBuf { + Config::default_data_directory(None).join(Self::DKG_PUBLIC_KEY_WITH_PROOF_FILE) + } +} + +impl Default for CoconutSigner { + fn default() -> Self { + Self { + enabled: Default::default(), + keypair_path: Default::default(), + decryption_key_path: CoconutSigner::default_dkg_decryption_key_path(), + public_key_with_proof_path: CoconutSigner::default_dkg_public_key_with_proof_path(), + all_validator_apis: Default::default(), + } + } +} + impl Config { pub fn new() -> Self { Config::default() @@ -288,14 +319,13 @@ impl Config { Config::default_data_directory(Some(id)).join(NodeStatusAPI::DB_FILE); self.network_monitor.credentials_database_path = Config::default_data_directory(Some(id)).join(NetworkMonitor::DB_FILE); + self.coconut_signer.decryption_key_path = + Config::default_data_directory(Some(id)).join(CoconutSigner::DKG_DECRYPTION_KEY_FILE); + self.coconut_signer.public_key_with_proof_path = Config::default_data_directory(Some(id)) + .join(CoconutSigner::DKG_PUBLIC_KEY_WITH_PROOF_FILE); self } - #[cfg(feature = "coconut")] - pub fn keypair_path(&self) -> PathBuf { - self.coconut_signer.keypair_path.clone() - } - pub fn with_network_monitor_enabled(mut self, enabled: bool) -> Self { self.network_monitor.enabled = enabled; self @@ -450,6 +480,21 @@ impl Config { self.node_status_api.database_path.clone() } + #[cfg(feature = "coconut")] + pub fn keypair_path(&self) -> PathBuf { + self.coconut_signer.keypair_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn decryption_key_path(&self) -> PathBuf { + self.coconut_signer.decryption_key_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn public_key_with_proof_path(&self) -> PathBuf { + self.coconut_signer.public_key_with_proof_path.clone() + } + // fix dead code warnings as this method is only ever used with coconut feature #[cfg(feature = "coconut")] pub fn get_all_validator_api_endpoints(&self) -> Vec { diff --git a/validator-api/src/config/template.rs b/validator-api/src/config/template.rs index 340b93b70a..fdb9ae6258 100644 --- a/validator-api/src/config/template.rs +++ b/validator-api/src/config/template.rs @@ -96,6 +96,12 @@ enabled = {{ coconut_signer.enabled }} # Path to the signing keypair keypair_path = '{{ coconut_signer.keypair_path }}' +# Path to the dkg dealer decryption key +decryption_key_path = '{{ coconut_signer.decryption_key_path }}' + +# Path to the dkg dealer public key with proof +public_key_with_proof_path = '{{ coconut_signer.public_key_with_proof_path }}' + # Specifies list of all validators on the network issuing coconut credentials. # A special care must be taken to ensure they are in correct order. # The list must also contain THIS validator that is running the test diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index ece8fab179..11b8a90a1d 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -39,7 +39,11 @@ use validator_client::nymd::SigningNymdClient; use crate::epoch_operations::RewardedSetUpdater; #[cfg(feature = "coconut")] -use coconut::{comm::QueryCommunicationChannel, InternalSignRequest}; +use coconut::{ + comm::QueryCommunicationChannel, + dkg::{init_keypair, DkgController}, + InternalSignRequest, +}; #[cfg(feature = "coconut")] use coconut_interface::{Base58, KeyPair}; use logging::setup_logging; @@ -506,8 +510,8 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { // try to load config from the file, if it doesn't exist, use default values let id = matches.value_of(ID); - let config = match Config::load_from_file(id) { - Ok(cfg) => cfg, + let (config, _already_inited) = match Config::load_from_file(id) { + Ok(cfg) => (cfg, true), Err(_) => { let config_path = Config::default_config_file_path(id) .into_os_string() @@ -517,11 +521,17 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { "Could not load the configuration file from {}. Either the file did not exist or was malformed. Using the default values instead", config_path ); - Config::new() + (Config::new(), false) } }; let config = override_config(config, &matches); + + #[cfg(feature = "coconut")] + if !_already_inited { + init_keypair(&config)?; + } + // if we just wanted to write data to the config, exit if matches.is_present(WRITE_CONFIG_ARG) { return Ok(()); @@ -547,6 +557,13 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { let validator_cache = rocket.state::().unwrap().clone(); let node_status_cache = rocket.state::().unwrap().clone(); + #[cfg(feature = "coconut")] + { + let dkg_controller = DkgController::new(&config, signing_nymd_client.clone())?; + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(async move { dkg_controller.run(shutdown_listener).await }); + } + // if network monitor is disabled, we're not going to be sending any rewarding hence // we're not starting signing client let validator_cache_listener = if config.get_network_monitor_enabled() { diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 33c85634bf..9f38582339 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -26,10 +26,20 @@ use async_trait::async_trait; #[cfg(feature = "coconut")] use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; #[cfg(feature = "coconut")] +use coconut_dkg_common::dealer::DealerDetailsResponse; +#[cfg(feature = "coconut")] +use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +#[cfg(feature = "coconut")] +use contracts_common::commitment::ContractSafeCommitment; +#[cfg(feature = "coconut")] use multisig_contract_common::msg::ProposalResponse; #[cfg(feature = "coconut")] use validator_client::nymd::{ - traits::{CoconutBandwidthQueryClient, MultisigQueryClient, MultisigSigningClient}, + cosmwasm_client::types::ExecuteResult, + traits::{ + CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, MultisigQueryClient, + MultisigSigningClient, + }, AccountId, Fee, }; @@ -294,6 +304,19 @@ where .await?) } + async fn get_self_registered_dealer_details( + &self, + ) -> crate::coconut::error::Result { + let self_address = &self.address().await; + Ok(self + .0 + .read() + .await + .nymd + .get_dealer_details(self_address) + .await?) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -308,4 +331,30 @@ where .await?; Ok(()) } + + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + ) -> Result { + Ok(self + .0 + .write() + .await + .nymd + .register_dealer(bte_key, None) + .await?) + } + + async fn submit_dealing_commitment( + &self, + commitment: ContractSafeCommitment, + ) -> Result { + Ok(self + .0 + .write() + .await + .nymd + .submit_dealing_commitment(commitment, None) + .await?) + } } From b71a8708db974dfc67ac41185e27ffe112b32085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 21 Nov 2022 18:00:47 +0200 Subject: [PATCH 29/75] Feature/dkg dealing (#1708) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Reintroduce epoch states Co-authored-by: Jędrzej Stuczyński * Use admin address for sensible txs * Validator-api watch contract and handle events * Handle dealing exchange * Dealing exchange * Recover raw verification keys for 5 dkgs * Test coconut with dkg keys * Split dealing storage * Finish dkg task when it achieved its purpose * Temporary fix for clippy * Fix clippy Co-authored-by: Jędrzej Stuczyński --- Cargo.lock | 13 +- .../validator-client/src/client.rs | 15 +- .../src/nymd/traits/dkg_query_client.rs | 24 ++- .../src/nymd/traits/dkg_signing_client.rs | 12 +- .../coconut-dkg/src/dealer.rs | 24 +-- .../coconut-dkg/src/msg.rs | 11 +- .../coconut-dkg/src/types.rs | 35 ++-- .../contracts-common/Cargo.toml | 6 +- .../contracts-common/src/commitment.rs | 186 ----------------- .../contracts-common/src/dealings.rs | 87 ++++++++ .../contracts-common/src/lib.rs | 2 +- common/nymcoconut/Cargo.toml | 8 +- common/nymcoconut/src/lib.rs | 1 + common/nymcoconut/src/scheme/keygen.rs | 14 ++ common/nymcoconut/src/tests/e2e.rs | 57 +++++- common/nymcoconut/src/tests/helpers.rs | 123 ++++++++++-- common/nymcoconut/src/utils.rs | 2 +- .../coconut-dkg/src/dealers/transactions.rs | 4 +- contracts/coconut-dkg/src/dealings/queries.rs | 33 +-- contracts/coconut-dkg/src/dealings/storage.rs | 20 +- .../coconut-dkg/src/dealings/transactions.rs | 25 ++- contracts/coconut-dkg/src/epoch_state/mod.rs | 6 + .../coconut-dkg/src/epoch_state/queries.rs | 13 ++ .../coconut-dkg/src/epoch_state/storage.rs | 29 +++ .../coconut-dkg/src/epoch_state/utils.rs | 21 ++ contracts/coconut-dkg/src/error.rs | 12 ++ contracts/coconut-dkg/src/lib.rs | 40 ++-- contracts/coconut-dkg/src/state/mod.rs | 8 +- validator-api/Cargo.toml | 2 +- validator-api/src/coconut/client.rs | 14 +- .../coconut/dkg/{publisher.rs => client.rs} | 44 ++-- validator-api/src/coconut/dkg/complaints.rs | 46 +++++ validator-api/src/coconut/dkg/controller.rs | 109 ++++++++++ validator-api/src/coconut/dkg/dealing.rs | 53 +++++ validator-api/src/coconut/dkg/mod.rs | 75 +------ validator-api/src/coconut/dkg/public_key.rs | 30 +++ validator-api/src/coconut/dkg/state.rs | 190 ++++++++++++++++++ .../src/coconut/dkg/verification_key.rs | 141 +++++++++++++ validator-api/src/coconut/error.rs | 13 ++ validator-api/src/coconut/keypair.rs | 27 +++ validator-api/src/coconut/mod.rs | 23 ++- validator-api/src/coconut/tests.rs | 59 ++++-- validator-api/src/config/mod.rs | 13 +- validator-api/src/main.rs | 21 +- validator-api/src/nymd_client.rs | 27 ++- 45 files changed, 1256 insertions(+), 462 deletions(-) delete mode 100644 common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs create mode 100644 common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs create mode 100644 contracts/coconut-dkg/src/epoch_state/mod.rs create mode 100644 contracts/coconut-dkg/src/epoch_state/queries.rs create mode 100644 contracts/coconut-dkg/src/epoch_state/storage.rs create mode 100644 contracts/coconut-dkg/src/epoch_state/utils.rs rename validator-api/src/coconut/dkg/{publisher.rs => client.rs} (52%) create mode 100644 validator-api/src/coconut/dkg/complaints.rs create mode 100644 validator-api/src/coconut/dkg/controller.rs create mode 100644 validator-api/src/coconut/dkg/dealing.rs create mode 100644 validator-api/src/coconut/dkg/public_key.rs create mode 100644 validator-api/src/coconut/dkg/state.rs create mode 100644 validator-api/src/coconut/dkg/verification_key.rs create mode 100644 validator-api/src/coconut/keypair.rs diff --git a/Cargo.lock b/Cargo.lock index e079c021e2..5c3f743cb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -755,11 +755,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "contracts-common" version = "0.1.0" dependencies = [ - "blake3", "bs58", "cosmwasm-std", - "digest 0.10.3", - "generic-array 0.14.5", + "dkg", "schemars", "serde", "serde_json", @@ -1986,7 +1984,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ - "serde", "typenum", "version_check", ] @@ -3476,16 +3473,18 @@ name = "nymcoconut" version = "0.5.0" dependencies = [ "bincode", - "bls12_381 0.5.0", + "bls12_381 0.6.0", "bs58", "criterion", "digest 0.9.0", + "dkg", "doc-comment", - "ff 0.10.1", + "ff 0.11.0", "getrandom 0.2.6", - "group 0.10.0", + "group 0.11.0", "itertools", "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_derive", "sha2 0.9.9", diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index e9e5fafc79..59e9841d78 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{validator_api, ValidatorClientError}; -use coconut_dkg_common::dealer::ContractDealingCommitment; +use coconut_dkg_common::dealer::ContractDealing; use coconut_dkg_common::types::DealerDetails; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::MixId; @@ -577,20 +577,21 @@ impl Client { Ok(dealers) } - pub async fn get_all_nymd_epoch_dealings_commitments( + pub async fn get_all_nymd_epoch_dealings( &self, - ) -> Result, ValidatorClientError> + idx: usize, + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync + Send, { - let mut commitments = Vec::new(); + let mut dealings = Vec::new(); let mut start_after = None; loop { let mut paged_response = self .nymd - .get_dealings_commitments_paged(start_after.take(), self.dealers_page_limit) + .get_dealings_paged(idx, start_after.take(), self.dealers_page_limit) .await?; - commitments.append(&mut paged_response.commitments); + dealings.append(&mut paged_response.dealings); if let Some(start_after_res) = paged_response.start_next_after { start_after = Some(start_after_res.into_string()) @@ -599,7 +600,7 @@ impl Client { } } - Ok(commitments) + Ok(dealings) } } diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs index 67efdec5cb..930346fe17 100644 --- a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs @@ -5,14 +5,15 @@ use crate::nymd::error::NymdError; use crate::nymd::{CosmWasmClient, NymdClient}; use async_trait::async_trait; use coconut_dkg_common::dealer::{ - DealerDetailsResponse, PagedCommitmentsResponse, PagedDealerResponse, + DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse, }; use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; -use coconut_dkg_common::types::MinimumDepositResponse; +use coconut_dkg_common::types::{EpochState, MinimumDepositResponse}; use cosmrs::AccountId; #[async_trait] pub trait DkgQueryClient { + async fn get_current_epoch_state(&self) -> Result; async fn get_dealer_details( &self, address: &AccountId, @@ -29,11 +30,12 @@ pub trait DkgQueryClient { ) -> Result; async fn get_deposit_amount(&self) -> Result; - async fn get_dealings_commitments_paged( + async fn get_dealings_paged( &self, + idx: usize, start_after: Option, page_limit: Option, - ) -> Result; + ) -> Result; } #[async_trait] @@ -41,6 +43,12 @@ impl DkgQueryClient for NymdClient where C: CosmWasmClient + Send + Sync, { + async fn get_current_epoch_state(&self) -> Result { + let request = DkgQueryMsg::GetCurrentEpochState {}; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } async fn get_dealer_details( &self, address: &AccountId, @@ -88,12 +96,14 @@ where .await } - async fn get_dealings_commitments_paged( + async fn get_dealings_paged( &self, + idx: usize, start_after: Option, page_limit: Option, - ) -> Result { - let request = DkgQueryMsg::GetDealingsCommitments { + ) -> Result { + let request = DkgQueryMsg::GetDealing { + idx: idx as u64, limit: page_limit, start_after, }; diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs index 9c9346d901..f93cbac41c 100644 --- a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs @@ -8,7 +8,7 @@ use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; use async_trait::async_trait; use coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg; use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; -use contracts_common::commitment::ContractSafeCommitment; +use contracts_common::dealings::ContractSafeBytes; #[async_trait] pub trait DkgSigningClient { @@ -18,9 +18,9 @@ pub trait DkgSigningClient { fee: Option, ) -> Result; - async fn submit_dealing_commitment( + async fn submit_dealing_bytes( &self, - commitment: ContractSafeCommitment, + commitment: ContractSafeBytes, fee: Option, ) -> Result; } @@ -52,12 +52,12 @@ where .await } - async fn submit_dealing_commitment( + async fn submit_dealing_bytes( &self, - commitment: ContractSafeCommitment, + dealing_bytes: ContractSafeBytes, fee: Option, ) -> Result { - let req = DkgExecuteMsg::CommitDealing { commitment }; + let req = DkgExecuteMsg::CommitDealing { dealing_bytes }; self.client .execute( diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs index 31ecce7269..46f48de623 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::types::{ContractSafeCommitment, EncodedBTEPublicKeyWithProof, NodeIndex}; +use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex}; use cosmwasm_std::{Addr, Coin}; use serde::{Deserialize, Serialize}; @@ -68,33 +68,33 @@ impl PagedDealerResponse { #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -pub struct ContractDealingCommitment { - pub commitment: ContractSafeCommitment, +pub struct ContractDealing { + pub dealing: ContractSafeBytes, pub dealer: Addr, } -impl ContractDealingCommitment { - pub fn new(commitment: ContractSafeCommitment, dealer: Addr) -> Self { - ContractDealingCommitment { commitment, dealer } +impl ContractDealing { + pub fn new(dealing: ContractSafeBytes, dealer: Addr) -> Self { + ContractDealing { dealing, dealer } } } #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] -pub struct PagedCommitmentsResponse { - pub commitments: Vec, +pub struct PagedDealingsResponse { + pub dealings: Vec, pub per_page: usize, pub start_next_after: Option, } -impl PagedCommitmentsResponse { +impl PagedDealingsResponse { pub fn new( - commitments: Vec, + dealings: Vec, per_page: usize, start_next_after: Option, ) -> Self { - PagedCommitmentsResponse { - commitments, + PagedDealingsResponse { + dealings, per_page, start_next_after, } diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 1e6741a66c..6ed6f4618a 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::types::{ContractSafeCommitment, EncodedBTEPublicKeyWithProof}; +use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize}; pub struct InstantiateMsg { pub group_addr: String, pub mix_denom: String, + pub admin: String, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] @@ -19,9 +20,11 @@ pub enum ExecuteMsg { }, CommitDealing { - commitment: ContractSafeCommitment, + dealing_bytes: ContractSafeBytes, }, + AdvanceEpochState {}, + // DEBUG ONLY TXs. THEY SHALL BE REMOVED BEFORE FINALISING THE CODE // only exists for debugging purposes on local network to reset the entire state of the contract DebugUnsafeResetAll { @@ -32,6 +35,7 @@ pub enum ExecuteMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { + GetCurrentEpochState {}, GetDealerDetails { dealer_address: String, }, @@ -44,7 +48,8 @@ pub enum QueryMsg { start_after: Option, }, GetDepositAmount {}, - GetDealingsCommitments { + GetDealing { + idx: u64, limit: Option, start_after: Option, }, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index c5b26483b4..6c64d41aa3 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -5,13 +5,16 @@ use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; pub use crate::dealer::{DealerDetails, PagedDealerResponse}; -pub use contracts_common::commitment::ContractSafeCommitment; +pub use contracts_common::dealings::ContractSafeBytes; pub use cosmwasm_std::{Addr, Coin}; pub type EncodedBTEPublicKeyWithProof = String; pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str; pub type NodeIndex = u64; +// 2 public attributes, 2 private attributes, 1 fixed for coconut credential +pub const TOTAL_DEALINGS: usize = 2 + 2 + 1; + // currently (it is still extremely likely to change, we might be able to get rid of verification key-related complaints), // the epoch can be in the following states (in order): // 1. PublicKeySubmission -> potential dealers are submitting their BTE and ed25519 public keys to participate in dealing exchange @@ -29,28 +32,22 @@ pub type NodeIndex = u64; pub enum EpochState { PublicKeySubmission, DealingExchange, - ComplaintSubmission, - ComplaintVoting, VerificationKeySubmission, - VerificationKeyMismatchSubmission, - VerificationKeyMismatchVoting, InProgress, } +impl Default for EpochState { + fn default() -> Self { + Self::PublicKeySubmission + } +} + impl Display for EpochState { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { EpochState::PublicKeySubmission => write!(f, "PublicKeySubmission"), EpochState::DealingExchange => write!(f, "DealingExchange"), - EpochState::ComplaintSubmission => write!(f, "ComplaintSubmission"), - EpochState::ComplaintVoting => write!(f, "ComplaintVoting"), EpochState::VerificationKeySubmission => write!(f, "VerificationKeySubmission"), - EpochState::VerificationKeyMismatchSubmission => { - write!(f, "VerificationKeyMismatchSubmission") - } - EpochState::VerificationKeyMismatchVoting => { - write!(f, "VerificationKeyMismatchVoting") - } EpochState::InProgress => write!(f, "InProgress"), } } @@ -60,16 +57,8 @@ impl EpochState { pub fn next(self) -> Option { match self { EpochState::PublicKeySubmission => Some(EpochState::DealingExchange), - EpochState::DealingExchange => Some(EpochState::ComplaintSubmission), - EpochState::ComplaintSubmission => Some(EpochState::ComplaintVoting), - EpochState::ComplaintVoting => Some(EpochState::VerificationKeySubmission), - EpochState::VerificationKeySubmission => { - Some(EpochState::VerificationKeyMismatchSubmission) - } - EpochState::VerificationKeyMismatchSubmission => { - Some(EpochState::VerificationKeyMismatchVoting) - } - EpochState::VerificationKeyMismatchVoting => Some(EpochState::InProgress), + EpochState::DealingExchange => Some(EpochState::VerificationKeySubmission), + EpochState::VerificationKeySubmission => Some(EpochState::InProgress), EpochState::InProgress => None, } } diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 9c34b5da8f..351830cfe3 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -7,11 +7,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -blake3 = { version = "1.3.1", features = ["traits-preview"], optional = true } bs58 = "0.4.0" cosmwasm-std = "1.0.0" -digest = { version = "0.10.3", optional = true } -generic-array = { version = "0.14.5", features = ["serde"], optional = true } +dkg = { path = "../../../common/crypto/dkg", optional = true } schemars = "0.8" serde = { version = "1.0", features = ["derive"] } thiserror = "1" @@ -20,4 +18,4 @@ thiserror = "1" serde_json = "1.0.0" [features] -committable_trait = ["blake3", "digest", "generic-array"] +coconut = ["dkg"] \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs b/common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs deleted file mode 100644 index e104090257..0000000000 --- a/common/cosmwasm-smart-contracts/contracts-common/src/commitment.rs +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(feature = "committable_trait")] -pub use digest::{Digest, Output}; -use schemars::JsonSchema; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::{Display, Formatter}; -#[cfg(feature = "committable_trait")] -use std::marker::PhantomData; -use std::ops::Deref; - -// some sane upper-bound size on commitment sizes -// currently set to 1024bits -pub const MAX_COMMITMENT_SIZE: usize = 128; - -// TODO: if we are to use commitments for different types, it might make sense to introduce something like -// CommitmentTypeId field on the below for distinguishing different ones. it would somehow become part of the trait -#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)] -pub struct ContractSafeCommitment(Vec); - -impl Deref for ContractSafeCommitment { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Display for ContractSafeCommitment { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if !self.0.is_empty() { - write!(f, "0x")?; - } - for byte in self.0.iter().take(MAX_COMMITMENT_SIZE) { - write!(f, "{:02X}", byte)?; - } - // just some sanity safeguards - if self.0.len() > MAX_COMMITMENT_SIZE { - write!(f, "...")?; - } - Ok(()) - } -} - -// since cosmwasm stores everything with byte representation of stringified json, it's actually more efficient -// to serialize this as a string as opposed to keeping it as vector of bytes. -// for example vec![255,255] would have string representation of "[255,255]" and will be serialized to -// [91, 50, 53, 53, 44, 50, 53, 53, 93]. the equivalent base58 encoded string `"LUv"` will be serialized to -// [34, 76, 85, 118, 34] -// -// the difference between base58 and base64 is rather minimal and I've gone with base58 for consistency sake -impl Serialize for ContractSafeCommitment { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&bs58::encode(&self.0).into_string()) - } -} - -impl<'de> Deserialize<'de> for ContractSafeCommitment { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = ::deserialize(deserializer)?; - let bytes = bs58::decode(&s) - .into_vec() - .map_err(serde::de::Error::custom)?; - Ok(ContractSafeCommitment(bytes)) - } -} - -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct UnsupportedCommitmentSize; - -#[cfg(feature = "committable_trait")] -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub struct InconsistentCommitmentSize; - -#[cfg(feature = "committable_trait")] -pub type DefaultHasher = blake3::Hasher; - -#[cfg(feature = "committable_trait")] -pub trait Committable { - type DigestAlgorithm: Digest; - - fn commitment_size() -> usize { - ::output_size() - } - - fn to_bytes(&self) -> Vec; - - fn produce_commitment(&self) -> MessageCommitment { - MessageCommitment { - commitment: Self::DigestAlgorithm::digest(self.to_bytes()), - _message_type: Default::default(), - } - } - - fn verify_commitment(&self, commitment: &MessageCommitment) -> bool { - let recomputed = self.produce_commitment(); - recomputed.commitment == commitment.commitment - } -} - -#[cfg(feature = "committable_trait")] -#[derive(Debug, Serialize, Deserialize)] -pub struct MessageCommitment -where - T: ?Sized + Committable, -{ - commitment: Output, - - #[serde(skip)] - _message_type: PhantomData, -} - -#[cfg(feature = "committable_trait")] -impl Clone for MessageCommitment -where - T: ?Sized + Committable, -{ - fn clone(&self) -> Self { - MessageCommitment { - commitment: self.commitment.clone(), - _message_type: Default::default(), - } - } -} - -#[cfg(feature = "committable_trait")] -impl MessageCommitment -where - T: ?Sized + Committable, -{ - pub fn value(&self) -> &[u8] { - self.commitment.as_ref() - } - - pub fn unchecked_set_value(value: &[u8]) -> Self { - MessageCommitment { - commitment: Output::::clone_from_slice(value), - _message_type: Default::default(), - } - } - - pub fn new(message: &T) -> MessageCommitment { - message.produce_commitment() - } - - pub fn contract_safe_commitment(&self) -> ContractSafeCommitment { - self.into() - } - - pub fn is_same_as(&self, other: &ContractSafeCommitment) -> bool { - self.commitment.as_slice() == other.0 - } -} - -#[cfg(feature = "committable_trait")] -impl<'a, T> From<&'a MessageCommitment> for ContractSafeCommitment -where - T: ?Sized + Committable, -{ - fn from(commitment: &'a MessageCommitment) -> Self { - ContractSafeCommitment(commitment.value().to_vec()) - } -} - -#[cfg(feature = "committable_trait")] -impl<'a, T> TryFrom<&'a ContractSafeCommitment> for MessageCommitment -where - T: ?Sized + Committable, -{ - type Error = InconsistentCommitmentSize; - - fn try_from(value: &'a ContractSafeCommitment) -> Result { - if value.len() != ::output_size() { - Err(InconsistentCommitmentSize) - } else { - Ok(MessageCommitment::unchecked_set_value(value)) - } - } -} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs new file mode 100644 index 0000000000..8beedd4a42 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs @@ -0,0 +1,87 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "coconut")] +use dkg::{error::DkgError, Dealing}; +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; + +// some sane upper-bound size on byte sizes +// currently set to 128 bytes +pub const MAX_DISPLAY_SIZE: usize = 128; + +// TODO: if we are to use this for different types, it might make sense to introduce something like +// CommitmentTypeId field on the below for distinguishing different ones. it would somehow become part of the trait +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)] +pub struct ContractSafeBytes(Vec); + +impl Deref for ContractSafeBytes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for ContractSafeBytes { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !self.0.is_empty() { + write!(f, "0x")?; + } + for byte in self.0.iter().take(MAX_DISPLAY_SIZE) { + write!(f, "{:02X}", byte)?; + } + // just some sanity safeguards + if self.0.len() > MAX_DISPLAY_SIZE { + write!(f, "...")?; + } + Ok(()) + } +} + +// since cosmwasm stores everything with byte representation of stringified json, it's actually more efficient +// to serialize this as a string as opposed to keeping it as vector of bytes. +// for example vec![255,255] would have string representation of "[255,255]" and will be serialized to +// [91, 50, 53, 53, 44, 50, 53, 53, 93]. the equivalent base58 encoded string `"LUv"` will be serialized to +// [34, 76, 85, 118, 34] +// +// the difference between base58 and base64 is rather minimal and I've gone with base58 for consistency sake +impl Serialize for ContractSafeBytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&bs58::encode(&self.0).into_string()) + } +} + +impl<'de> Deserialize<'de> for ContractSafeBytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + let bytes = bs58::decode(&s) + .into_vec() + .map_err(serde::de::Error::custom)?; + Ok(ContractSafeBytes(bytes)) + } +} + +#[cfg(feature = "coconut")] +impl<'a> From<&'a Dealing> for ContractSafeBytes { + fn from(dealing: &'a Dealing) -> Self { + ContractSafeBytes(dealing.to_bytes()) + } +} + +#[cfg(feature = "coconut")] +impl<'a> TryFrom<&'a ContractSafeBytes> for Dealing { + type Error = DkgError; + + fn try_from(value: &'a ContractSafeBytes) -> Result { + Dealing::try_from_bytes(&value.0) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index 31fd5a4a19..4c351a0ccf 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -4,7 +4,7 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] -pub mod commitment; +pub mod dealings; pub mod events; pub mod types; diff --git a/common/nymcoconut/Cargo.toml b/common/nymcoconut/Cargo.toml index 60b916e58f..c17aa280d3 100644 --- a/common/nymcoconut/Cargo.toml +++ b/common/nymcoconut/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] } +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["pairings", "alloc", "experimental"] } itertools = "0.10" digest = "0.9" rand = "0.8" @@ -18,16 +18,18 @@ bs58 = "0.4.0" sha2 = "0.9" [dependencies.ff] -version = "0.10" +version = "0.11" default-features = false [dependencies.group] -version = "0.10" +version = "0.11" default-features = false [dev-dependencies] criterion = { version="0.3", features=["html_reports"] } doc-comment = "0.3" +dkg = { path = "../crypto/dkg" } +rand_chacha = "0.3" [dev-dependencies.bincode] version = "1" diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index ab1cab7cf7..b48bbc2837 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -17,6 +17,7 @@ pub use scheme::issuance::prepare_blind_sign; pub use scheme::issuance::BlindSignRequest; pub use scheme::keygen::ttp_keygen; pub use scheme::keygen::KeyPair; +pub use scheme::keygen::SecretKey; pub use scheme::keygen::VerificationKey; pub use scheme::setup::setup; pub use scheme::setup::Parameters; diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index bd7dd6cf85..75fb203dc2 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -71,6 +71,12 @@ impl TryFrom<&[u8]> for SecretKey { } impl SecretKey { + /// Following a (distributed) key generation process, scalar values can be obtained + /// outside of the normal key generation process. + pub fn create_from_raw(x: Scalar, ys: Vec) -> Self { + Self { x, ys } + } + /// Derive verification key using this secret key. pub fn verification_key(&self, params: &Parameters) -> VerificationKey { let g1 = params.gen1(); @@ -363,6 +369,14 @@ pub struct KeyPair { impl KeyPair { const MARKER_BYTES: &'static [u8] = b"coconutkeypair"; + pub fn from_keys(secret_key: SecretKey, verification_key: VerificationKey) -> Self { + Self { + secret_key, + verification_key, + index: None, + } + } + pub fn secret_key(&self) -> SecretKey { self.secret_key.clone() } diff --git a/common/nymcoconut/src/tests/e2e.rs b/common/nymcoconut/src/tests/e2e.rs index ce4fe2ab57..235b2adbab 100644 --- a/common/nymcoconut/src/tests/e2e.rs +++ b/common/nymcoconut/src/tests/e2e.rs @@ -1,11 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::tests::helpers::tests::generate_dkg_keys; use crate::{ - aggregate_verification_keys, setup, tests::helpers::theta_from_keys_and_attributes, ttp_keygen, - verify_credential, CoconutError, VerificationKey, + aggregate_verification_keys, setup, tests::helpers::*, ttp_keygen, verify_credential, + CoconutError, VerificationKey, }; #[test] -fn main() -> Result<(), CoconutError> { +fn keygen() -> Result<(), CoconutError> { let params = setup(5)?; + let node_indices = vec![15u64, 248, 33521]; let public_attributes = params.n_random_scalars(2); @@ -18,10 +23,52 @@ fn main() -> Result<(), CoconutError> { .collect(); // aggregate verification keys - let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?; + let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?; // Generate cryptographic material to verify them - let theta = theta_from_keys_and_attributes(¶ms, &coconut_keypairs, &public_attributes)?; + let theta = theta_from_keys_and_attributes( + ¶ms, + &coconut_keypairs, + &node_indices, + &public_attributes, + )?; + + // Verify credentials + assert!(verify_credential( + ¶ms, + &verification_key, + &theta, + &public_attributes, + )); + + Ok(()) +} + +#[test] +fn dkg() -> Result<(), CoconutError> { + let params = setup(5)?; + let node_indices = vec![15u64, 248, 33521]; + + let public_attributes = params.n_random_scalars(2); + + // generate_keys + let coconut_keypairs = generate_dkg_keys(5, &node_indices); + + let verification_keys: Vec = coconut_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + // aggregate verification keys + let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?; + + // Generate cryptographic material to verify them + let theta = theta_from_keys_and_attributes( + ¶ms, + &coconut_keypairs, + &node_indices, + &public_attributes, + )?; // Verify credentials assert!(verify_credential( diff --git a/common/nymcoconut/src/tests/helpers.rs b/common/nymcoconut/src/tests/helpers.rs index 7d9c7098b9..a9be6b885c 100644 --- a/common/nymcoconut/src/tests/helpers.rs +++ b/common/nymcoconut/src/tests/helpers.rs @@ -7,6 +7,7 @@ use itertools::izip; pub fn theta_from_keys_and_attributes( params: &Parameters, coconut_keypairs: &Vec, + indices: &[scheme::SignerIndex], public_attributes: &Vec, ) -> Result { let serial_number = params.random_scalar(); @@ -23,12 +24,7 @@ pub fn theta_from_keys_and_attributes( .collect(); // aggregate verification keys - let indices: Vec = coconut_keypairs - .iter() - .enumerate() - .map(|(idx, _)| (idx + 1) as u64) - .collect(); - let verification_key = aggregate_verification_keys(&verification_keys, Some(&indices))?; + let verification_key = aggregate_verification_keys(&verification_keys, Some(indices))?; // generate blinded signatures let mut blinded_signatures = Vec::new(); @@ -44,26 +40,31 @@ pub fn theta_from_keys_and_attributes( } // Unblind - let unblinded_signatures: Vec = - izip!(blinded_signatures.iter(), verification_keys.iter()) - .map(|(s, vk)| { - s.unblind( - params, - vk, - &private_attributes, - public_attributes, - &blind_sign_request.get_commitment_hash(), - &commitments_openings, - ) - .unwrap() - }) - .collect(); + let unblinded_signatures: Vec<(scheme::SignerIndex, Signature)> = izip!( + indices.iter(), + blinded_signatures.iter(), + verification_keys.iter() + ) + .map(|(idx, s, vk)| { + ( + *idx, + s.unblind( + params, + vk, + &private_attributes, + public_attributes, + &blind_sign_request.get_commitment_hash(), + &commitments_openings, + ) + .unwrap(), + ) + }) + .collect(); // Aggregate signatures let signature_shares: Vec = unblinded_signatures .iter() - .enumerate() - .map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64)) + .map(|(idx, signature)| SignatureShare::new(*signature, *idx)) .collect(); let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); @@ -85,3 +86,81 @@ pub fn theta_from_keys_and_attributes( Ok(theta) } + +#[cfg(test)] +pub mod tests { + use crate::{KeyPair, Parameters, SecretKey}; + use bls12_381::Scalar; + use dkg::{bte::decrypt_share, combine_shares, Dealing, NodeIndex}; + use rand_chacha::rand_core::SeedableRng; + use std::fmt::Debug; + + pub fn generate_dkg_secrets(node_indices: &[NodeIndex]) -> Vec { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = dkg::bte::setup(); + + // the simplest possible case + let threshold = 2; + + let mut receivers = std::collections::BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in node_indices { + let (dk, pk) = dkg::bte::keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + let dealings = node_indices + .iter() + .map(|&dealer_index| { + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 + }) + .collect::>(); + let mut derived_secrets = Vec::new(); + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + let shares = dealings + .iter() + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .collect(); + + let recovered_secret = + combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(); + + derived_secrets.push(recovered_secret) + } + derived_secrets + } + pub fn generate_dkg_keys(num_attributes: u32, node_indices: &[NodeIndex]) -> Vec { + let params = Parameters::new(num_attributes).unwrap(); + let mut all_secrets = vec![]; + for _ in 0..num_attributes { + let secrets = generate_dkg_secrets(node_indices); + all_secrets.push(secrets); + } + let signers = transpose_matrix(all_secrets); + signers + .into_iter() + .map(|mut secrets| { + let x = secrets.pop().unwrap(); + let sk = SecretKey::create_from_raw(x, secrets); + let vk = sk.verification_key(¶ms); + KeyPair::from_keys(sk, vk) + }) + .collect() + } + + pub(crate) fn transpose_matrix(matrix: Vec>) -> Vec> { + let len = matrix[0].len(); + let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect(); + (0..len) + .map(|_| { + iters + .iter_mut() + .map(|it| it.next().unwrap()) + .collect::>() + }) + .collect::>() + .try_into() + .unwrap() + } +} diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs index 764cfe090c..9fec3b334c 100644 --- a/common/nymcoconut/src/utils.rs +++ b/common/nymcoconut/src/utils.rs @@ -32,7 +32,7 @@ impl Polynomial { Scalar::zero() // if x is zero then we can ignore most of the expensive computation and // just return the last term of the polynomial - } else if x.is_zero() { + } else if x.is_zero().into() { // we checked that coefficients are not empty so unwrap here is fine *self.coefficients.first().unwrap() } else { diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index c921caaeb5..c7a0411f8c 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -3,8 +3,9 @@ use crate::constants::MINIMUM_DEPOSIT; use crate::dealers::storage as dealers_storage; +use crate::epoch_state::utils::check_epoch_state; use crate::{ContractError, State, STATE}; -use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof}; +use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState}; use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response}; // currently we only require that @@ -65,6 +66,7 @@ pub fn try_add_dealer( info: MessageInfo, bte_key_with_proof: EncodedBTEPublicKeyWithProof, ) -> Result { + check_epoch_state(deps.storage, EpochState::PublicKeySubmission)?; let state = STATE.load(deps.storage)?; verify_dealer(deps.branch(), &state, &info.sender)?; diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs index 7d6abd9d73..cf2321837c 100644 --- a/contracts/coconut-dkg/src/dealings/queries.rs +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -2,19 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use crate::dealings::storage; -use crate::dealings::storage::DEALING_COMMITMENTS; -use coconut_dkg_common::dealer::{ContractDealingCommitment, PagedCommitmentsResponse}; +use crate::dealings::storage::DEALINGS_BYTES; +use coconut_dkg_common::dealer::{ContractDealing, PagedDealingsResponse}; +use coconut_dkg_common::types::TOTAL_DEALINGS; use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; -pub fn query_epoch_dealings_commitments_paged( +pub fn query_dealings_paged( deps: Deps<'_>, + idx: u64, start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit - .unwrap_or(storage::COMMITMENTS_PAGE_DEFAULT_LIMIT) - .min(storage::COMMITMENTS_PAGE_MAX_LIMIT) as usize; + .unwrap_or(storage::DEALINGS_PAGE_DEFAULT_LIMIT) + .min(storage::DEALINGS_PAGE_MAX_LIMIT) as usize; + + let idx = idx as usize; + if idx >= TOTAL_DEALINGS { + return Ok(PagedDealingsResponse::new(vec![], limit, None)); + } let addr = start_after .map(|addr| deps.api.addr_validate(&addr)) @@ -22,20 +29,16 @@ pub fn query_epoch_dealings_commitments_paged( let start = addr.as_ref().map(Bound::exclusive); - let commitments = DEALING_COMMITMENTS + let dealings = DEALINGS_BYTES[idx] .range(deps.storage, start, None, Order::Ascending) .take(limit) - .map(|res| { - res.map(|(dealer, commitment)| ContractDealingCommitment::new(commitment, dealer)) - }) + .map(|res| res.map(|(dealer, dealing)| ContractDealing::new(dealing, dealer))) .collect::>>()?; - let start_next_after = commitments - .last() - .map(|commitment| commitment.dealer.clone()); + let start_next_after = dealings.last().map(|dealing| dealing.dealer.clone()); - Ok(PagedCommitmentsResponse::new( - commitments, + Ok(PagedDealingsResponse::new( + dealings, limit, start_next_after, )) diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs index bcad1b95ee..26cb5b35db 100644 --- a/contracts/coconut-dkg/src/dealings/storage.rs +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -1,14 +1,14 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use coconut_dkg_common::types::ContractSafeCommitment; +use coconut_dkg_common::types::{ContractSafeBytes, TOTAL_DEALINGS}; use cosmwasm_std::Addr; use cw_storage_plus::Map; -pub(crate) const COMMITMENTS_PAGE_MAX_LIMIT: u32 = 75; -pub(crate) const COMMITMENTS_PAGE_DEFAULT_LIMIT: u32 = 50; +pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2; +pub(crate) const DEALINGS_PAGE_DEFAULT_LIMIT: u32 = 1; -type CommitmentKey<'a> = &'a Addr; +type DealingKey<'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: @@ -21,5 +21,13 @@ type CommitmentKey<'a> = &'a Addr; // 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. -pub(crate) const DEALING_COMMITMENTS: Map<'_, CommitmentKey<'_>, ContractSafeCommitment> = - Map::new("dcmt"); + +// 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"), +]; diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index 7c3160b8b6..7b2b132254 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -2,16 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 use crate::dealers::storage as dealers_storage; -use crate::dealings::storage::DEALING_COMMITMENTS; +use crate::dealings::storage::DEALINGS_BYTES; +use crate::epoch_state::utils::check_epoch_state; use crate::ContractError; -use coconut_dkg_common::types::ContractSafeCommitment; +use coconut_dkg_common::types::{ContractSafeBytes, EpochState}; use cosmwasm_std::{DepsMut, MessageInfo, Response}; -pub fn try_commit_dealing( +pub fn try_commit_dealings( deps: DepsMut<'_>, info: MessageInfo, - commitment: ContractSafeCommitment, + dealing_bytes: ContractSafeBytes, ) -> Result { + check_epoch_state(deps.storage, EpochState::DealingExchange)?; // ensure the sender is a dealer for the current epoch if dealers_storage::current_dealers() .may_load(deps.storage, &info.sender)? @@ -20,13 +22,14 @@ pub fn try_commit_dealing( return Err(ContractError::NotADealer); } - // check if this dealer has already committed to a dealing - // (we don't want to allow overwriting it as some receivers might already be using the commitment) - if DEALING_COMMITMENTS.has(deps.storage, &info.sender) { - return Err(ContractError::AlreadyCommitted); + // 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()); + } } - DEALING_COMMITMENTS.save(deps.storage, &info.sender, &commitment)?; - - Ok(Response::new()) + Err(ContractError::AlreadyCommitted) } diff --git a/contracts/coconut-dkg/src/epoch_state/mod.rs b/contracts/coconut-dkg/src/epoch_state/mod.rs new file mode 100644 index 0000000000..195ff89361 --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod utils; diff --git a/contracts/coconut-dkg/src/epoch_state/queries.rs b/contracts/coconut-dkg/src/epoch_state/queries.rs new file mode 100644 index 0000000000..142ee4826a --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/queries.rs @@ -0,0 +1,13 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::ContractError; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::Storage; + +pub(crate) fn query_current_epoch_state( + storage: &dyn Storage, +) -> Result { + storage::current_epoch_state(storage) +} diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs new file mode 100644 index 0000000000..b0abe5fa5b --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -0,0 +1,29 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractError, ADMIN}; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::{DepsMut, MessageInfo, Response, Storage}; +use cw_storage_plus::Item; + +pub(crate) const CURRENT_EPOCH_STATE: Item<'_, EpochState> = Item::new("current_epoch_state"); + +pub(crate) fn current_epoch_state(storage: &dyn Storage) -> Result { + CURRENT_EPOCH_STATE + .load(storage) + .map_err(|_| ContractError::EpochNotInitialised) +} + +pub(crate) fn advance_epoch_state( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; + CURRENT_EPOCH_STATE.update::<_, ContractError>(deps.storage, |mut epoch_state| { + // TODO: When defaulting to the first state, some action will probably need to be taken on the + // rest of the contract, as we're starting with a new set of signers + epoch_state = epoch_state.next().unwrap_or_default(); + Ok(epoch_state) + })?; + Ok(Response::default()) +} diff --git a/contracts/coconut-dkg/src/epoch_state/utils.rs b/contracts/coconut-dkg/src/epoch_state/utils.rs new file mode 100644 index 0000000000..d78fd49ae3 --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/utils.rs @@ -0,0 +1,21 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractError, CURRENT_EPOCH_STATE}; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::Storage; + +pub(crate) fn check_epoch_state( + storage: &dyn Storage, + against: EpochState, +) -> Result<(), ContractError> { + let epoch_state = CURRENT_EPOCH_STATE.load(storage)?; + if epoch_state != against { + Err(ContractError::IncorrectEpochState { + current_state: epoch_state.to_string(), + expected_state: against.to_string(), + }) + } else { + Ok(()) + } +} diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index 4d187c7a8d..a29ffd8223 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use cosmwasm_std::{Addr, StdError, VerificationError}; +use cw_controllers::AdminError; use thiserror::Error; /// Custom errors for contract failure conditions. @@ -10,6 +11,9 @@ pub enum ContractError { #[error("{0}")] Std(#[from] StdError), + #[error("{0}")] + Admin(#[from] AdminError), + #[error("Group contract invalid address '{addr}'")] InvalidGroup { addr: String }, @@ -40,6 +44,14 @@ pub enum ContractError { #[error("Epoch hasn't been correctly initialised!")] EpochNotInitialised, + #[error( + "Requested action needs state to be {expected_state}, currently in state {current_state}, " + )] + IncorrectEpochState { + current_state: String, + expected_state: String, + }, + // we should never ever see this error (famous last words in programming), therefore, I'd want to // explicitly declare it so that when we ultimate do see it, it's gonna be more informative over "normal" panic #[error("Somehow our validated address {address} is not using correct bech32 encoding")] diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index 78be98bdd8..0151770d1f 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -5,19 +5,22 @@ use crate::constants::MINIMUM_DEPOSIT; use crate::dealers::queries::{ query_current_dealers_paged, query_dealer_details, query_past_dealers_paged, }; -use crate::dealings::queries::query_epoch_dealings_commitments_paged; +use crate::dealings::queries::query_dealings_paged; +use crate::epoch_state::queries::query_current_epoch_state; use crate::error::ContractError; -use crate::state::{State, STATE}; +use crate::state::{State, ADMIN, STATE}; use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use coconut_dkg_common::types::MinimumDepositResponse; +use coconut_dkg_common::types::{EpochState, MinimumDepositResponse}; use cosmwasm_std::{ entry_point, to_binary, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; use cw4::Cw4Contract; +use epoch_state::storage::{advance_epoch_state, CURRENT_EPOCH_STATE}; mod constants; mod dealers; mod dealings; +mod epoch_state; mod error; mod state; @@ -28,11 +31,14 @@ mod state; /// `msg` is the contract initialization message, sort of like a constructor call. #[entry_point] pub fn instantiate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, _info: MessageInfo, msg: InstantiateMsg, ) -> Result { + let admin_addr = deps.api.addr_validate(&msg.admin)?; + ADMIN.set(deps.branch(), Some(admin_addr))?; + let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { ContractError::InvalidGroup { addr: msg.group_addr.clone(), @@ -45,6 +51,8 @@ pub fn instantiate( }; STATE.save(deps.storage, &state)?; + CURRENT_EPOCH_STATE.save(deps.storage, &EpochState::default())?; + Ok(Response::default()) } @@ -60,12 +68,13 @@ pub fn execute( ExecuteMsg::RegisterDealer { bte_key_with_proof } => { dealers::transactions::try_add_dealer(deps, info, bte_key_with_proof) } - ExecuteMsg::CommitDealing { commitment } => { - dealings::transactions::try_commit_dealing(deps, info, commitment) + ExecuteMsg::CommitDealing { dealing_bytes } => { + dealings::transactions::try_commit_dealings(deps, info, dealing_bytes) } ExecuteMsg::DebugUnsafeResetAll { init_msg } => { reset_contract_state(deps, env, info, init_msg) } + ExecuteMsg::AdvanceEpochState {} => advance_epoch_state(deps, info), } } @@ -75,6 +84,7 @@ fn reset_contract_state( info: MessageInfo, init_msg: InstantiateMsg, ) -> Result { + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; // this resets the epoch instantiate(deps.branch(), env, info, init_msg)?; @@ -85,7 +95,7 @@ fn reset_contract_state( let past = dealers::storage::past_dealers() .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) .collect::, _>>()?; - let commitments = crate::dealings::storage::DEALING_COMMITMENTS + let dealings = dealings::storage::DEALINGS_BYTES[0] .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) .collect::, _>>()?; @@ -97,8 +107,10 @@ fn reset_contract_state( dealers::storage::past_dealers().remove(deps.storage, &dealer)?; } - for addr in commitments { - dealings::storage::DEALING_COMMITMENTS.remove(deps.storage, &addr); + for addr in dealings { + for map in dealings::storage::DEALINGS_BYTES { + map.remove(deps.storage, &addr); + } } dealers::storage::NODE_INDEX_COUNTER.save(deps.storage, &0u64)?; @@ -109,6 +121,7 @@ fn reset_contract_state( #[entry_point] pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { let response = match msg { + QueryMsg::GetCurrentEpochState {} => to_binary(&query_current_epoch_state(deps.storage)?)?, QueryMsg::GetDealerDetails { dealer_address } => { to_binary(&query_dealer_details(deps, dealer_address)?)? } @@ -122,9 +135,11 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result to_binary( - &query_epoch_dealings_commitments_paged(deps, start_after, limit)?, - )?, + QueryMsg::GetDealing { + idx, + limit, + start_after, + } => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?, }; Ok(response) @@ -147,6 +162,7 @@ mod tests { let msg = InstantiateMsg { group_addr: "group_addr".to_string(), mix_denom: "nym".to_string(), + admin: "admin".to_string(), }; let info = mock_info("creator", &[]); diff --git a/contracts/coconut-dkg/src/state/mod.rs b/contracts/coconut-dkg/src/state/mod.rs index c08f9e49cb..cc5e965839 100644 --- a/contracts/coconut-dkg/src/state/mod.rs +++ b/contracts/coconut-dkg/src/state/mod.rs @@ -2,15 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use cw4::Cw4Contract; +use cw_controllers::Admin; use cw_storage_plus::Item; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +// unique items +pub const STATE: Item = Item::new("state"); +pub const ADMIN: Admin = Admin::new("admin"); + #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct State { pub mix_denom: String, pub group_addr: Cw4Contract, } - -// unique items -pub const STATE: Item = Item::new("state"); diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 4ab65b6e4a..90574cb063 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -76,7 +76,7 @@ dkg = { path = "../common/crypto/dkg", optional = true } gateway-client = { path = "../common/client-libs/gateway-client" } inclusion-probability = { path = "../common/inclusion-probability" } mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } -contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["committable_trait"] } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["coconut"] } multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nymcoconut = { path = "../common/nymcoconut", optional = true } nymsphinx = { path = "../common/nymsphinx" } diff --git a/validator-api/src/coconut/client.rs b/validator-api/src/coconut/client.rs index 7df918c67d..a09215a9ff 100644 --- a/validator-api/src/coconut/client.rs +++ b/validator-api/src/coconut/client.rs @@ -3,9 +3,9 @@ use crate::coconut::error::Result; use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; -use coconut_dkg_common::dealer::DealerDetailsResponse; -use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; -use contracts_common::commitment::ContractSafeCommitment; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +use contracts_common::dealings::ContractSafeBytes; use multisig_contract_common::msg::ProposalResponse; use validator_client::nymd::cosmwasm_client::types::ExecuteResult; use validator_client::nymd::{AccountId, Fee, TxResponse}; @@ -19,13 +19,13 @@ pub trait Client { &self, blinded_serial_number: String, ) -> Result; + async fn get_current_epoch_state(&self) -> Result; async fn get_self_registered_dealer_details(&self) -> Result; + async fn get_current_dealers(&self) -> Result>; + async fn get_dealings(&self, idx: usize) -> Result>; async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; async fn register_dealer(&self, bte_key: EncodedBTEPublicKeyWithProof) -> Result; - async fn submit_dealing_commitment( - &self, - commitment: ContractSafeCommitment, - ) -> Result; + async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result; } diff --git a/validator-api/src/coconut/dkg/publisher.rs b/validator-api/src/coconut/dkg/client.rs similarity index 52% rename from validator-api/src/coconut/dkg/publisher.rs rename to validator-api/src/coconut/dkg/client.rs index d3312faa2b..87d5c926be 100644 --- a/validator-api/src/coconut/dkg/publisher.rs +++ b/validator-api/src/coconut/dkg/client.rs @@ -3,40 +3,56 @@ use crate::coconut::client::Client; use crate::coconut::error::CoconutError; -use coconut_dkg_common::dealer::DealerDetailsResponse; -use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, NodeIndex}; -use contracts_common::commitment::ContractSafeCommitment; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState, NodeIndex}; +use contracts_common::dealings::ContractSafeBytes; use validator_client::nymd::cosmwasm_client::logs::{find_attribute, NODE_INDEX}; use validator_client::nymd::AccountId; -pub(crate) struct Publisher { - client: Box, +pub(crate) struct DkgClient { + inner: Box, } -impl Publisher { +impl DkgClient { pub(crate) fn new(nymd_client: C) -> Self where C: Client + Send + Sync + 'static, { - let client = Box::new(nymd_client); - Publisher { client } + DkgClient { + inner: Box::new(nymd_client), + } } pub(crate) async fn _get_address(&self) -> AccountId { - self.client.address().await + self.inner.address().await + } + + pub(crate) async fn get_current_epoch_state(&self) -> Result { + self.inner.get_current_epoch_state().await } pub(crate) async fn get_self_registered_dealer_details( &self, ) -> Result { - self.client.get_self_registered_dealer_details().await + self.inner.get_self_registered_dealer_details().await + } + + pub(crate) async fn get_current_dealers(&self) -> Result, CoconutError> { + self.inner.get_current_dealers().await + } + + pub(crate) async fn get_dealings( + &self, + idx: usize, + ) -> Result, CoconutError> { + self.inner.get_dealings(idx).await } pub(crate) async fn register_dealer( &self, bte_key: EncodedBTEPublicKeyWithProof, ) -> Result { - let res = self.client.register_dealer(bte_key).await?; + let res = self.inner.register_dealer(bte_key).await?; let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) .ok_or(CoconutError::NodeIndexRecoveryError { reason: String::from("node index not found"), @@ -50,11 +66,11 @@ impl Publisher { Ok(node_index) } - pub(crate) async fn _submit_dealing_commitment( + pub(crate) async fn submit_dealing( &self, - commitment: ContractSafeCommitment, + dealing_bytes: ContractSafeBytes, ) -> Result<(), CoconutError> { - self.client.submit_dealing_commitment(commitment).await?; + self.inner.submit_dealing(dealing_bytes).await?; Ok(()) } } diff --git a/validator-api/src/coconut/dkg/complaints.rs b/validator-api/src/coconut/dkg/complaints.rs new file mode 100644 index 0000000000..dbb44a8497 --- /dev/null +++ b/validator-api/src/coconut/dkg/complaints.rs @@ -0,0 +1,46 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dkg::error::DkgError; + +pub(crate) enum ComplaintReason { + MalformedBTEPublicKey, + MissingDealing, + MalformedDealing(DkgError), + DealingVerificationError(DkgError), +} + +// pub(crate) async fn complaint_period( +// dkg_client: &DkgClient, +// state: &mut State, +// ) -> Result<(), CoconutError> { +// let dkg_params = dkg::bte::setup(); +// let threshold = state +// .threshold() +// .expect("We should have a tentative threshold by now"); +// let dealings = dkg_client.get_dealings().await?; +// for contract_dealing in dealings { +// match Dealing::try_from_bytes(&contract_dealing.dealing) { +// Ok(dealing) => { +// if let Err(err) = +// dealing.verify(&dkg_params, threshold, &state.current_receivers(), None) +// { +// state.remove_good_dealer(&contract_dealing.dealer); +// state.add_bad_dealer( +// contract_dealing.dealer, +// ComplaintReason::DealingVerificationError(err), +// ); +// } +// } +// Err(err) => { +// state.remove_good_dealer(&contract_dealing.dealer); +// state.add_bad_dealer( +// contract_dealing.dealer, +// ComplaintReason::MalformedDealing(err), +// ); +// } +// } +// } +// +// Ok(()) +// } diff --git a/validator-api/src/coconut/dkg/controller.rs b/validator-api/src/coconut/dkg/controller.rs new file mode 100644 index 0000000000..26ddc308ed --- /dev/null +++ b/validator-api/src/coconut/dkg/controller.rs @@ -0,0 +1,109 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::dkg::{ + dealing::dealing_exchange, public_key::public_key_submission, + verification_key::verification_key_submission, +}; +use crate::coconut::keypair::KeyPair as CoconutKeyPair; +use crate::{nymd_client, Config}; +use anyhow::Result; +use coconut_dkg_common::types::EpochState; +use dkg::bte::keys::KeyPair as DkgKeyPair; +use rand::rngs::OsRng; +use rand::RngCore; +use std::time::Duration; +use task::ShutdownListener; +use tokio::time::interval; +use validator_client::nymd::SigningNymdClient; + +pub(crate) fn init_keypair(config: &Config) -> Result<()> { + let mut rng = OsRng; + let dkg_params = dkg::bte::setup(); + let kp = DkgKeyPair::new(&dkg_params, &mut rng); + pemstore::store_keypair( + &kp, + &pemstore::KeyPairPath::new( + config.decryption_key_path(), + config.public_key_with_proof_path(), + ), + )?; + Ok(()) +} + +pub(crate) struct DkgController { + dkg_client: DkgClient, + state: State, + rng: R, + polling_rate: Duration, +} + +impl DkgController { + pub(crate) fn new( + config: &Config, + nymd_client: nymd_client::Client, + coconut_keypair: CoconutKeyPair, + rng: R, + ) -> Result { + let dkg_keypair = pemstore::load_keypair(&pemstore::KeyPairPath::new( + config.decryption_key_path(), + config.public_key_with_proof_path(), + ))?; + + Ok(DkgController { + dkg_client: DkgClient::new(nymd_client), + state: State::new(dkg_keypair, coconut_keypair), + rng, + polling_rate: config.get_dkg_contract_polling_rate(), + }) + } + + async fn handle_epoch_state(&mut self) -> bool { + match self.dkg_client.get_current_epoch_state().await { + Err(e) => warn!("Could not get current epoch state {}", e), + Ok(epoch_state) => { + if let Err(e) = self.state.is_consistent(epoch_state) { + error!( + "Epoch state is corrupted - {}, the process should be terminated", + e + ); + } + let ret = match epoch_state { + EpochState::PublicKeySubmission => { + public_key_submission(&self.dkg_client, &mut self.state).await + } + EpochState::DealingExchange => { + dealing_exchange(&self.dkg_client, &mut self.state, self.rng.clone()).await + } + EpochState::VerificationKeySubmission => { + verification_key_submission(&self.dkg_client, &mut self.state).await + } + EpochState::InProgress => return true, + }; + if let Err(e) = ret { + warn!("Could not handle this iteration for the epoch state: {}", e); + } + } + } + false + } + + pub(crate) async fn run(mut self, mut shutdown: ShutdownListener) { + let mut interval = interval(self.polling_rate); + while !shutdown.is_shutdown() { + tokio::select! { + _ = interval.tick() => { + if self.handle_epoch_state().await { + // If dkg finished, we can finish this task + break; + } + } + _ = shutdown.recv() => { + trace!("DkgController: Received shutdown"); + } + } + } + } +} diff --git a/validator-api/src/coconut/dkg/dealing.rs b/validator-api/src/coconut/dkg/dealing.rs new file mode 100644 index 0000000000..647df75797 --- /dev/null +++ b/validator-api/src/coconut/dkg/dealing.rs @@ -0,0 +1,53 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::error::CoconutError; +use coconut_dkg_common::types::TOTAL_DEALINGS; +use contracts_common::dealings::ContractSafeBytes; +use dkg::bte::setup; +use dkg::Dealing; +use rand::RngCore; + +pub(crate) async fn dealing_exchange( + dkg_client: &DkgClient, + state: &mut State, + rng: impl RngCore + Clone, +) -> Result<(), CoconutError> { + if state.receiver_index().is_some() { + info!("Already have index {}", state.receiver_index().unwrap()); + return Ok(()); + } + + let dealers = dkg_client.get_current_dealers().await?; + // note: ceiling in integer division can be achieved via q = (x + y - 1) / y; + let threshold = (2 * dealers.len() as u64 + 3 - 1) / 3; + + state.set_dealers(dealers); + state.set_threshold(threshold); + let receivers = state.current_dealers_by_idx(); + let params = setup(); + let dealer_index = state.node_index_value()?; + let receiver_index = receivers + .keys() + .position(|node_index| *node_index == dealer_index); + for _ in 0..TOTAL_DEALINGS { + let (dealing, _) = Dealing::create( + rng.clone(), + ¶ms, + dealer_index, + threshold, + &receivers, + None, + ); + dkg_client + .submit_dealing(ContractSafeBytes::from(&dealing)) + .await?; + } + + info!("Setting index to {}", receiver_index.unwrap()); + state.set_receiver_index(receiver_index); + + Ok(()) +} diff --git a/validator-api/src/coconut/dkg/mod.rs b/validator-api/src/coconut/dkg/mod.rs index aba57513d5..4149a3e236 100644 --- a/validator-api/src/coconut/dkg/mod.rs +++ b/validator-api/src/coconut/dkg/mod.rs @@ -1,71 +1,10 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::coconut::client::Client; -use crate::coconut::dkg::publisher::Publisher; -use crate::Config; -use anyhow::Result; -use dkg::bte::keys::KeyPair; -use rand::rngs::OsRng; -use task::ShutdownListener; - -pub(crate) mod publisher; - -pub(crate) fn init_keypair(config: &Config) -> Result<()> { - let mut rng = OsRng; - let dkg_params = dkg::bte::setup(); - let kp = KeyPair::new(&dkg_params, &mut rng); - pemstore::store_keypair( - &kp, - &pemstore::KeyPairPath::new( - config.decryption_key_path(), - config.public_key_with_proof_path(), - ), - )?; - Ok(()) -} - -pub(crate) struct DkgController { - publisher: Publisher, - keypair: KeyPair, -} - -impl DkgController { - pub(crate) fn new(config: &Config, nymd_client: C) -> Result - where - C: Client + Send + Sync + 'static, - { - let publisher = Publisher::new(nymd_client); - let keypair = pemstore::load_keypair(&pemstore::KeyPairPath::new( - config.decryption_key_path(), - config.public_key_with_proof_path(), - ))?; - Ok(DkgController { publisher, keypair }) - } - - pub(crate) async fn run(&self, mut shutdown: ShutdownListener) { - let bte_key = bs58::encode(&self.keypair.public_key().to_bytes()).into_string(); - let index = if let Some(details) = self - .publisher - .get_self_registered_dealer_details() - .await - .expect("Could not query for dealer details") - .details - { - details.assigned_index - } else { - self.publisher - .register_dealer(bte_key) - .await - .expect("Could not register dealer in dkg protocol") - }; - info!("Starting dkg protocol with index {}", index); - while !shutdown.is_shutdown() { - tokio::select! { - _ = shutdown.recv() => { - trace!("DkgController: Received shutdown"); - } - } - } - } -} +pub(crate) mod client; +pub(crate) mod complaints; +pub(crate) mod controller; +pub(crate) mod dealing; +pub(crate) mod public_key; +pub(crate) mod state; +pub(crate) mod verification_key; diff --git a/validator-api/src/coconut/dkg/public_key.rs b/validator-api/src/coconut/dkg/public_key.rs new file mode 100644 index 0000000000..de1c8c86e6 --- /dev/null +++ b/validator-api/src/coconut/dkg/public_key.rs @@ -0,0 +1,30 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::state::State; +use crate::coconut::error::CoconutError; + +pub(crate) async fn public_key_submission( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.node_index().is_some() { + return Ok(()); + } + + let bte_key = bs58::encode(&state.dkg_keypair().public_key().to_bytes()).into_string(); + let index = if let Some(details) = dkg_client + .get_self_registered_dealer_details() + .await? + .details + { + details.assigned_index + } else { + dkg_client.register_dealer(bte_key).await? + }; + state.set_node_index(index); + info!("Starting dkg protocol with index {}", index); + + Ok(()) +} diff --git a/validator-api/src/coconut/dkg/state.rs b/validator-api/src/coconut/dkg/state.rs new file mode 100644 index 0000000000..43098d771b --- /dev/null +++ b/validator-api/src/coconut/dkg/state.rs @@ -0,0 +1,190 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::error::CoconutError; +use crate::coconut::keypair::KeyPair as CoconutKeyPair; +use coconut_dkg_common::dealer::DealerDetails; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::Addr; +use dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof}; +use dkg::{NodeIndex, Threshold}; +use std::collections::BTreeMap; + +// note: each dealer is also a receiver which simplifies some logic significantly +#[derive(Debug)] +pub(crate) struct DkgParticipant { + pub(crate) _address: Addr, + pub(crate) bte_public_key_with_proof: PublicKeyWithProof, + pub(crate) assigned_index: NodeIndex, +} + +impl TryFrom for DkgParticipant { + type Error = ComplaintReason; + + fn try_from(dealer: DealerDetails) -> Result { + let bte_public_key_with_proof = bs58::decode(dealer.bte_public_key_with_proof) + .into_vec() + .map(|bytes| PublicKeyWithProof::try_from_bytes(&bytes)) + .map_err(|_| ComplaintReason::MalformedBTEPublicKey)? + .map_err(|_| ComplaintReason::MalformedBTEPublicKey)?; + + Ok(DkgParticipant { + _address: dealer.address, + bte_public_key_with_proof, + assigned_index: dealer.assigned_index, + }) + } +} + +pub(crate) trait ConsistentState { + fn node_index_value(&self) -> Result; + fn receiver_index_value(&self) -> Result; + fn threshold(&self) -> Result; + fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> { + match epoch_state { + EpochState::PublicKeySubmission => {} + EpochState::DealingExchange => { + self.node_index_value()?; + } + EpochState::VerificationKeySubmission => { + self.node_index_value()?; + self.receiver_index_value()?; + self.threshold()?; + } + EpochState::InProgress => { + self.node_index_value()?; + self.receiver_index_value()?; + self.threshold()?; + } + } + Ok(()) + } +} + +pub(crate) struct State { + dkg_keypair: DkgKeyPair, + coconut_keypair: CoconutKeyPair, + node_index: Option, + dealers: BTreeMap>, + receiver_index: Option, + threshold: Option, +} + +impl ConsistentState for State { + fn node_index_value(&self) -> Result { + self.node_index.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Node index should have been set"), + }) + } + + fn receiver_index_value(&self) -> Result { + self.receiver_index.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Receiver index should have been set"), + }) + } + + fn threshold(&self) -> Result { + let threshold = self.threshold.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Threshold should have been set"), + })?; + if self.current_dealers_by_idx().len() < threshold as usize { + Err(CoconutError::UnrecoverableState { + reason: String::from( + "Not enough good dealers in the signer set to achieve threshold", + ), + }) + } else { + Ok(threshold) + } + } +} + +impl State { + pub fn new(dkg_keypair: DkgKeyPair, coconut_keypair: CoconutKeyPair) -> Self { + State { + dkg_keypair, + coconut_keypair, + node_index: None, + dealers: BTreeMap::new(), + receiver_index: None, + threshold: None, + } + } + + pub fn dkg_keypair(&self) -> &DkgKeyPair { + &self.dkg_keypair + } + + pub async fn coconut_keypair_is_some(&self) -> bool { + self.coconut_keypair.get().await.is_some() + } + + pub fn node_index(&self) -> Option { + self.node_index + } + + pub fn receiver_index(&self) -> Option { + self.receiver_index + } + + pub fn current_dealers_by_addr(&self) -> BTreeMap { + self.dealers + .iter() + .filter_map(|(addr, dealer)| { + dealer + .as_ref() + .ok() + .map(|participant| (addr.clone(), participant.assigned_index)) + }) + .collect() + } + + pub fn current_dealers_by_idx(&self) -> BTreeMap { + self.dealers + .iter() + .filter_map(|(_, dealer)| { + dealer.as_ref().ok().map(|participant| { + ( + participant.assigned_index, + *participant.bte_public_key_with_proof.public_key(), + ) + }) + }) + .collect() + } + + pub async fn set_coconut_keypair(&mut self, coconut_keypair: coconut_interface::KeyPair) { + self.coconut_keypair.set(coconut_keypair).await + } + + pub fn set_node_index(&mut self, node_index: NodeIndex) { + self.node_index = Some(node_index); + } + + pub fn set_dealers(&mut self, dealers: Vec) { + self.dealers = BTreeMap::from_iter( + dealers + .into_iter() + .map(|details| (details.address.clone(), DkgParticipant::try_from(details))), + ) + } + + pub fn mark_bad_dealer(&mut self, dealer_addr: &Addr, reason: ComplaintReason) { + if let Some((_, value)) = self + .dealers + .iter_mut() + .find(|(addr, _)| *addr == dealer_addr) + { + *value = Err(reason); + } + } + + pub fn set_receiver_index(&mut self, receiver_index: Option) { + self.receiver_index = receiver_index; + } + + pub fn set_threshold(&mut self, threshold: Threshold) { + self.threshold = Some(threshold); + } +} diff --git a/validator-api/src/coconut/dkg/verification_key.rs b/validator-api/src/coconut/dkg/verification_key.rs new file mode 100644 index 0000000000..a900708fd2 --- /dev/null +++ b/validator-api/src/coconut/dkg/verification_key.rs @@ -0,0 +1,141 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::error::CoconutError; +use coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS}; +use coconut_interface::KeyPair as CoconutKeyPair; +use cosmwasm_std::Addr; +use credentials::coconut::bandwidth::{PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; +use dkg::bte::{decrypt_share, setup}; +use dkg::{combine_shares, Dealing}; +use nymcoconut::{KeyPair, Parameters, SecretKey}; +use std::collections::BTreeMap; + +// Filter the dealers based on what dealing they posted (or not) in the contract +async fn deterministic_filter_dealers( + dkg_client: &DkgClient, + state: &mut State, +) -> Result>, CoconutError> { + let mut dealings_maps = vec![]; + let initial_dealers_by_addr = state.current_dealers_by_addr(); + let initial_receivers = state.current_dealers_by_idx(); + let threshold = state.threshold()?; + let params = setup(); + let retries = 3; + + for idx in 0..TOTAL_DEALINGS { + let mut try_no = 0; + let dealings = loop { + // this is a really ugly way to get the dealings, but for some reason the first query + // always fails with a RPC error. + try_no += 1; + if let Ok(dealings) = dkg_client.get_dealings(idx).await { + break dealings; + } else if try_no == retries { + return Err(CoconutError::UnrecoverableState { + reason: String::from("Could not get dealings"), + }); + } + }; + let dealings_map = + BTreeMap::from_iter(dealings.into_iter().filter_map(|contract_dealing| { + match Dealing::try_from(&contract_dealing.dealing) { + Ok(dealing) => { + if let Err(err) = + dealing.verify(¶ms, threshold, &initial_receivers, None) + { + state.mark_bad_dealer( + &contract_dealing.dealer, + ComplaintReason::DealingVerificationError(err), + ); + None + } else if let Some(idx) = + initial_dealers_by_addr.get(&contract_dealing.dealer) + { + Some((*idx, (contract_dealing.dealer, dealing))) + } else { + None + } + } + Err(err) => { + state.mark_bad_dealer( + &contract_dealing.dealer, + ComplaintReason::MalformedDealing(err), + ); + None + } + } + })); + dealings_maps.push(dealings_map); + } + for (addr, _) in initial_dealers_by_addr.iter() { + for dealings_map in dealings_maps.iter() { + if !dealings_map.iter().any(|(_, (address, _))| address == addr) { + state.mark_bad_dealer(addr, ComplaintReason::MissingDealing); + break; + } + } + } + + Ok(dealings_maps) +} + +fn derive_partial_keypair( + state: &State, + dealings_maps: Vec>, +) -> Result { + let filtered_receivers_by_idx = state.current_dealers_by_idx(); + let filtered_dealers_by_addr = state.current_dealers_by_addr(); + let dk = state.dkg_keypair().private_key(); + let node_index_value = state.receiver_index_value()?; + let mut scalars = vec![]; + for dealings_map in dealings_maps.into_iter() { + let filtered_dealings: Vec<_> = dealings_map + .into_iter() + .filter_map(|(_, (addr, dealing))| { + if filtered_dealers_by_addr.keys().any(|a| addr == *a) { + Some(dealing) + } else { + None + } + }) + .collect(); + let shares = filtered_dealings + .iter() + .map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None)) + .collect::>()?; + let scalar = combine_shares( + shares, + &filtered_receivers_by_idx + .keys() + .copied() + .collect::>(), + )?; + scalars.push(scalar); + } + + let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; + let x = scalars.pop().unwrap(); + let sk = SecretKey::create_from_raw(x, scalars); + let vk = sk.verification_key(¶ms); + + Ok(CoconutKeyPair::from_keys(sk, vk)) +} + +pub(crate) async fn verification_key_submission( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.coconut_keypair_is_some().await { + return Ok(()); + } + + let dealings_maps = deterministic_filter_dealers(dkg_client, state).await?; + let coconut_keypair = derive_partial_keypair(state, dealings_maps)?; + state.set_coconut_keypair(coconut_keypair).await; + + Ok(()) +} diff --git a/validator-api/src/coconut/error.rs b/validator-api/src/coconut/error.rs index 1dcad8224d..4bf9ba0343 100644 --- a/validator-api/src/coconut/error.rs +++ b/validator-api/src/coconut/error.rs @@ -11,6 +11,7 @@ use crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, }; +use dkg::error::DkgError; use validator_client::nymd::error::NymdError; use crate::node_status_api::models::ValidatorApiStorageError; @@ -31,6 +32,9 @@ pub enum CoconutError { #[error("Nymd error - {0}")] NymdError(#[from] NymdError), + #[error("Validator client error - {0}")] + ValidatorClientError(#[from] validator_client::ValidatorClientError), + #[error("Coconut internal error - {0}")] CoconutInternalError(#[from] nymcoconut::CoconutError), @@ -78,8 +82,17 @@ pub enum CoconutError { #[error("Invalid status of credential: {status}")] InvalidCredentialStatus { status: String }, + #[error("DKG error: {0}")] + DkgError(#[from] DkgError), + #[error("Failed to recover assigned node index: {reason}")] NodeIndexRecoveryError { reason: String }, + + #[error("Unrecoverable state: {reason}. Process should be restarted")] + UnrecoverableState { reason: String }, + + #[error("DKG has not finished yet in order to derive the coconut key")] + KeyPairNotDerivedYet, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/validator-api/src/coconut/keypair.rs b/validator-api/src/coconut/keypair.rs new file mode 100644 index 0000000000..680f1c4d13 --- /dev/null +++ b/validator-api/src/coconut/keypair.rs @@ -0,0 +1,27 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Clone, Debug)] +pub struct KeyPair { + inner: Arc>>, +} + +impl KeyPair { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(None)), + } + } + + pub async fn get(&self) -> RwLockReadGuard<'_, Option> { + self.inner.read().await + } + + pub async fn set(&self, keypair: coconut_interface::KeyPair) { + let mut w_lock = self.inner.write().await; + *w_lock = Some(keypair); + } +} diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index 6e0552cace..7902aea36a 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod comm; mod deposit; pub(crate) mod dkg; pub(crate) mod error; +pub(crate) mod keypair; #[cfg(test)] mod tests; @@ -17,8 +18,9 @@ use crate::ValidatorApiStorage; use coconut_bandwidth_contract_common::spend_credential::{ funds_from_cosmos_msgs, SpendCredentialStatus, }; +use coconut_interface::KeyPair as CoconutKeyPair; use coconut_interface::{ - Attribute, BlindSignRequest, BlindedSignature, KeyPair, Parameters, VerificationKey, + Attribute, BlindSignRequest, BlindedSignature, Parameters, VerificationKey, }; use config::defaults::VALIDATOR_API_VERSION; use credentials::coconut::params::{ @@ -27,6 +29,7 @@ use credentials::coconut::params::{ use crypto::asymmetric::encryption; use crypto::shared_key::new_ephemeral_shared_key; use crypto::symmetric::stream_cipher; +use keypair::KeyPair; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, @@ -193,7 +196,7 @@ impl InternalSignRequest { } } -fn blind_sign(request: InternalSignRequest, key_pair: &KeyPair) -> Result { +fn blind_sign(request: InternalSignRequest, key_pair: &CoconutKeyPair) -> Result { let params = Parameters::new(request.total_params())?; Ok(coconut_interface::blind_sign( ¶ms, @@ -226,7 +229,11 @@ pub async fn post_blind_sign( blind_sign_request_body.public_attributes(), blind_sign_request_body.blind_sign_request().clone(), ); - let blinded_signature = blind_sign(internal_request, &state.key_pair)?; + let blinded_signature = if let Some(keypair) = state.key_pair.get().await.as_ref() { + blind_sign(internal_request, keypair)? + } else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; let response = state .encrypt_and_store( @@ -255,9 +262,13 @@ pub async fn post_partial_bandwidth_credential( pub async fn get_verification_key( state: &RocketState, ) -> Result> { - Ok(Json(VerificationKeyResponse::new( - state.key_pair.verification_key(), - ))) + state + .key_pair + .get() + .await + .as_ref() + .map(|keypair| Json(VerificationKeyResponse::new(keypair.verification_key()))) + .ok_or(CoconutError::KeyPairNotDerivedYet) } #[get("/cosmos-address")] diff --git a/validator-api/src/coconut/tests.rs b/validator-api/src/coconut/tests.rs index afbfe2c889..2f37a5ab0c 100644 --- a/validator-api/src/coconut/tests.rs +++ b/validator-api/src/coconut/tests.rs @@ -39,9 +39,9 @@ use validator_client::validator_api::routes::{ use crate::coconut::State; use crate::ValidatorApiStorage; use async_trait::async_trait; -use coconut_dkg_common::dealer::DealerDetailsResponse; -use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; -use contracts_common::commitment::ContractSafeCommitment; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +use contracts_common::dealings::ContractSafeBytes; use crypto::asymmetric::{encryption, identity}; use rand_07::rngs::OsRng; use rocket::http::Status; @@ -121,10 +121,22 @@ impl super::client::Client for DummyClient { }) } + async fn get_current_epoch_state(&self) -> Result { + todo!() + } + async fn get_self_registered_dealer_details(&self) -> Result { todo!() } + async fn get_current_dealers(&self) -> Result> { + todo!() + } + + async fn get_dealings(&self, idx: usize) -> Result> { + todo!() + } + async fn vote_proposal( &self, proposal_id: u64, @@ -148,10 +160,7 @@ impl super::client::Client for DummyClient { todo!() } - async fn submit_dealing_commitment( - &self, - commitment: ContractSafeCommitment, - ) -> Result { + async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result { todo!() } } @@ -209,11 +218,13 @@ async fn check_signer_verif_key(key_pair: KeyPair) { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage, )); @@ -303,11 +314,13 @@ async fn signed_before() { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -373,10 +386,12 @@ async fn state_functions() { db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let state = State::new( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), ); @@ -543,11 +558,13 @@ async fn blind_sign_correct() { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -622,11 +639,13 @@ async fn signature_test() { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -694,10 +713,12 @@ async fn get_cosmos_address() { db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -740,15 +761,23 @@ async fn verification_of_bandwidth_credential() { hash_to_scalar(voucher_value.to_string()), hash_to_scalar(voucher_info), ]; - let theta = theta_from_keys_and_attributes(¶ms, &key_pairs, &public_attributes).unwrap(); + let indices: Vec = key_pairs + .iter() + .enumerate() + .map(|(idx, _)| (idx + 1) as u64) + .collect(); + let theta = + theta_from_keys_and_attributes(¶ms, &key_pairs, &indices, &public_attributes).unwrap(); let key_pair = key_pairs.remove(0); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage1 = ValidatorApiStorage::init(db_dir).await.unwrap(); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client.clone(), TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel.clone(), storage1.clone(), )); diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 2eb19d351c..6b6196eb41 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -12,6 +12,8 @@ mod template; pub const DEFAULT_LOCAL_VALIDATOR: &str = "http://localhost:26657"; +pub const DEFAULT_DKG_CONTRACT_POLLING_RATE: Duration = Duration::from_secs(10); + const DEFAULT_GATEWAY_SENDING_RATE: usize = 200; const DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS: usize = 50; const DEFAULT_PACKET_DELIVERY_TIMEOUT: Duration = Duration::from_secs(20); @@ -277,6 +279,9 @@ pub struct CoconutSigner { /// Path to the dkg dealer public key with proof. public_key_with_proof_path: PathBuf, + /// Duration of the interval for polling the dkg contract. + dkg_contract_polling_rate: Duration, + /// Specifies list of all validators on the network issuing coconut credentials. /// A special care must be taken to ensure they are in correct order. /// The list must also contain THIS validator that is running the test @@ -303,6 +308,7 @@ impl Default for CoconutSigner { keypair_path: Default::default(), decryption_key_path: CoconutSigner::default_dkg_decryption_key_path(), public_key_with_proof_path: CoconutSigner::default_dkg_public_key_with_proof_path(), + dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE, all_validator_apis: Default::default(), } } @@ -481,7 +487,7 @@ impl Config { } #[cfg(feature = "coconut")] - pub fn keypair_path(&self) -> PathBuf { + pub fn _keypair_path(&self) -> PathBuf { self.coconut_signer.keypair_path.clone() } @@ -495,6 +501,11 @@ impl Config { self.coconut_signer.public_key_with_proof_path.clone() } + #[cfg(feature = "coconut")] + pub fn get_dkg_contract_polling_rate(&self) -> Duration { + self.coconut_signer.dkg_contract_polling_rate + } + // fix dead code warnings as this method is only ever used with coconut feature #[cfg(feature = "coconut")] pub fn get_all_validator_api_endpoints(&self) -> Vec { diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index 11b8a90a1d..ff44f6057e 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -41,12 +41,12 @@ use crate::epoch_operations::RewardedSetUpdater; #[cfg(feature = "coconut")] use coconut::{ comm::QueryCommunicationChannel, - dkg::{init_keypair, DkgController}, + dkg::controller::{init_keypair, DkgController}, InternalSignRequest, }; -#[cfg(feature = "coconut")] -use coconut_interface::{Base58, KeyPair}; use logging::setup_logging; +#[cfg(feature = "coconut")] +use validator_client::nymd::bip32::secp256k1::elliptic_curve::rand_core::OsRng; pub(crate) mod config; pub(crate) mod contract_cache; @@ -419,6 +419,7 @@ async fn setup_rocket( _mix_denom: String, liftoff_notify: Arc, _nymd_client: Client, + #[cfg(feature = "coconut")] coconut_keypair: coconut::keypair::KeyPair, ) -> Result> { let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); let mut rocket = rocket::build(); @@ -451,14 +452,10 @@ async fn setup_rocket( #[cfg(feature = "coconut")] let rocket = if config.get_coconut_signer_enabled() { - let keypair_bs58 = fs::read_to_string(config.keypair_path())? - .trim() - .to_string(); - let keypair = KeyPair::try_from_bs58(keypair_bs58)?; rocket.attach(InternalSignRequest::stage( _nymd_client, _mix_denom, - keypair, + coconut_keypair, QueryCommunicationChannel::new(config.get_all_validator_api_endpoints()), storage.clone().unwrap(), )) @@ -544,12 +541,17 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { // We need a bigger timeout let shutdown = ShutdownNotifier::new(10); + #[cfg(feature = "coconut")] + let coconut_keypair = coconut::keypair::KeyPair::new(); + // let's build our rocket! let rocket = setup_rocket( &config, mix_denom, Arc::clone(&liftoff_notify), signing_nymd_client.clone(), + #[cfg(feature = "coconut")] + coconut_keypair.clone(), ) .await?; let monitor_builder = setup_network_monitor(&config, system_version, &rocket); @@ -559,7 +561,8 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { #[cfg(feature = "coconut")] { - let dkg_controller = DkgController::new(&config, signing_nymd_client.clone())?; + let dkg_controller = + DkgController::new(&config, signing_nymd_client.clone(), coconut_keypair, OsRng)?; let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { dkg_controller.run(shutdown_listener).await }); } diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 9f38582339..d2fd06acdb 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -26,11 +26,11 @@ use async_trait::async_trait; #[cfg(feature = "coconut")] use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; #[cfg(feature = "coconut")] -use coconut_dkg_common::dealer::DealerDetailsResponse; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; #[cfg(feature = "coconut")] -use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; #[cfg(feature = "coconut")] -use contracts_common::commitment::ContractSafeCommitment; +use contracts_common::dealings::ContractSafeBytes; #[cfg(feature = "coconut")] use multisig_contract_common::msg::ProposalResponse; #[cfg(feature = "coconut")] @@ -304,6 +304,10 @@ where .await?) } + async fn get_current_epoch_state(&self) -> crate::coconut::error::Result { + Ok(self.0.read().await.nymd.get_current_epoch_state().await?) + } + async fn get_self_registered_dealer_details( &self, ) -> crate::coconut::error::Result { @@ -317,6 +321,17 @@ where .await?) } + async fn get_current_dealers(&self) -> crate::coconut::error::Result> { + Ok(self.0.read().await.get_all_nymd_current_dealers().await?) + } + + async fn get_dealings( + &self, + idx: usize, + ) -> crate::coconut::error::Result> { + Ok(self.0.read().await.get_all_nymd_epoch_dealings(idx).await?) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -345,16 +360,16 @@ where .await?) } - async fn submit_dealing_commitment( + async fn submit_dealing( &self, - commitment: ContractSafeCommitment, + dealing_bytes: ContractSafeBytes, ) -> Result { Ok(self .0 .write() .await .nymd - .submit_dealing_commitment(commitment, None) + .submit_dealing_bytes(dealing_bytes, None) .await?) } } From fa95d15eac1be606b243f7eab0d5e2080b82e2f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 21 Nov 2022 23:52:30 +0100 Subject: [PATCH 30/75] socks5-client: throttle connection inbound from application until data is sent (#1783) * socks5: throttle send * client-connections: add additional methods * WIP * Update * Input message sender bounded * WIP * Remove the delay that is no longer needed * rustfmt * clippy * Fix wasm build * clippy * Try to use MixProxySender/Reader type alias * Extract out wait function * Wait on every msg * changelog: add note * rustfmt --- CHANGELOG.md | 2 + .../src/client/inbound_messages.rs | 5 +- .../input_message_listener.rs | 16 ++-- .../retransmission_request_listener.rs | 11 ++- .../src/client/real_messages_control/mod.rs | 2 +- .../real_traffic_stream.rs | 9 +- clients/native/src/client/mod.rs | 33 +++++-- clients/native/src/websocket/handler.rs | 41 +++++--- clients/socks5/src/client/mod.rs | 7 +- clients/socks5/src/socks/client.rs | 16 +++- clients/socks5/src/socks/server.rs | 6 +- clients/webassembly/Cargo.toml | 9 +- clients/webassembly/src/client/mod.rs | 13 ++- common/client-connections/src/lib.rs | 32 ++++++- .../proxy-helpers/src/proxy_runner/inbound.rs | 96 ++++++++++++++++--- .../proxy-helpers/src/proxy_runner/mod.rs | 12 ++- .../network-requester/src/connection.rs | 6 +- .../network-requester/src/core.rs | 36 ++++--- .../src/statistics/collector.rs | 28 ++++-- 19 files changed, 282 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d21a49d7fd..7cd8056355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### Changed - clients: add concept of transmission lanes to better handle multiple data streams ([#1720]) +- socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) [#1720]: https://github.com/nymtech/nym/pull/1720 +[#1783]: https://github.com/nymtech/nym/pull/1783 ## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09) diff --git a/clients/client-core/src/client/inbound_messages.rs b/clients/client-core/src/client/inbound_messages.rs index 3cc71d4460..909e3720f9 100644 --- a/clients/client-core/src/client/inbound_messages.rs +++ b/clients/client-core/src/client/inbound_messages.rs @@ -1,10 +1,9 @@ use client_connections::TransmissionLane; -use futures::channel::mpsc; use nymsphinx::addressing::clients::Recipient; use nymsphinx::anonymous_replies::ReplySurb; -pub type InputMessageSender = mpsc::UnboundedSender; -pub type InputMessageReceiver = mpsc::UnboundedReceiver; +pub type InputMessageSender = tokio::sync::mpsc::Sender; +pub type InputMessageReceiver = tokio::sync::mpsc::Receiver; #[derive(Debug)] pub enum InputMessage { diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 20cbc10de5..58fa65af56 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -9,7 +9,6 @@ use crate::client::{ topology_control::TopologyAccessor, }; use client_connections::TransmissionLane; -use futures::StreamExt; use log::*; use nymsphinx::anonymous_replies::ReplySurb; use nymsphinx::preparer::MessagePreparer; @@ -188,9 +187,14 @@ where // there's no point in trying to send nothing if let Some(real_messages) = real_messages { // tells real message sender (with the poisson timer) to send this to the mix network - self.real_message_sender - .unbounded_send((real_messages, lane)) - .unwrap(); + if self + .real_message_sender + .send((real_messages, lane)) + .await + .is_err() + { + panic!(); + } } } @@ -200,7 +204,7 @@ where while !shutdown.is_shutdown() { tokio::select! { - input_msg = self.input_receiver.next() => match input_msg { + input_msg = self.input_receiver.recv() => match input_msg { Some(input_msg) => { self.on_input_message(input_msg).await; }, @@ -221,7 +225,7 @@ where #[cfg(target_arch = "wasm32")] pub(super) async fn run(&mut self) { debug!("Started InputMessageListener without graceful shutdown support"); - while let Some(input_msg) = self.input_receiver.next().await { + while let Some(input_msg) = self.input_receiver.recv().await { self.on_input_message(input_msg).await; } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 8a26809c79..2fbab44b61 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -116,12 +116,17 @@ where .unwrap(); // send to `OutQueueControl` to eventually send to the mix network - self.real_message_sender - .unbounded_send(( + if self + .real_message_sender + .send(( vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], TransmissionLane::Retransmission, )) - .unwrap(); + .await + .is_err() + { + panic!(); + } } #[cfg(not(target_arch = "wasm32"))] diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index a1e9290c64..d8b9dd34a6 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -119,7 +119,7 @@ impl RealMessagesController { ) -> Self { let rng = OsRng; - let (real_message_sender, real_message_receiver) = mpsc::unbounded(); + let (real_message_sender, real_message_receiver) = tokio::sync::mpsc::channel(1); let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded(); let ack_controller_connectors = AcknowledgementControllerConnectors::new( diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 43be7524b3..9f42232db5 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -7,7 +7,6 @@ use crate::client::topology_control::TopologyAccessor; use client_connections::{ ClosedConnectionReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, }; -use futures::channel::mpsc; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; use log::*; @@ -158,8 +157,8 @@ impl RealMessage { // messages are already prepared, etc. the real point of it is to forward it to mix_traffic // after sufficient delay pub(crate) type BatchRealMessageSender = - mpsc::UnboundedSender<(Vec, TransmissionLane)>; -type BatchRealMessageReceiver = mpsc::UnboundedReceiver<(Vec, TransmissionLane)>; + tokio::sync::mpsc::Sender<(Vec, TransmissionLane)>; +type BatchRealMessageReceiver = tokio::sync::mpsc::Receiver<(Vec, TransmissionLane)>; pub(crate) enum StreamMessage { Cover, @@ -368,7 +367,7 @@ where // in `Vec`, this ensures that on average we will fetch messages faster than we can // send, which is a condition for being able to multiplex sphinx packets from multiple // data streams. - match Pin::new(&mut self.real_receiver).poll_next(cx) { + match Pin::new(&mut self.real_receiver).poll_recv(cx) { // in the case our real message channel stream was closed, we should also indicate we are closed // (and whoever is using the stream should panic) Poll::Ready(None) => Poll::Ready(None), @@ -416,7 +415,7 @@ where self.on_close_connection(id); } - match Pin::new(&mut self.real_receiver).poll_next(cx) { + match Pin::new(&mut self.real_receiver).poll_recv(cx) { // in the case our real message channel stream was closed, we should also indicate we are closed // (and whoever is using the stream should panic) Poll::Ready(None) => Poll::Ready(None), diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 91fb13bcfa..ecc057e62b 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -304,28 +304,43 @@ impl NymClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub fn send_message(&mut self, recipient: Recipient, message: Vec, with_reply_surb: bool) { + pub async fn send_message( + &mut self, + recipient: Recipient, + message: Vec, + with_reply_surb: bool, + ) { let lane = TransmissionLane::General; let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); - self.input_tx + if self + .input_tx .as_ref() .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap(); + .send(input_msg) + .await + .is_err() + { + panic!(); + } } /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { + pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { let input_msg = InputMessage::new_reply(reply_surb, message); - self.input_tx + if self + .input_tx .as_ref() .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap(); + .send(input_msg) + .await + .is_err() + { + panic!(); + } } /// EXPERIMENTAL DIRECT RUST API @@ -382,7 +397,7 @@ impl NymClient { let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); // channels responsible for controlling real messages - let (input_sender, input_receiver) = mpsc::unbounded::(); + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 9830b6fdd0..07b196354d 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -81,7 +81,7 @@ impl Handler { } } - fn handle_send( + async fn handle_send( &mut self, recipient: &Recipient, message: Vec, @@ -91,18 +91,26 @@ impl Handler { // the ack control is now responsible for chunking, etc. let lane = TransmissionLane::ConnectionId(connection_id); let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); - self.msg_input.unbounded_send(input_msg).unwrap(); + if self.msg_input.send(input_msg).await.is_err() { + panic!(); + } None } - fn handle_reply(&mut self, reply_surb: ReplySurb, message: Vec) -> Option { + async fn handle_reply( + &mut self, + reply_surb: ReplySurb, + message: Vec, + ) -> Option { if message.len() > ReplySurb::max_msg_len(Default::default()) { return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySurb::max_msg_len(Default::default())))); } let input_msg = InputMessage::new_reply(reply_surb, message); - self.msg_input.unbounded_send(input_msg).unwrap(); + if self.msg_input.send(input_msg).await.is_err() { + panic!(); + } None } @@ -118,24 +126,27 @@ impl Handler { None } - fn handle_request(&mut self, request: ClientRequest) -> Option { + async fn handle_request(&mut self, request: ClientRequest) -> Option { match request { ClientRequest::Send { recipient, message, with_reply_surb, connection_id, - } => self.handle_send(&recipient, message, with_reply_surb, connection_id), + } => { + self.handle_send(&recipient, message, with_reply_surb, connection_id) + .await + } ClientRequest::Reply { message, reply_surb, - } => self.handle_reply(reply_surb, message), + } => self.handle_reply(reply_surb, message).await, ClientRequest::SelfAddress => Some(self.handle_self_address()), ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id), } } - fn handle_text_message(&mut self, msg: String) -> Option { + async fn handle_text_message(&mut self, msg: String) -> Option { debug!("Handling text message request"); trace!("Content: {:?}", msg); @@ -144,13 +155,13 @@ impl Handler { let response = match client_request { Err(err) => Some(ServerResponse::Error(err)), - Ok(req) => self.handle_request(req), + Ok(req) => self.handle_request(req).await, }; response.map(|resp| WsMessage::text(resp.into_text())) } - fn handle_binary_message(&mut self, msg: &[u8]) -> Option { + async fn handle_binary_message(&mut self, msg: &[u8]) -> Option { debug!("Handling binary message request"); self.received_response_type = ReceivedResponseType::Binary; @@ -158,19 +169,19 @@ impl Handler { let response = match client_request { Err(err) => Some(ServerResponse::Error(err)), - Ok(req) => self.handle_request(req), + Ok(req) => self.handle_request(req).await, }; response.map(|resp| WsMessage::Binary(resp.into_binary())) } - fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option { + async fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option { // apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore // them and let's test that claim. If that's not the case, just copy code from // old version of this file. match raw_request { - WsMessage::Text(text_message) => self.handle_text_message(text_message), - WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message), + WsMessage::Text(text_message) => self.handle_text_message(text_message).await, + WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message).await, _ => None, } } @@ -232,7 +243,7 @@ impl Handler { break; } - if let Some(response) = self.handle_ws_request(socket_msg) { + if let Some(response) = self.handle_ws_request(socket_msg).await { if let Err(err) = self.send_websocket_response(response).await { warn!( "Failed to send message over websocket: {}. Assuming the connection is dead.", diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index b35d7fd9fe..5f6a261285 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -286,6 +286,7 @@ impl NymClient { buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, closed_connection_tx: ClosedConnectionSender, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { info!("Starting socks5 listener..."); @@ -298,6 +299,7 @@ impl NymClient { authenticator, self.config.get_provider_mix_address(), self.as_mix_recipient(), + lane_queue_lengths, shutdown, ); tokio::spawn(async move { @@ -372,7 +374,7 @@ impl NymClient { let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); // channels responsible for controlling real messages - let (input_sender, input_receiver) = mpsc::unbounded::(); + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); @@ -422,7 +424,7 @@ impl NymClient { input_receiver, sphinx_message_sender.clone(), closed_connection_rx, - shared_lane_queue_lengths, + shared_lane_queue_lengths.clone(), shutdown.subscribe(), ); @@ -442,6 +444,7 @@ impl NymClient { received_buffer_request_sender, input_sender, closed_connection_tx, + shared_lane_queue_lengths, shutdown.subscribe(), ); diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index 5188f7a15d..e77dba2975 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -4,7 +4,7 @@ use super::authentication::{AuthenticationMethods, Authenticator, User}; use super::request::{SocksCommand, SocksRequest}; use super::types::{ResponseCode, SocksProxyError}; use super::{RESERVED, SOCKS_VERSION}; -use client_connections::TransmissionLane; +use client_connections::{LaneQueueLengths, TransmissionLane}; use client_core::client::inbound_messages::{InputMessage, InputMessageSender}; use futures::channel::mpsc; use futures::task::{Context, Poll}; @@ -141,6 +141,7 @@ pub(crate) struct SocksClient { service_provider: Recipient, self_address: Recipient, started_proxy: bool, + lane_queue_lengths: LaneQueueLengths, shutdown_listener: ShutdownListener, } @@ -158,6 +159,7 @@ impl Drop for SocksClient { impl SocksClient { /// Create a new SOCKClient + #[allow(clippy::too_many_arguments)] pub fn new( stream: TcpStream, authenticator: Authenticator, @@ -165,6 +167,7 @@ impl SocksClient { service_provider: Recipient, controller_sender: ControllerSender, self_address: Recipient, + lane_queue_lengths: LaneQueueLengths, shutdown_listener: ShutdownListener, ) -> Self { let connection_id = Self::generate_random(); @@ -179,6 +182,7 @@ impl SocksClient { service_provider, self_address, started_proxy: false, + lane_queue_lengths, shutdown_listener, } } @@ -226,7 +230,7 @@ impl SocksClient { } } - fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { + async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { let req = Request::new_connect(self.connection_id, remote_address, self.self_address); let msg = Message::Request(req); @@ -236,11 +240,14 @@ impl SocksClient { false, TransmissionLane::ConnectionId(self.connection_id), ); - self.input_sender.unbounded_send(input_message).unwrap(); + if self.input_sender.send(input_message).await.is_err() { + panic!(); + } } async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) { - self.send_connect_to_mixnet(remote_proxy_target.clone()); + self.send_connect_to_mixnet(remote_proxy_target.clone()) + .await; let stream = self.stream.run_proxy(); let local_stream_remote = stream @@ -258,6 +265,7 @@ impl SocksClient { conn_receiver, input_sender, connection_id, + Some(self.lane_queue_lengths.clone()), self.shutdown_listener.clone(), ) .run(move |conn_id, read_data, socket_closed| { diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index 11912d0a0d..af5a6f9fed 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -4,7 +4,7 @@ use super::{ mixnet_responses::MixnetResponseListener, types::{ResponseCode, SocksProxyError}, }; -use client_connections::ClosedConnectionSender; +use client_connections::{ClosedConnectionSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, }; @@ -21,6 +21,7 @@ pub struct SphinxSocksServer { listening_address: SocketAddr, service_provider: Recipient, self_address: Recipient, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, } @@ -31,6 +32,7 @@ impl SphinxSocksServer { authenticator: Authenticator, service_provider: Recipient, self_address: Recipient, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) -> Self { // hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can @@ -42,6 +44,7 @@ impl SphinxSocksServer { listening_address: format!("{}:{}", ip, port).parse().unwrap(), service_provider, self_address, + lane_queue_lengths, shutdown, } } @@ -85,6 +88,7 @@ impl SphinxSocksServer { self.service_provider, controller_sender.clone(), self.self_address, + self.lane_queue_lengths.clone(), self.shutdown.clone(), ); diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index 708c2aeb1c..2833598a16 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -19,13 +19,14 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] [dependencies] futures = "0.3" -serde = { version = "1.0", features = ["derive"] } -serde-wasm-bindgen = "0.4" -wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4" js-sys = "0.3" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.4" +tokio = { version = "1.21.2", features = ["sync"] } url = "2.2" +wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4" # internal client-core = { path = "../client-core", default-features = false, features = ["wasm"] } diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index e49d6199db..654989644e 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -326,7 +326,7 @@ impl NymClient { let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); // channels responsible for controlling real messages - let (input_sender, input_receiver) = mpsc::unbounded::(); + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); @@ -395,11 +395,16 @@ impl NymClient { let input_msg = InputMessage::new_fresh(recipient, message, false, lane); - self.input_tx + if self + .input_tx .as_ref() .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap(); + .send(input_msg) + .await + .is_err() + { + panic!(); + } self } diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index 265e21f9fd..d1782e8d90 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -24,7 +24,7 @@ pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver; // The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down // if needed. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct LaneQueueLengths(std::sync::Arc>); impl LaneQueueLengths { @@ -52,6 +52,16 @@ impl LaneQueueLengths { Err(err) => log::warn!("Failed to set lane queue length: {err}"), } } + + pub fn get(&self, lane: &TransmissionLane) -> Option { + match self.0.lock() { + Ok(inner) => inner.get(lane), + Err(err) => { + log::warn!("Failed to get lane queue length: {err}"); + None + } + } + } } impl Default for LaneQueueLengths { @@ -68,6 +78,24 @@ impl std::ops::Deref for LaneQueueLengths { } } +#[derive(Debug)] pub struct LaneQueueLengthsInner { - map: HashMap, + pub map: HashMap, +} + +impl LaneQueueLengthsInner { + pub fn get(&self, lane: &TransmissionLane) -> Option { + self.map.get(lane).copied() + } + + pub fn values(&self) -> impl Iterator { + self.map.values() + } + + pub fn modify(&mut self, lane: &TransmissionLane, f: F) + where + F: FnOnce(&mut usize), + { + self.map.entry(*lane).and_modify(f); + } } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index e116ba1dc5..ef0a09336d 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -5,17 +5,20 @@ use super::MixProxySender; use super::SHUTDOWN_TIMEOUT; use crate::available_reader::AvailableReader; use bytes::Bytes; +use client_connections::LaneQueueLengths; +use client_connections::TransmissionLane; use futures::FutureExt; use futures::StreamExt; use log::*; use ordered_buffer::OrderedMessageSender; use socks5_requests::ConnectionId; +use std::time::Duration; use std::{io, sync::Arc}; use task::ShutdownListener; use tokio::select; use tokio::{net::tcp::OwnedReadHalf, sync::Notify, time::sleep}; -fn send_empty_close( +async fn send_empty_close( connection_id: ConnectionId, message_sender: &mut OrderedMessageSender, mix_sender: &MixProxySender, @@ -24,12 +27,17 @@ fn send_empty_close( F: Fn(ConnectionId, Vec, bool) -> S, { let ordered_msg = message_sender.wrap_message(Vec::new()).into_bytes(); - mix_sender - .unbounded_send(adapter_fn(connection_id, ordered_msg, true)) - .unwrap(); + if mix_sender + .send(adapter_fn(connection_id, ordered_msg, true)) + .await + .is_err() + { + panic!(); + } } -fn deal_with_data( +#[allow(clippy::too_many_arguments)] +async fn deal_with_data( read_data: Option>, local_destination_address: &str, remote_source_address: &str, @@ -37,6 +45,7 @@ fn deal_with_data( message_sender: &mut OrderedMessageSender, mix_sender: &MixProxySender, adapter_fn: F, + lane_queue_lengths: Option, ) -> bool where F: Fn(ConnectionId, Vec, bool) -> S, @@ -67,9 +76,26 @@ where "pushing data down the input sender: size: {}", ordered_msg.len() ); - mix_sender - .unbounded_send(adapter_fn(connection_id, ordered_msg, is_finished)) - .unwrap(); + + // If we are closing the socket, wait until the data has passed `OutQueueControl` and the lane + // is empty, otherwise just wait until we are reasonably close to finish sending as a way to + // throttle the incoming data. + if let Some(lane_queue_lengths) = lane_queue_lengths { + if is_finished { + wait_until_lane_empty(lane_queue_lengths, connection_id).await; + } else { + // We allow a bit of slack when this is not the last msg + wait_until_lane_almost_empty(lane_queue_lengths, connection_id).await; + } + } + + if mix_sender + .send(adapter_fn(connection_id, ordered_msg, is_finished)) + .await + .is_err() + { + panic!(); + } if is_finished { // technically we already informed it when we sent the message to mixnet above @@ -79,6 +105,41 @@ where is_finished } +async fn wait_until_lane_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { + wait_for_lane( + lane_queue_lengths, + connection_id, + 0, + Duration::from_millis(500), + ) + .await +} + +async fn wait_until_lane_almost_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { + wait_for_lane( + lane_queue_lengths, + connection_id, + 10, + Duration::from_millis(100), + ) + .await +} + +async fn wait_for_lane( + lane_queue_lengths: LaneQueueLengths, + connection_id: u64, + queue_length_threshold: usize, + sleep_duration: Duration, +) { + while let Some(queue) = lane_queue_lengths.get(&TransmissionLane::ConnectionId(connection_id)) { + if queue > queue_length_threshold { + sleep(sleep_duration).await; + } else { + break; + } + } +} + #[allow(clippy::too_many_arguments)] pub(super) async fn run_inbound( mut reader: OwnedReadHalf, @@ -88,6 +149,7 @@ pub(super) async fn run_inbound( mix_sender: MixProxySender, adapter_fn: F, shutdown_notify: Arc, + lane_queue_lengths: Option, mut shutdown_listener: ShutdownListener, ) -> OwnedReadHalf where @@ -102,15 +164,27 @@ where loop { select! { read_data = &mut available_reader.next() => { - if deal_with_data(read_data, &local_destination_address, &remote_source_address, connection_id, &mut message_sender, &mix_sender, &adapter_fn) { + if deal_with_data( + read_data, + &local_destination_address, + &remote_source_address, + connection_id, + &mut message_sender, + &mix_sender, + &adapter_fn, + lane_queue_lengths.clone() + ).await { break } } _ = &mut shutdown_future => { - debug!("closing inbound proxy after outbound was closed {:?} ago", SHUTDOWN_TIMEOUT); + debug!( + "closing inbound proxy after outbound was closed {:?} ago", + SHUTDOWN_TIMEOUT + ); // inform remote just in case it was closed because of lack of heartbeat. // worst case the remote will just have couple of false negatives - send_empty_close(connection_id, &mut message_sender, &mix_sender, &adapter_fn); + send_empty_close(connection_id, &mut message_sender, &mix_sender, &adapter_fn).await; break; } _ = shutdown_listener.recv() => { diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index 1d5b493ced..573f09f884 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::connection_controller::ConnectionReceiver; -use futures::channel::mpsc; +use client_connections::LaneQueueLengths; use socks5_requests::ConnectionId; use std::{sync::Arc, time::Duration}; use task::ShutdownListener; @@ -29,7 +29,8 @@ impl From<(Vec, bool)> for ProxyMessage { } } -pub type MixProxySender = mpsc::UnboundedSender; +pub type MixProxySender = tokio::sync::mpsc::Sender; +pub type MixProxyReader = tokio::sync::mpsc::Receiver; // TODO: when we finally get to implementing graceful shutdown, // on Drop this guy should tell the remote that it's closed now @@ -45,6 +46,7 @@ pub struct ProxyRunner { local_destination_address: String, remote_source_address: String, connection_id: ConnectionId, + lane_queue_lengths: Option, // Listens to shutdown commands from higher up shutdown_listener: ShutdownListener, @@ -54,6 +56,7 @@ impl ProxyRunner where S: Send + 'static, { + #[allow(clippy::too_many_arguments)] pub fn new( socket: TcpStream, local_destination_address: String, // addresses are provided for better logging @@ -61,6 +64,7 @@ where mix_receiver: ConnectionReceiver, mix_sender: MixProxySender, connection_id: ConnectionId, + lane_queue_lengths: Option, shutdown_listener: ShutdownListener, ) -> Self { ProxyRunner { @@ -70,6 +74,7 @@ where local_destination_address, remote_source_address, connection_id, + lane_queue_lengths, shutdown_listener, } } @@ -78,7 +83,7 @@ where // request/response as required by entity running particular side of the proxy. pub async fn run(mut self, adapter_fn: F) -> Self where - F: Fn(ConnectionId, Vec, bool) -> S + Send + 'static, + F: Fn(ConnectionId, Vec, bool) -> S + Send + Sync + 'static, { let (read_half, write_half) = self.socket.take().unwrap().into_split(); let shutdown_notify = Arc::new(Notify::new()); @@ -92,6 +97,7 @@ where self.mix_sender.clone(), adapter_fn, Arc::clone(&shutdown_notify), + self.lane_queue_lengths.clone(), self.shutdown_listener.clone(), ); diff --git a/service-providers/network-requester/src/connection.rs b/service-providers/network-requester/src/connection.rs index 5c5fea04b4..6627fdbf4b 100644 --- a/service-providers/network-requester/src/connection.rs +++ b/service-providers/network-requester/src/connection.rs @@ -1,10 +1,9 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use futures::channel::mpsc; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::ConnectionReceiver; -use proxy_helpers::proxy_runner::ProxyRunner; +use proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner}; use socks5_requests::{ConnectionId, Message as Socks5Message, RemoteAddress, Response}; use std::io; use task::ShutdownListener; @@ -40,7 +39,7 @@ impl Connection { pub(crate) async fn run_proxy( &mut self, mix_receiver: ConnectionReceiver, - mix_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_sender: MixProxySender<(Socks5Message, Recipient)>, shutdown: ShutdownListener, ) { let stream = self.conn.take().unwrap(); @@ -54,6 +53,7 @@ impl Connection { mix_receiver, mix_sender, connection_id, + None, shutdown, ) .run(move |conn_id, read_data, socket_closed| { diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index c9333d6bf6..88d66d159d 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -15,6 +15,7 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::receiver::ReconstructedMessage; use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender}; +use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender}; use socks5_requests::{ ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response, }; @@ -67,14 +68,14 @@ impl ServiceProvider { /// via the `websocket_writer`. async fn mixnet_response_listener( mut websocket_writer: SplitSink, - mut mix_reader: mpsc::UnboundedReceiver<(Socks5Message, Recipient)>, + mut mix_reader: MixProxyReader<(Socks5Message, Recipient)>, stats_collector: Option, mut closed_connection_rx: ClosedConnectionReceiver, ) { loop { tokio::select! { // TODO: wire SURBs in here once they're available - socks5_msg = mix_reader.next() => { + socks5_msg = mix_reader.recv() => { if let Some((msg, return_address)) = socks5_msg { if let Some(stats_collector) = stats_collector.as_ref() { if let Some(remote_addr) = stats_collector @@ -153,7 +154,7 @@ impl ServiceProvider { remote_addr: String, return_address: Recipient, controller_sender: ControllerSender, - mix_input_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, shutdown: ShutdownListener, ) { let mut conn = match Connection::new(conn_id, remote_addr.clone(), return_address).await { @@ -166,12 +167,16 @@ impl ServiceProvider { ); // inform the remote that the connection is closed before it even was established - mix_input_sender - .unbounded_send(( + if mix_input_sender + .send(( Socks5Message::Response(Response::new(conn_id, Vec::new(), true)), return_address, )) - .unwrap(); + .await + .is_err() + { + panic!(); + } return; } @@ -207,10 +212,10 @@ impl ServiceProvider { ); } - fn handle_proxy_connect( + async fn handle_proxy_connect( &mut self, controller_sender: &mut ControllerSender, - mix_input_sender: &mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, conn_id: ConnectionId, remote_addr: String, return_address: Recipient, @@ -219,14 +224,18 @@ impl ServiceProvider { if !self.open_proxy && !self.outbound_request_filter.check(&remote_addr) { let log_msg = format!("Domain {:?} failed filter check", remote_addr); log::info!("{}", log_msg); - mix_input_sender - .unbounded_send(( + if mix_input_sender + .send(( Socks5Message::NetworkRequesterResponse(NetworkRequesterResponse::new( conn_id, log_msg, )), return_address, )) - .unwrap(); + .await + .is_err() + { + panic!(); + } return; } @@ -263,7 +272,7 @@ impl ServiceProvider { &mut self, raw_request: &[u8], controller_sender: &mut ControllerSender, - mix_input_sender: &mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, stats_collector: Option, shutdown: ShutdownListener, ) { @@ -292,6 +301,7 @@ impl ServiceProvider { req.return_address, shutdown, ) + .await } Request::Send(conn_id, data, closed) => { @@ -326,7 +336,7 @@ impl ServiceProvider { // channels responsible for managing messages that are to be sent to the mix network. The receiver is // going to be used by `mixnet_response_listener` let (mix_input_sender, mix_input_receiver) = - mpsc::unbounded::<(Socks5Message, Recipient)>(); + tokio::sync::mpsc::channel::<(Socks5Message, Recipient)>(1); // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). let shutdown = task::ShutdownNotifier::default(); diff --git a/service-providers/network-requester/src/statistics/collector.rs b/service-providers/network-requester/src/statistics/collector.rs index 96e7292d82..72fc276860 100644 --- a/service-providers/network-requester/src/statistics/collector.rs +++ b/service-providers/network-requester/src/statistics/collector.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -use futures::channel::mpsc; use log::*; +use proxy_helpers::proxy_runner::MixProxySender; use rand::RngCore; use serde::Deserialize; use sqlx::types::chrono::{DateTime, Utc}; @@ -77,13 +77,13 @@ pub struct ServiceStatisticsCollector { pub(crate) response_stats_data: Arc>, pub(crate) connected_services: Arc>>, stats_provider_addr: Recipient, - mix_input_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, } impl ServiceStatisticsCollector { pub async fn new( stats_provider_addr: Option, - mix_input_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, ) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) @@ -175,20 +175,30 @@ impl StatisticsCollector for ServiceStatisticsCollector { ), self.stats_provider_addr, ); - self.mix_input_sender - .unbounded_send(( + if self + .mix_input_sender + .send(( Socks5Message::Request(connect_req), self.stats_provider_addr, )) - .unwrap(); + .await + .is_err() + { + panic!(); + } trace!("Sending data to statistics service"); let mut message_sender = OrderedMessageSender::new(); let ordered_msg = message_sender.wrap_message(msg).into_bytes(); let send_req = Request::new_send(conn_id, ordered_msg, true); - self.mix_input_sender - .unbounded_send((Socks5Message::Request(send_req), self.stats_provider_addr)) - .unwrap(); + if self + .mix_input_sender + .send((Socks5Message::Request(send_req), self.stats_provider_addr)) + .await + .is_err() + { + panic!(); + } Ok(()) } From d82fd620adddd0115b5ecdadf1a5b02354ac2383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 22 Nov 2022 01:09:12 +0100 Subject: [PATCH 31/75] Update blake2 and ed25519 deps (#1762) --- Cargo.lock | 4 ++-- contracts/Cargo.lock | 4 ++-- nym-wallet/Cargo.lock | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5c3f743cb7..5b7abc2685 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,9 +1475,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "serde", "signature", diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 810da4ac89..b32e8092f6 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -595,9 +595,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed12bbf7b5312f8da1c2722bc06d8c6b12c2d86a7fb35a194c7f3e6fc2bbe39" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "signature", ] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index b8e93eb289..895eab810c 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -258,9 +258,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blake2" -version = "0.10.4" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +checksum = "b12e5fd123190ce1c2e559308a94c9bacad77907d4c6005d9e58fe1a0689e55e" dependencies = [ "digest 0.10.3", ] @@ -1183,9 +1183,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed12bbf7b5312f8da1c2722bc06d8c6b12c2d86a7fb35a194c7f3e6fc2bbe39" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "signature", ] From 9063a86d26466f8dddf849df7b4b747367ba1720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 22 Nov 2022 01:15:50 +0100 Subject: [PATCH 32/75] client: log and handle error when cant load reply key storage (#1785) * client: log and handle error when cant load reply key storage * clippy --- Cargo.lock | 2 ++ clients/client-core/src/client/reply_key_storage.rs | 5 ++++- clients/native/Cargo.toml | 1 + clients/native/src/client/mod.rs | 6 +++++- clients/native/src/error.rs | 4 +++- clients/socks5/Cargo.toml | 1 + clients/socks5/src/client/mod.rs | 6 +++++- clients/socks5/src/error.rs | 4 +++- 8 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b7abc2685..9012b80f10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3169,6 +3169,7 @@ dependencies = [ "serde", "serde_json", "sled", + "tap", "task", "thiserror", "tokio", @@ -3348,6 +3349,7 @@ dependencies = [ "serde", "snafu 0.6.10", "socks5-requests", + "tap", "task", "thiserror", "tokio", diff --git a/clients/client-core/src/client/reply_key_storage.rs b/clients/client-core/src/client/reply_key_storage.rs index a717f44ea2..bfe1c43b9b 100644 --- a/clients/client-core/src/client/reply_key_storage.rs +++ b/clients/client-core/src/client/reply_key_storage.rs @@ -8,10 +8,13 @@ use nymsphinx::anonymous_replies::{ }; use std::path::Path; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ReplyKeyStorageError { + #[error("DB Read Error: {0}")] DbReadError(sled::Error), + #[error("DB Write Error: {0}")] DbWriteError(sled::Error), + #[error("DB Open Error: {0}")] DbOpenError(sled::Error), } diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 5da490d504..4f4cca6a85 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -51,6 +51,7 @@ topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } websocket-requests = { path = "websocket-requests" } +tap = "1.0.1" [features] coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"] diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index ecc057e62b..dba7d6b47d 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -34,6 +34,7 @@ use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use nymsphinx::anonymous_replies::ReplySurb; use nymsphinx::receiver::ReconstructedMessage; +use tap::TapFallible; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; use crate::client::config::{Config, SocketType}; @@ -405,7 +406,10 @@ impl NymClient { let reply_key_storage = ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) - .expect("Failed to load reply key storage!"); + .tap_err(|err| { + log::error!("Failed to load reply key storage - is it perhaps already in use?"); + log::error!("{}", err); + })?; // Shutdown notifier for signalling tasks to stop let shutdown = ShutdownNotifier::default(); diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index a32a44d3b6..6ffdc86e99 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,4 +1,4 @@ -use client_core::error::ClientCoreError; +use client_core::{client::reply_key_storage::ReplyKeyStorageError, error::ClientCoreError}; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; @@ -15,6 +15,8 @@ pub enum ClientError { ValidatorClientError(#[from] ValidatorClientError), #[error("client-core error: {0}")] ClientCoreError(#[from] ClientCoreError), + #[error("Reply key storage error: {0}")] + ReplyKeyStorageError(#[from] ReplyKeyStorageError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index ee103e7fda..859bacfe62 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -46,6 +46,7 @@ task = { path = "../../common/task" } topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } +tap = "1.0.1" [features] coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"] diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 5f6a261285..863f2252c2 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -37,6 +37,7 @@ use gateway_client::{ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; +use tap::TapFallible; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; pub mod config; @@ -382,7 +383,10 @@ impl NymClient { let reply_key_storage = ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) - .expect("Failed to load reply key storage!"); + .tap_err(|err| { + log::error!("Failed to load reply key storage - is it perhaps already in use?"); + log::error!("{}", err); + })?; // Shutdown notifier for signalling tasks to stop let shutdown = ShutdownNotifier::default(); diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index 8037d7f23e..4ebca0e2e5 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,4 +1,4 @@ -use client_core::error::ClientCoreError; +use client_core::{client::reply_key_storage::ReplyKeyStorageError, error::ClientCoreError}; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; @@ -15,6 +15,8 @@ pub enum Socks5ClientError { ValidatorClientError(#[from] ValidatorClientError), #[error("client-core error: {0}")] ClientCoreError(#[from] ClientCoreError), + #[error("Reply key storage error: {0}")] + ReplyKeyStorageError(#[from] ReplyKeyStorageError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), From f1deebc0f11c5f351204eaccb03eaaff9fd15f7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 22 Nov 2022 09:28:57 +0100 Subject: [PATCH 33/75] ci: check formatting first, and add all targets to coconut clippy step --- .github/workflows/build.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0777368574..ad1f3d4d54 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,12 @@ jobs: override: true components: rustfmt, clippy + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Build all binaries uses: actions-rs/cargo@v1 with: @@ -48,12 +54,6 @@ jobs: command: test args: --workspace --all-features -- --ignored - - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - - uses: actions-rs/clippy-check@v1 name: Clippy checks with: @@ -66,6 +66,8 @@ jobs: command: clippy args: --workspace -- -D warnings + # COCONUT stuff + - name: Build all binaries with coconut enabled uses: actions-rs/cargo@v1 with: @@ -82,4 +84,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --features=coconut -- -D warnings + args: --all-targets --features=coconut -- -D warnings From 2db1bc8efa4ddb980050215e4dab67d0a1295ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Tue, 22 Nov 2022 11:16:02 +0200 Subject: [PATCH 34/75] Feature/dkg publish vk (#1747) * Save to disk coconut keypair * Check verification keys of the other signers * Post verification key to chain * Add multisig propose/vote for vks * Execute the proposal * Parse announce address argument * Gateway uses chain data * Network requester uses chain data * Native&socks5 clients use chain data * Credential client signature uses chain data * Remove redundant api endpoints * Undo debugging logging * Fix some tests * Fix clippy * Fix wasm client and contract test * More contract clippy * Update CHANGELOG * Use a bigger expiry period then the testing one --- CHANGELOG.md | 7 + Cargo.lock | 49 ++++-- .../src/client/topology_control.rs | 6 +- clients/client-core/src/config/mod.rs | 12 ++ clients/client-core/src/init.rs | 2 +- clients/credential/src/commands.rs | 21 ++- clients/credential/src/error.rs | 4 + clients/native/src/client/config/template.rs | 7 + clients/native/src/client/mod.rs | 21 ++- clients/native/src/commands/init.rs | 11 +- clients/native/src/commands/mod.rs | 16 +- clients/native/src/commands/run.rs | 11 +- clients/socks5/src/client/config/template.rs | 7 + clients/socks5/src/client/mod.rs | 21 ++- clients/socks5/src/commands/init.rs | 11 +- clients/socks5/src/commands/mod.rs | 14 +- clients/socks5/src/commands/run.rs | 11 +- clients/webassembly/src/gateway_selector.rs | 2 +- .../gateway-client/src/bandwidth.rs | 10 +- .../client-libs/validator-client/Cargo.toml | 3 +- .../validator-client/src/client.rs | 141 ++++++++++++++--- .../client-libs/validator-client/src/lib.rs | 3 +- .../src/nymd/traits/dkg_query_client.rs | 20 +++ .../src/nymd/traits/dkg_signing_client.rs | 29 ++++ .../src/nymd/traits/multisig_query_client.rs | 19 ++- .../nymd/traits/multisig_signing_client.rs | 2 +- .../validator-client/src/validator_api/mod.rs | 32 +--- .../src/validator_api/routes.rs | 2 - .../src/event_attributes.rs | 2 +- .../coconut-dkg/Cargo.toml | 4 +- .../coconut-dkg/src/dealer.rs | 1 + .../coconut-dkg/src/event_attributes.rs | 1 + .../coconut-dkg/src/lib.rs | 1 + .../coconut-dkg/src/msg.rs | 18 ++- .../coconut-dkg/src/types.rs | 8 +- .../coconut-dkg/src/verification_key.rs | 70 +++++++++ .../multisig-contract/Cargo.toml | 7 +- .../multisig-contract/src/msg.rs | 3 +- common/credentials/Cargo.toml | 1 - common/credentials/src/coconut/utils.rs | 86 ++++------- common/crypto/dkg/src/dealing.rs | 26 +++- common/crypto/dkg/tests/integration.rs | 19 ++- common/nymcoconut/Cargo.toml | 4 +- common/nymcoconut/src/lib.rs | 1 + common/nymcoconut/src/scheme/keygen.rs | 50 ++++++ common/nymcoconut/src/scheme/verification.rs | 42 +++++ common/nymcoconut/src/tests/helpers.rs | 31 ++-- contracts/Cargo.lock | 20 ++- contracts/coconut-dkg/src/constants.rs | 2 + .../coconut-dkg/src/dealers/transactions.rs | 2 + .../coconut-dkg/src/dealings/transactions.rs | 6 +- contracts/coconut-dkg/src/error.rs | 7 +- contracts/coconut-dkg/src/lib.rs | 30 +++- contracts/coconut-dkg/src/state/mod.rs | 3 + .../src/verification_key_shares/mod.rs | 6 + .../src/verification_key_shares/queries.rs | 38 +++++ .../src/verification_key_shares/storage.rs | 15 ++ .../verification_key_shares/transactions.rs | 71 +++++++++ contracts/coconut-test/Cargo.toml | 5 +- contracts/coconut-test/src/helpers.rs | 14 ++ .../src/spend_credential_creates_proposal.rs | 3 + .../src/submit_vk_creates_proposal.rs | 142 +++++++++++++++++ contracts/coconut-test/src/tests.rs | 1 + .../multisig/cw3-flex-multisig/Cargo.toml | 16 +- .../cw3-flex-multisig/src/contract.rs | 14 +- .../multisig/cw3-flex-multisig/src/state.rs | 1 + contracts/multisig/cw4-group/Cargo.toml | 12 +- .../websocket/connection_handler/coconut.rs | 39 ++--- gateway/src/node/mod.rs | 59 ++++---- validator-api/Cargo.toml | 6 +- validator-api/src/coconut/client.rs | 17 ++- validator-api/src/coconut/comm.rs | 19 ++- validator-api/src/coconut/dkg/client.rs | 76 +++++++++- validator-api/src/coconut/dkg/complaints.rs | 35 ----- validator-api/src/coconut/dkg/controller.rs | 50 ++++-- validator-api/src/coconut/dkg/dealing.rs | 3 +- validator-api/src/coconut/dkg/public_key.rs | 6 +- validator-api/src/coconut/dkg/state.rs | 84 +++++++++- .../src/coconut/dkg/verification_key.rs | 143 +++++++++++++++--- validator-api/src/coconut/error.rs | 6 + validator-api/src/coconut/mod.rs | 25 +-- validator-api/src/coconut/tests.rs | 141 ++++------------- validator-api/src/config/mod.rs | 86 +++++++---- validator-api/src/config/template.rs | 21 ++- validator-api/src/main.rs | 55 +++---- validator-api/src/network_monitor/mod.rs | 22 ++- .../src/network_monitor/monitor/sender.rs | 11 -- validator-api/src/nymd_client.rs | 45 +++++- 88 files changed, 1623 insertions(+), 603 deletions(-) create mode 100644 common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs create mode 100644 contracts/coconut-dkg/src/verification_key_shares/mod.rs create mode 100644 contracts/coconut-dkg/src/verification_key_shares/queries.rs create mode 100644 contracts/coconut-dkg/src/verification_key_shares/storage.rs create mode 100644 contracts/coconut-dkg/src/verification_key_shares/transactions.rs create mode 100644 contracts/coconut-test/src/submit_vk_creates_proposal.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cd8056355..de700926d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,20 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - binaries: add `-c` shortform for `--config-env-file` - websocket-requests: add server response signalling current packet queue length in the client +- contracts: DKG contract that handles coconut key generation ([#1678][#1708][#1747]) +- validator-api: generate coconut keys interactively, using DKG and multisig contracts ([#1678][#1708][#1747]) ### Changed - clients: add concept of transmission lanes to better handle multiple data streams ([#1720]) +- clients,validator-api: take coconut signers from the chain instead of specifying them via CLI ([#1747]) +- multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747]) - socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) +[#1678]: https://github.com/nymtech/nym/pull/1678 +[#1708]: https://github.com/nymtech/nym/pull/1708 [#1720]: https://github.com/nymtech/nym/pull/1720 +[#1747]: https://github.com/nymtech/nym/pull/1747 [#1783]: https://github.com/nymtech/nym/pull/1783 diff --git a/Cargo.lock b/Cargo.lock index 9012b80f10..4303004d95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,6 +642,8 @@ version = "0.1.0" dependencies = [ "contracts-common", "cosmwasm-std", + "cw-utils", + "multisig-contract-common", "schemars", "serde", ] @@ -963,7 +965,6 @@ dependencies = [ "crypto", "rand 0.7.3", "thiserror", - "url", "validator-api-requests", "validator-client", ] @@ -1242,9 +1243,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", @@ -1253,10 +1254,22 @@ dependencies = [ ] [[package]] -name = "cw3" -version = "0.13.2" +name = "cw2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f871854338a54c7bb094d16ffe17212b93b146d9659dbce4c9402a9b77e240ef" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" dependencies = [ "cosmwasm-std", "cw-utils", @@ -1265,10 +1278,26 @@ dependencies = [ ] [[package]] -name = "cw4" -version = "0.13.2" +name = "cw3-fixed-multisig" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4476d6a7c13c46ed9ff260bd0e1cf648dc37b13f483822e1ff2a431f0f6ee52" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -2952,6 +2981,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -3485,6 +3515,7 @@ dependencies = [ "getrandom 0.2.6", "group 0.11.0", "itertools", + "pemstore", "rand 0.8.5", "rand_chacha 0.3.1", "serde", diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index 3b0fc4afc3..afedc7ad61 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -138,7 +138,7 @@ impl TopologyRefresherConfig { } pub struct TopologyRefresher { - validator_client: validator_client::ApiClient, + validator_client: validator_client::client::ApiClient, client_version: String, validator_api_urls: Vec, @@ -154,7 +154,9 @@ impl TopologyRefresher { cfg.validator_api_urls.shuffle(&mut thread_rng()); TopologyRefresher { - validator_client: validator_client::ApiClient::new(cfg.validator_api_urls[0].clone()), + validator_client: validator_client::client::ApiClient::new( + cfg.validator_api_urls[0].clone(), + ), client_version: cfg.client_version, validator_api_urls: cfg.validator_api_urls, topology_accessor, diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 678b70ef54..4a3d1c1273 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -125,6 +125,10 @@ impl Config { self.client.gateway_endpoint.gateway_id = id.into(); } + pub fn set_custom_validators(&mut self, validator_urls: Vec) { + self.client.validator_urls = validator_urls; + } + pub fn set_custom_validator_apis(&mut self, validator_api_urls: Vec) { self.client.validator_api_urls = validator_api_urls; } @@ -179,6 +183,10 @@ impl Config { self.client.ack_key_file.clone() } + pub fn get_validator_endpoints(&self) -> Vec { + self.client.validator_urls.clone() + } + pub fn get_validator_api_endpoints(&self) -> Vec { self.client.validator_api_urls.clone() } @@ -306,6 +314,9 @@ pub struct Client { #[serde(default)] disabled_credentials_mode: bool, + /// Addresses to nymd validators via which the client can communicate with the chain. + validator_urls: Vec, + /// Addresses to APIs running on validator from which the client gets the view of the network. validator_api_urls: Vec, @@ -354,6 +365,7 @@ impl Default for Client { version: env!("CARGO_PKG_VERSION").to_string(), id: "".to_string(), disabled_credentials_mode: true, + validator_urls: vec![], validator_api_urls: vec![], private_identity_key_file: Default::default(), public_identity_key_file: Default::default(), diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init.rs index 98cc42df64..be9758d363 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init.rs @@ -31,7 +31,7 @@ pub async fn query_gateway_details( let validator_api = validator_servers .choose(&mut thread_rng()) .ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?; - let validator_client = validator_client::ApiClient::new(validator_api.clone()); + let validator_client = validator_client::client::ApiClient::new(validator_api.clone()); log::trace!("Fetching list of gateways from: {}", validator_api); let gateways = validator_client.get_cached_gateways().await?; diff --git a/clients/credential/src/commands.rs b/clients/credential/src/commands.rs index c281332810..7f9d3ae2e1 100644 --- a/clients/credential/src/commands.rs +++ b/clients/credential/src/commands.rs @@ -14,8 +14,9 @@ use credential_storage::PersistentStorage; use credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES}; use credentials::coconut::utils::obtain_aggregate_signature; use crypto::asymmetric::{encryption, identity}; -use network_defaults::VOUCHER_INFO; +use network_defaults::{NymNetworkDetails, VOUCHER_INFO}; use validator_client::nymd::tx::Hash; +use validator_client::{CoconutApiClient, Config}; use crate::client::Client; use crate::error::{CredentialClientError, Result}; @@ -107,10 +108,9 @@ pub(crate) struct GetCredential { /// The hash of a successful deposit transaction #[clap(long)] tx_hash: String, - /// The URLs to the validator-api endpoints the are run as coconut signer authorities, separated - /// by comma (,) + /// The nymd URL that should be used #[clap(long)] - signer_authorities: String, + nymd_url: String, /// If we want to get the signature without attaching a blind sign request; it is expected that /// there is already a signature stored on the signer #[clap(long, parse(from_flag))] @@ -124,7 +124,10 @@ impl Execute for GetCredential { .get::(&self.tx_hash) .ok_or(CredentialClientError::NoDeposit)?; - let urls = config::parse_validators(&self.signer_authorities); + let network_details = NymNetworkDetails::new_from_env(); + let config = Config::try_from_nym_network_details(&network_details)?; + let client = validator_client::Client::new_query(config)?; + let coconut_api_clients = CoconutApiClient::all_coconut_api_clients(&client).await?; let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap(); let bandwidth_credential_attributes = if self.__no_request { @@ -178,8 +181,12 @@ impl Execute for GetCredential { )?); db.set(&self.tx_hash, &state).unwrap(); - let signature = - obtain_aggregate_signature(¶ms, &bandwidth_credential_attributes, &urls).await?; + let signature = obtain_aggregate_signature( + ¶ms, + &bandwidth_credential_attributes, + &coconut_api_clients, + ) + .await?; shared_storage .insert_coconut_credential( state.amount.to_string(), diff --git a/clients/credential/src/error.rs b/clients/credential/src/error.rs index 830e4d9e16..08bd0001e9 100644 --- a/clients/credential/src/error.rs +++ b/clients/credential/src/error.rs @@ -8,6 +8,7 @@ use credentials::error::Error as CredentialError; use crypto::asymmetric::encryption::KeyRecoveryError; use crypto::asymmetric::identity::Ed25519RecoveryError; use validator_client::nymd::error::NymdError; +use validator_client::ValidatorClientError; pub type Result = std::result::Result; @@ -16,6 +17,9 @@ pub enum CredentialClientError { #[error("Nymd error: {0}")] Nymd(#[from] NymdError), + #[error("Validator client error: {0}")] + ValidatorClientError(#[from] ValidatorClientError), + #[error("Credential error: {0}")] Credential(#[from] CredentialError), diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index ca25e6c794..c327d286ff 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -23,6 +23,13 @@ id = '{{ client.id }}' # to claim bandwidth without presenting bandwidth credentials. disabled_credentials_mode = {{ client.disabled_credentials_mode }} +# Addresses to nymd validators via which the client can communicate with the chain. +validator_urls = [ + {{#each client.validator_urls }} + '{{this}}', + {{/each}} +] + # Addresses to APIs running on validator from which the client gets the view of the network. validator_api_urls = [ {{#each client.validator_api_urls }} diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index dba7d6b47d..357293d17e 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -201,11 +201,22 @@ impl NymClient { .expect("provided gateway id is invalid!"); #[cfg(feature = "coconut")] - let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, - self.config.get_base().get_validator_api_endpoints(), - ); + let bandwidth_controller = { + let details = network_defaults::NymNetworkDetails::new_from_env(); + let client_config = validator_client::Config::try_from_nym_network_details(&details) + .expect("failed to construct validator client config"); + let client = validator_client::Client::new_query(client_config) + .expect("Could not construct query client"); + let coconut_api_clients = + validator_client::CoconutApiClient::all_coconut_api_clients(&client) + .await + .expect("Could not query api clients"); + BandwidthController::new( + credential_storage::initialise_storage(self.config.get_base().get_database_path()) + .await, + coconut_api_clients, + ) + }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_base().get_database_path()) diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 208a026959..6a38770eec 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -25,9 +25,13 @@ pub(crate) struct Init { #[clap(long)] force_register_gateway: bool, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Whether to not start the websocket #[clap(long)] @@ -52,7 +56,8 @@ pub(crate) struct Init { impl From for OverrideConfig { fn from(init_config: Init) -> Self { OverrideConfig { - validators: init_config.validators, + nymd_validators: init_config.nymd_validators, + api_validators: init_config.api_validators, disable_socket: init_config.disable_socket, port: init_config.port, fastmode: init_config.fastmode, diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 08323a6454..648a1d6002 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -75,7 +75,8 @@ pub(crate) enum Commands { // Configuration that can be overridden. pub(crate) struct OverrideConfig { - validators: Option, + nymd_validators: Option, + api_validators: Option, disable_socket: bool, port: Option, fastmode: bool, @@ -98,7 +99,18 @@ pub(crate) async fn execute(args: &Cli) -> Result<(), ClientError> { } pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { - if let Some(raw_validators) = args.validators { + if let Some(raw_validators) = args.nymd_validators { + config + .get_base_mut() + .set_custom_validators(config::parse_validators(&raw_validators)); + } else if std::env::var(network_defaults::var_names::CONFIGURED).is_ok() { + let raw_validators = std::env::var(network_defaults::var_names::NYMD_VALIDATOR) + .expect("nymd validator not set"); + config + .get_base_mut() + .set_custom_validators(config::parse_validators(&raw_validators)); + } + if let Some(raw_validators) = args.api_validators { config .get_base_mut() .set_custom_validator_apis(config::parse_validators(&raw_validators)); diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index 7332f81448..d5c892fadc 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -18,9 +18,13 @@ pub(crate) struct Run { #[clap(long)] id: String, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Id of the gateway we want to connect to. If overridden, it is user's responsibility to /// ensure prior registration happened @@ -45,7 +49,8 @@ pub(crate) struct Run { impl From for OverrideConfig { fn from(run_config: Run) -> Self { OverrideConfig { - validators: run_config.validators, + nymd_validators: run_config.nymd_validators, + api_validators: run_config.api_validators, disable_socket: run_config.disable_socket, port: run_config.port, fastmode: false, diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 4168d44e85..11d3185f56 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -23,6 +23,13 @@ id = '{{ client.id }}' # to claim bandwidth without presenting bandwidth credentials. disabled_credentials_mode = {{ client.disabled_credentials_mode }} +# Addresses to nymd validators via which the client can communicate with the chain. +validator_urls = [ + {{#each client.validator_urls }} + '{{this}}', + {{/each}} +] + # Addresses to APIs running on validator from which the client gets the view of the network. validator_api_urls = [ {{#each client.validator_api_urls }} diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 863f2252c2..ef07360e2b 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -199,11 +199,22 @@ impl NymClient { .expect("provided gateway id is invalid!"); #[cfg(feature = "coconut")] - let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, - self.config.get_base().get_validator_api_endpoints(), - ); + let bandwidth_controller = { + let details = network_defaults::NymNetworkDetails::new_from_env(); + let client_config = validator_client::Config::try_from_nym_network_details(&details) + .expect("failed to construct validator client config"); + let client = validator_client::Client::new_query(client_config) + .expect("Could not construct query client"); + let coconut_api_clients = + validator_client::CoconutApiClient::all_coconut_api_clients(&client) + .await + .expect("Could not query api clients"); + BandwidthController::new( + credential_storage::initialise_storage(self.config.get_base().get_database_path()) + .await, + coconut_api_clients, + ) + }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_base().get_database_path()) diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index fb93e4e30c..58b10002c4 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -29,9 +29,13 @@ pub(crate) struct Init { #[clap(long)] force_register_gateway: bool, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Port for the socket to listen on in all subsequent runs #[clap(short, long)] @@ -52,7 +56,8 @@ pub(crate) struct Init { impl From for OverrideConfig { fn from(init_config: Init) -> Self { OverrideConfig { - validators: init_config.validators, + nymd_validators: init_config.nymd_validators, + api_validators: init_config.api_validators, port: init_config.port, fastmode: init_config.fastmode, #[cfg(feature = "coconut")] diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 192e905b62..b069c13542 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -78,7 +78,8 @@ pub(crate) enum Commands { // Configuration that can be overridden. pub(crate) struct OverrideConfig { - validators: Option, + nymd_validators: Option, + api_validators: Option, port: Option, fastmode: bool, @@ -100,7 +101,16 @@ pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> { } pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { - if let Some(raw_validators) = args.validators { + if let Some(raw_validators) = args.nymd_validators { + config + .get_base_mut() + .set_custom_validators(parse_validators(&raw_validators)); + } else if let Ok(raw_validators) = std::env::var(network_defaults::var_names::NYMD_VALIDATOR) { + config + .get_base_mut() + .set_custom_validators(parse_validators(&raw_validators)); + } + if let Some(raw_validators) = args.api_validators { config .get_base_mut() .set_custom_validator_apis(parse_validators(&raw_validators)); diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index af7a22b078..406ae72d94 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -31,9 +31,13 @@ pub(crate) struct Run { #[clap(long)] gateway: Option, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Port for the socket to listen on #[clap(short, long)] @@ -49,7 +53,8 @@ pub(crate) struct Run { impl From for OverrideConfig { fn from(run_config: Run) -> Self { OverrideConfig { - validators: run_config.validators, + nymd_validators: run_config.nymd_validators, + api_validators: run_config.api_validators, port: run_config.port, fastmode: false, diff --git a/clients/webassembly/src/gateway_selector.rs b/clients/webassembly/src/gateway_selector.rs index f20ca90fbb..7e15252b8a 100644 --- a/clients/webassembly/src/gateway_selector.rs +++ b/clients/webassembly/src/gateway_selector.rs @@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub async fn get_gateway(api_server: String, preferred: Option) -> GatewayEndpoint { - let validator_client = validator_client::ApiClient::new(api_server.parse().unwrap()); + let validator_client = validator_client::client::ApiClient::new(api_server.parse().unwrap()); let gateways = match validator_client.get_cached_gateways().await { Err(err) => panic!("failed to obtain list of all gateways - {}", err), diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 77916876f1..6f3b7722dc 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -17,6 +17,8 @@ use credential_storage::error::StorageError; #[cfg(feature = "coconut")] use std::str::FromStr; #[cfg(feature = "coconut")] +use validator_client::client::CoconutApiClient; +#[cfg(feature = "coconut")] use { coconut_interface::Base58, credentials::coconut::{ @@ -29,7 +31,7 @@ pub struct BandwidthController { #[allow(dead_code)] storage: St, #[cfg(feature = "coconut")] - validator_endpoints: Vec, + coconut_api_clients: Vec, } impl BandwidthController @@ -37,10 +39,10 @@ where St: Storage + Clone + 'static, { #[cfg(feature = "coconut")] - pub fn new(storage: St, validator_endpoints: Vec) -> Self { + pub fn new(storage: St, coconut_api_clients: Vec) -> Self { BandwidthController { storage, - validator_endpoints, + coconut_api_clients, } } @@ -53,7 +55,7 @@ where pub async fn prepare_coconut_credential( &self, ) -> Result { - let verification_key = obtain_aggregate_verification_key(&self.validator_endpoints).await?; + let verification_key = obtain_aggregate_verification_key(&self.coconut_api_clients).await?; let bandwidth_credential = self.storage.get_next_coconut_credential().await?; let voucher_value = u64::from_str(&bandwidth_credential.voucher_value) .map_err(|_| StorageError::InconsistentData)?; diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index bd9bb6ef80..ec842828e7 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -10,7 +10,6 @@ rust-version = "1.56" [dependencies] base64 = "0.13" colored = "2.0" -cw3 = "0.13.1" coconut-dkg-common = { path = "../../cosmwasm-smart-contracts/coconut-dkg" } contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } @@ -39,6 +38,7 @@ async-trait = { version = "0.1.51", optional = true } bip39 = { version = "1", features = ["rand"], optional = true } config = { path = "../../config", optional = true } cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32", "cosmwasm"], optional = true} +cw3 = { version = "0.13.4", optional = true } prost = { version = "0.10", default-features = false, optional = true } flate2 = { version = "1.0.20", optional = true } sha2 = { version = "0.9.5", optional = true } @@ -55,6 +55,7 @@ nymd-client = [ "bip39", "config", "cosmrs", + "cw3", "prost", "flate2", "sha2", diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 59e9841d78..8ddce33a4f 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -2,15 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{validator_api, ValidatorClientError}; -use coconut_dkg_common::dealer::ContractDealing; -use coconut_dkg_common::types::DealerDetails; +use coconut_dkg_common::types::NodeIndex; +#[cfg(feature = "nymd-client")] +use coconut_dkg_common::{ + dealer::ContractDealing, types::DealerDetails, verification_key::ContractVKShare, +}; +#[cfg(feature = "nymd-client")] +use coconut_interface::Base58; +use coconut_interface::VerificationKey; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::MixId; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; -use url::Url; +#[cfg(feature = "nymd-client")] +use std::str::FromStr; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, @@ -18,10 +24,12 @@ use validator_api_requests::models::{ }; #[cfg(feature = "nymd-client")] -use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient}; +use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient}; #[cfg(feature = "nymd-client")] use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] +use cw3::ProposalResponse; +#[cfg(feature = "nymd-client")] use mixnet_contract_common::{ mixnode::MixNodeBond, pending_events::{PendingEpochEvent, PendingIntervalEvent}, @@ -29,6 +37,7 @@ use mixnet_contract_common::{ }; #[cfg(feature = "nymd-client")] use network_defaults::NymNetworkDetails; +use url::Url; #[cfg(feature = "nymd-client")] use validator_api_requests::models::MixNodeBondAnnotated; @@ -46,6 +55,8 @@ pub struct Config { mixnode_delegations_page_limit: Option, rewarded_set_page_limit: Option, dealers_page_limit: Option, + verification_key_page_limit: Option, + proposals_page_limit: Option, } #[cfg(feature = "nymd-client")] @@ -76,6 +87,8 @@ impl Config { mixnode_delegations_page_limit: None, rewarded_set_page_limit: None, dealers_page_limit: None, + verification_key_page_limit: None, + proposals_page_limit: None, }) } @@ -124,6 +137,8 @@ pub struct Client { mixnode_delegations_page_limit: Option, rewarded_set_page_limit: Option, dealers_page_limit: Option, + verification_key_page_limit: Option, + proposals_page_limit: Option, // ideally they would have been read-only, but unfortunately rust doesn't have such features pub validator_api: validator_api::Client, @@ -151,6 +166,8 @@ impl Client { mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, rewarded_set_page_limit: config.rewarded_set_page_limit, dealers_page_limit: config.dealers_page_limit, + verification_key_page_limit: config.verification_key_page_limit, + proposals_page_limit: config.proposals_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -185,6 +202,8 @@ impl Client { mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, rewarded_set_page_limit: config.rewarded_set_page_limit, dealers_page_limit: config.dealers_page_limit, + verification_key_page_limit: config.verification_key_page_limit, + proposals_page_limit: config.proposals_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -602,6 +621,59 @@ impl Client { Ok(dealings) } + + pub async fn get_all_nymd_verification_key_shares( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut shares = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_vk_shares_paged(start_after.take(), self.verification_key_page_limit) + .await?; + shares.append(&mut paged_response.shares); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(shares) + } + + pub async fn get_all_nymd_proposals( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut proposals = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .nymd + .list_proposals(start_after.take(), self.proposals_page_limit) + .await?; + + let last_id = paged_response.proposals.last().map(|prop| prop.id); + proposals.append(&mut paged_response.proposals); + + if let Some(start_after_res) = last_id { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(proposals) + } } // validator-api wrappers @@ -655,14 +727,53 @@ impl Client { ) -> Result { Ok(self.validator_api.blind_sign(request_body).await?) } +} - pub async fn get_coconut_verification_key( - &self, - ) -> Result { - Ok(self.validator_api.get_coconut_verification_key().await?) +#[derive(Clone)] +pub struct CoconutApiClient { + pub api_client: ApiClient, + pub verification_key: VerificationKey, + pub node_id: NodeIndex, + #[cfg(feature = "nymd-client")] + pub cosmos_address: cosmrs::AccountId, +} + +#[cfg(feature = "nymd-client")] +impl CoconutApiClient { + pub async fn all_coconut_api_clients( + nymd_client: &Client, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + Ok(nymd_client + .get_all_nymd_verification_key_shares() + .await? + .into_iter() + .filter_map(Self::try_from) + .collect()) + } + + fn try_from(share: ContractVKShare) -> Option { + if share.verified { + if let Ok(url_address) = Url::parse(&share.announce_address) { + if let Ok(verification_key) = VerificationKey::try_from_bs58(&share.share) { + if let Ok(cosmos_address) = cosmrs::AccountId::from_str(share.owner.as_str()) { + return Some(CoconutApiClient { + api_client: ApiClient::new(url_address), + verification_key, + node_id: share.node_index, + cosmos_address, + }); + } + } + } + } + None } } +#[derive(Clone)] pub struct ApiClient { pub validator_api: validator_api::Client, // TODO: perhaps if we really need it at some (currently I don't see any reasons for it) @@ -768,16 +879,6 @@ impl ApiClient { .await?) } - pub async fn get_coconut_verification_key( - &self, - ) -> Result { - Ok(self.validator_api.get_coconut_verification_key().await?) - } - - pub async fn get_cosmos_address(&self) -> Result { - Ok(self.validator_api.get_cosmos_address().await?) - } - pub async fn verify_bandwidth_credential( &self, request_body: &VerifyCredentialBody, diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 3e11d3fcb6..2a5e010338 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -9,7 +9,8 @@ mod error; pub mod nymd; pub mod validator_api; -pub use crate::client::ApiClient; +#[cfg(feature = "nymd-client")] +pub use crate::client::{ApiClient, CoconutApiClient}; pub use crate::error::ValidatorClientError; pub use validator_api_requests::*; diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs index 930346fe17..5e4d5a7388 100644 --- a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs @@ -9,6 +9,7 @@ use coconut_dkg_common::dealer::{ }; use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; use coconut_dkg_common::types::{EpochState, MinimumDepositResponse}; +use coconut_dkg_common::verification_key::PagedVKSharesResponse; use cosmrs::AccountId; #[async_trait] @@ -36,6 +37,11 @@ pub trait DkgQueryClient { start_after: Option, page_limit: Option, ) -> Result; + async fn get_vk_shares_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; } #[async_trait] @@ -111,4 +117,18 @@ where .query_contract_smart(self.coconut_dkg_contract_address(), &request) .await } + + async fn get_vk_shares_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetVerificationKeys { + limit: page_limit, + start_after, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } } diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs index f93cbac41c..d55664f0d3 100644 --- a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs @@ -8,6 +8,7 @@ use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; use async_trait::async_trait; use coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg; use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +use coconut_dkg_common::verification_key::VerificationKeyShare; use contracts_common::dealings::ContractSafeBytes; #[async_trait] @@ -15,6 +16,7 @@ pub trait DkgSigningClient { async fn register_dealer( &self, bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, fee: Option, ) -> Result; @@ -23,6 +25,12 @@ pub trait DkgSigningClient { commitment: ContractSafeBytes, fee: Option, ) -> Result; + + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + fee: Option, + ) -> Result; } #[async_trait] @@ -33,10 +41,12 @@ where async fn register_dealer( &self, bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, fee: Option, ) -> Result { let req = DkgExecuteMsg::RegisterDealer { bte_key_with_proof: bte_key, + announce_address, }; let deposit = self.get_deposit_amount().await?; @@ -70,4 +80,23 @@ where ) .await } + + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + fee: Option, + ) -> Result { + let req = DkgExecuteMsg::CommitVerificationKeyShare { share }; + + self.client + .execute( + self.address(), + self.coconut_dkg_contract_address(), + &req, + fee.unwrap_or_default(), + "verification key share commitment", + vec![], + ) + .await + } } diff --git a/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs index 9166897b18..89643e8136 100644 --- a/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs @@ -4,13 +4,19 @@ use crate::nymd::error::NymdError; use crate::nymd::{CosmWasmClient, NymdClient}; -use multisig_contract_common::msg::{ProposalResponse, QueryMsg}; +use cw3::{ProposalListResponse, ProposalResponse}; +use multisig_contract_common::msg::QueryMsg; use async_trait::async_trait; #[async_trait] pub trait MultisigQueryClient { async fn get_proposal(&self, proposal_id: u64) -> Result; + async fn list_proposals( + &self, + start_after: Option, + limit: Option, + ) -> Result; } #[async_trait] @@ -21,4 +27,15 @@ impl MultisigQueryClient for NymdClient { .query_contract_smart(self.multisig_contract_address(), &request) .await } + + async fn list_proposals( + &self, + start_after: Option, + limit: Option, + ) -> Result { + let request = QueryMsg::ListProposals { start_after, limit }; + self.client + .query_contract_smart(self.multisig_contract_address(), &request) + .await + } } diff --git a/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs index 46bb118ef0..a715c85075 100644 --- a/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs @@ -7,11 +7,11 @@ use crate::nymd::error::NymdError; use crate::nymd::{Fee, NymdClient}; use coconut_bandwidth_contract_common::msg::ExecuteMsg as CoconutBandwidthExecuteMsg; +use cw3::Vote; use multisig_contract_common::msg::ExecuteMsg; use async_trait::async_trait; use cosmwasm_std::{to_binary, Coin, CosmosMsg, WasmMsg}; -use cw3::Vote; #[async_trait] pub trait MultisigSigningClient { diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 0f4ebc60db..4a7b7af3cd 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -9,8 +9,7 @@ use reqwest::Response; use serde::{Deserialize, Serialize}; use url::Url; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, @@ -27,6 +26,7 @@ type Params<'a, K, V> = &'a [(K, V)]; const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; +#[derive(Clone)] pub struct Client { url: Url, reqwest_client: reqwest::Client, @@ -443,34 +443,6 @@ impl Client { .await } - pub async fn get_coconut_verification_key( - &self, - ) -> Result { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_VERIFICATION_KEY, - ], - NO_PARAMS, - ) - .await - } - - pub async fn get_cosmos_address(&self) -> Result { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_COSMOS_ADDRESS, - ], - NO_PARAMS, - ) - .await - } - pub async fn verify_bandwidth_credential( &self, request_body: &VerifyCredentialBody, diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index 64aa1b8cd9..66a822b4fd 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -15,8 +15,6 @@ pub const BANDWIDTH: &str = "bandwidth"; pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; pub const COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL: &str = "partial-bandwidth-credential"; -pub const COCONUT_VERIFICATION_KEY: &str = "verification-key"; -pub const COCONUT_COSMOS_ADDRESS: &str = "cosmos-address"; pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; pub const STATUS_ROUTES: &str = "status"; diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs index 431caa226c..80f5daf68a 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs @@ -1,4 +1,4 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub const PROPOSAL_ID: &str = "proposal_id"; +pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml b/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml index 1ba84dadbc..cffca1ac3d 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml +++ b/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml @@ -7,7 +7,9 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" +cw-utils = "0.13.4" schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -contracts-common = { path = "../contracts-common" } \ No newline at end of file +contracts-common = { path = "../contracts-common" } +multisig-contract-common = { path = "../multisig-contract" } \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs index 46f48de623..0b60fec2e8 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; pub struct DealerDetails { pub address: Addr, pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, + pub announce_address: String, pub assigned_index: NodeIndex, pub deposit: Coin, } diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs index ed28d113ce..28166677af 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 pub const NODE_INDEX: &str = "node_index"; +pub const DKG_PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs index 993fc3ae41..545451eeb6 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs @@ -2,3 +2,4 @@ pub mod dealer; pub mod event_attributes; pub mod msg; pub mod types; +pub mod verification_key; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index 6ed6f4618a..dd99dde2c0 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -2,14 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof}; +use crate::verification_key::VerificationKeyShare; +use cosmwasm_std::Addr; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct InstantiateMsg { pub group_addr: String, - pub mix_denom: String, + pub multisig_addr: String, pub admin: String, + pub mix_denom: String, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] @@ -17,12 +20,21 @@ pub struct InstantiateMsg { pub enum ExecuteMsg { RegisterDealer { bte_key_with_proof: EncodedBTEPublicKeyWithProof, + announce_address: String, }, CommitDealing { dealing_bytes: ContractSafeBytes, }, + CommitVerificationKeyShare { + share: VerificationKeyShare, + }, + + VerifyVerificationKeyShare { + owner: Addr, + }, + AdvanceEpochState {}, // DEBUG ONLY TXs. THEY SHALL BE REMOVED BEFORE FINALISING THE CODE @@ -53,6 +65,10 @@ pub enum QueryMsg { limit: Option, start_after: Option, }, + GetVerificationKeys { + limit: Option, + start_after: Option, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index 6c64d41aa3..d2696f2941 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -33,6 +33,8 @@ pub enum EpochState { PublicKeySubmission, DealingExchange, VerificationKeySubmission, + VerificationKeyValidation, + VerificationKeyFinalization, InProgress, } @@ -48,6 +50,8 @@ impl Display for EpochState { EpochState::PublicKeySubmission => write!(f, "PublicKeySubmission"), EpochState::DealingExchange => write!(f, "DealingExchange"), EpochState::VerificationKeySubmission => write!(f, "VerificationKeySubmission"), + EpochState::VerificationKeyValidation => write!(f, "VerificationKeyValidation"), + EpochState::VerificationKeyFinalization => write!(f, "VerificationKeyFinalization"), EpochState::InProgress => write!(f, "InProgress"), } } @@ -58,7 +62,9 @@ impl EpochState { match self { EpochState::PublicKeySubmission => Some(EpochState::DealingExchange), EpochState::DealingExchange => Some(EpochState::VerificationKeySubmission), - EpochState::VerificationKeySubmission => Some(EpochState::InProgress), + EpochState::VerificationKeySubmission => Some(EpochState::VerificationKeyValidation), + EpochState::VerificationKeyValidation => Some(EpochState::VerificationKeyFinalization), + EpochState::VerificationKeyFinalization => Some(EpochState::InProgress), EpochState::InProgress => None, } } diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs new file mode 100644 index 0000000000..cfc3ffae03 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs @@ -0,0 +1,70 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::msg::ExecuteMsg; +use crate::types::NodeIndex; +use cosmwasm_std::{from_binary, to_binary, Addr, CosmosMsg, StdResult, Timestamp, WasmMsg}; +use cw_utils::Expiration; +use multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; +use serde::{Deserialize, Serialize}; + +pub type VerificationKeyShare = String; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ContractVKShare { + pub share: VerificationKeyShare, + pub announce_address: String, + pub node_index: NodeIndex, + pub owner: Addr, + pub verified: bool, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PagedVKSharesResponse { + pub shares: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +pub fn to_cosmos_msg( + owner: Addr, + coconut_dkg_addr: String, + multisig_addr: String, + expiration_time: Timestamp, +) -> StdResult { + let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { owner }; + let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: coconut_dkg_addr, + msg: to_binary(&verify_vk_share_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: String::from("Verify VK share, as ordered by Coconut DKG Contract"), + description: String::new(), + msgs: vec![verify_vk_share_msg], + latest: Some(Expiration::AtTime(expiration_time)), + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + Ok(msg) +} + +pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option { + if let Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: _, + msg, + funds: _, + })) = msgs.get(0) + { + if let Ok(ExecuteMsg::VerifyVerificationKeyShare { owner }) = from_binary::(msg) + { + return Some(owner); + } + } + None +} diff --git a/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml b/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml index cb3c2c0cb1..f493cb6635 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml @@ -6,9 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -cw-utils = { version = "0.13.1" } -cw3 = { version = "0.13.1" } -cw4 = { version = "0.13.1" } +cw-utils = { version = "0.13.4" } +cw3 = { version = "0.13.4" } +cw3-fixed-multisig = { version = "0.13.4", features = ["library"] } +cw4 = { version = "0.13.4" } cosmwasm-std = "1.0.0" schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs index 93fd9f4839..db37835f64 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs @@ -5,7 +5,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{CosmosMsg, Empty}; -pub use cw3::ProposalResponse; use cw3::Vote; use cw4::MemberChangedHookMsg; use cw_utils::{Duration, Expiration, Threshold}; @@ -15,6 +14,7 @@ pub struct InstantiateMsg { // this is the group contract that contains the member list pub group_addr: String, pub coconut_bandwidth_contract_address: String, + pub coconut_dkg_contract_address: String, pub threshold: Threshold, pub max_voting_period: Duration, } @@ -82,4 +82,5 @@ pub enum QueryMsg { #[serde(rename_all = "snake_case")] pub struct MigrateMsg { pub coconut_bandwidth_address: String, + pub coconut_dkg_address: String, } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 4ff6b60d2b..58d43af03f 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] } cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", optional = true } thiserror = "1.0" -url = "2.2" # I guess temporarily until we get serde support in coconut up and running coconut-interface = { path = "../coconut-interface" } diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs index 537ea801ff..f701938d59 100644 --- a/common/credentials/src/coconut/utils.rs +++ b/common/credentials/src/coconut/utils.rs @@ -8,8 +8,8 @@ use coconut_interface::{ use crypto::asymmetric::encryption::PublicKey; use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; -use url::Url; use validator_api_requests::coconut::BlindSignRequestBody; +use validator_client::client::CoconutApiClient; use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; use crate::coconut::params::{ @@ -17,46 +17,21 @@ use crate::coconut::params::{ }; use crate::error::Error; -/// Contacts all provided validators and then aggregate their verification keys. -/// -/// # Arguments -/// -/// * `validators`: list of validators to obtain verification keys from. -/// -/// Note: list of validators must be correctly ordered by the polynomial coordinates used -/// during key generation and it is responsibility of the caller to ensure that correct -/// number of them is provided -/// -/// # Examples -/// -/// ```no_run -/// use url::{Url, ParseError}; -/// use credentials::obtain_aggregate_verification_key; -/// -/// async fn example() -> Result<(), ParseError> { -/// let validators = vec!["https://sandbox-validator1.nymtech.net/api".parse()?, "https://sandbox-validator2.nymtech.net/api".parse()?]; -/// let aggregated_key = obtain_aggregate_verification_key(&validators).await; -/// // deal with the obtained Result -/// Ok(()) -/// } -/// ``` pub async fn obtain_aggregate_verification_key( - validators: &[Url], + api_clients: &[CoconutApiClient], ) -> Result { - if validators.is_empty() { + if api_clients.is_empty() { return Err(Error::NoValidatorsAvailable); } - let mut indices = Vec::with_capacity(validators.len()); - let mut shares = Vec::with_capacity(validators.len()); - - let mut client = validator_client::ApiClient::new(validators[0].clone()); - for (id, validator_url) in validators.iter().enumerate() { - client.change_validator_api(validator_url.clone()); - let response = client.get_coconut_verification_key().await?; - indices.push((id + 1) as u64); - shares.push(response.key); - } + let indices: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.node_id) + .collect(); + let shares: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.verification_key.clone()) + .collect(); Ok(aggregate_verification_keys(&shares, Some(&indices))?) } @@ -64,7 +39,7 @@ pub async fn obtain_aggregate_verification_key( async fn obtain_partial_credential( params: &Parameters, attributes: &BandwidthVoucher, - client: &validator_client::ApiClient, + client: &validator_client::client::ApiClient, validator_vk: &VerificationKey, ) -> Result { let public_attributes = attributes.get_public_attributes(); @@ -118,26 +93,33 @@ async fn obtain_partial_credential( pub async fn obtain_aggregate_signature( params: &Parameters, attributes: &BandwidthVoucher, - validators: &[Url], + coconut_api_clients: &[CoconutApiClient], ) -> Result { - if validators.is_empty() { + if coconut_api_clients.is_empty() { return Err(Error::NoValidatorsAvailable); } let public_attributes = attributes.get_public_attributes(); let private_attributes = attributes.get_private_attributes(); - let mut shares = Vec::with_capacity(validators.len()); - let mut validators_partial_vks: Vec = Vec::with_capacity(validators.len()); + let mut shares = Vec::with_capacity(coconut_api_clients.len()); + let validators_partial_vks: Vec<_> = coconut_api_clients + .iter() + .map(|api_client| api_client.verification_key.clone()) + .collect(); + let indices: Vec<_> = coconut_api_clients + .iter() + .map(|api_client| api_client.node_id) + .collect(); - let mut client = validator_client::ApiClient::new(validators[0].clone()); - for (id, validator_url) in validators.iter().enumerate() { - client.change_validator_api(validator_url.clone()); - let validator_partial_vk = client.get_coconut_verification_key().await?; - validators_partial_vks.push(validator_partial_vk.key.clone()); - let signature = - obtain_partial_credential(params, attributes, &client, &validator_partial_vk.key) - .await?; - let share = SignatureShare::new(signature, (id + 1) as u64); + for coconut_api_client in coconut_api_clients.iter() { + let signature = obtain_partial_credential( + params, + attributes, + &coconut_api_client.api_client, + &coconut_api_client.verification_key, + ) + .await?; + let share = SignatureShare::new(signature, coconut_api_client.node_id); shares.push(share) } @@ -145,10 +127,6 @@ pub async fn obtain_aggregate_signature( attributes.extend_from_slice(&private_attributes); attributes.extend_from_slice(&public_attributes); - let mut indices: Vec = Vec::with_capacity(validators_partial_vks.len()); - for i in 0..validators_partial_vks.len() { - indices.push((i + 1) as u64); - } let verification_key = aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref()))?; diff --git a/common/crypto/dkg/src/dealing.rs b/common/crypto/dkg/src/dealing.rs index ead51b7f80..817bffdf12 100644 --- a/common/crypto/dkg/src/dealing.rs +++ b/common/crypto/dkg/src/dealing.rs @@ -15,6 +15,13 @@ use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct RecoveredVerificationKeys { + pub recovered_master: G2Projective, + pub recovered_partials: Vec, +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct Dealing { @@ -245,7 +252,7 @@ pub fn try_recover_verification_keys( dealings: &[Dealing], threshold: Threshold, receivers: &BTreeMap, -) -> Result<(G2Projective, Vec), DkgError> { +) -> Result { if dealings.is_empty() { return Err(DkgError::NoDealingsAvailable); } @@ -292,7 +299,10 @@ pub fn try_recover_verification_keys( .map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index))) .collect(); - Ok((master_verification_key, verification_key_shares)) + Ok(RecoveredVerificationKeys { + recovered_master: master_verification_key, + recovered_partials: verification_key_shares, + }) } pub fn verify_verification_keys( @@ -389,8 +399,10 @@ mod tests { .unwrap(); // END OF SETUP - let (recovered_master, recovered_partials) = - try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master, + recovered_partials, + } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); let g2 = G2Projective::generator(); assert_eq!(g2 * master_secret, recovered_master); @@ -425,8 +437,10 @@ mod tests { }) .collect::>(); - let (recovered_master, recovered_partials) = - try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master, + recovered_partials, + } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); assert!(verify_verification_keys( &recovered_master, diff --git a/common/crypto/dkg/tests/integration.rs b/common/crypto/dkg/tests/integration.rs index 24518dd789..2d25c31d40 100644 --- a/common/crypto/dkg/tests/integration.rs +++ b/common/crypto/dkg/tests/integration.rs @@ -3,6 +3,7 @@ use bls12_381::{G2Projective, Scalar}; use dkg::bte::{decrypt_share, keygen, setup}; +use dkg::dealing::RecoveredVerificationKeys; use dkg::interpolation::perform_lagrangian_interpolation_at_origin; use dkg::{combine_shares, try_recover_verification_keys, Dealing}; use rand_core::SeedableRng; @@ -100,8 +101,10 @@ fn full_threshold_secret_sharing() { } // recover verification keys - let (recovered_master, recovered_partials) = - try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master, + recovered_partials, + } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); let g2 = G2Projective::generator(); @@ -171,8 +174,10 @@ fn full_threshold_secret_resharing() { .collect::>(); // recover verification keys - let (public_original_master, recovered_partials) = - try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master: public_original_master, + recovered_partials, + } = try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap(); let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { @@ -217,8 +222,10 @@ fn full_threshold_secret_resharing() { } // recover verification keys - let (public_reshared_master, reshared_partials) = - try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master: public_reshared_master, + recovered_partials: reshared_partials, + } = try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap(); let mut reshared_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { diff --git a/common/nymcoconut/Cargo.toml b/common/nymcoconut/Cargo.toml index c17aa280d3..5dd70dbd2c 100644 --- a/common/nymcoconut/Cargo.toml +++ b/common/nymcoconut/Cargo.toml @@ -17,6 +17,9 @@ serde_derive = "1.0" bs58 = "0.4.0" sha2 = "0.9" +dkg = { path = "../crypto/dkg" } +pemstore = { path = "../pemstore" } + [dependencies.ff] version = "0.11" default-features = false @@ -28,7 +31,6 @@ default-features = false [dev-dependencies] criterion = { version="0.3", features=["html_reports"] } doc-comment = "0.3" -dkg = { path = "../crypto/dkg" } rand_chacha = "0.3" [dev-dependencies.bincode] diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index b48bbc2837..a98b546fc1 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -21,6 +21,7 @@ pub use scheme::keygen::SecretKey; pub use scheme::keygen::VerificationKey; pub use scheme::setup::setup; pub use scheme::setup::Parameters; +pub use scheme::verification::check_vk_pairing; pub use scheme::verification::prove_bandwidth_credential; pub use scheme::verification::verify_credential; pub use scheme::verification::Theta; diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index 75fb203dc2..ea9c88aa65 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -9,6 +9,7 @@ use std::convert::TryInto; use bls12_381::{G1Projective, G2Projective, Scalar}; use group::Curve; +use pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use serde_derive::{Deserialize, Serialize}; use crate::error::{CoconutError, Result}; @@ -29,6 +30,22 @@ pub struct SecretKey { pub(crate) ys: Vec, } +impl PemStorableKey for SecretKey { + type Error = CoconutError; + + fn pem_type() -> &'static str { + "COCONUT SECRET KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + impl TryFrom<&[u8]> for SecretKey { type Error = CoconutError; @@ -128,6 +145,22 @@ pub struct VerificationKey { pub(crate) beta_g2: Vec, } +impl PemStorableKey for VerificationKey { + type Error = CoconutError; + + fn pem_type() -> &'static str { + "COCONUT VERIFICATION KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + impl TryFrom<&[u8]> for VerificationKey { type Error = CoconutError; @@ -366,6 +399,23 @@ pub struct KeyPair { pub index: Option, } +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = SecretKey; + type PublicPemKey = VerificationKey; + + fn private_key(&self) -> &Self::PrivatePemKey { + &self.secret_key + } + + fn public_key(&self) -> &Self::PublicPemKey { + &self.verification_key + } + + fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self { + Self::from_keys(secret_key, verification_key) + } +} + impl KeyPair { const MARKER_BYTES: &'static [u8] = b"coconutkeypair"; diff --git a/common/nymcoconut/src/scheme/verification.rs b/common/nymcoconut/src/scheme/verification.rs index 9f7aba12b5..96ef28e9ca 100644 --- a/common/nymcoconut/src/scheme/verification.rs +++ b/common/nymcoconut/src/scheme/verification.rs @@ -212,6 +212,39 @@ pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2 multi_miller.final_exponentiation().is_identity().into() } +pub fn check_vk_pairing( + params: &Parameters, + dkg_values: &[G2Projective], + vk: &VerificationKey, +) -> bool { + let values_len = dkg_values.len(); + if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() { + return false; + } + if vk.alpha != *dkg_values.last().unwrap() { + return false; + } + if dkg_values + .iter() + .zip(vk.beta_g2.iter()) + .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) + { + return false; + } + if vk.beta_g1.iter().zip(vk.beta_g2.iter()).any(|(g1, g2)| { + !check_bilinear_pairing( + params.gen1(), + &G2Prepared::from(g2.to_affine()), + &g1.to_affine(), + params.prepared_miller_g2(), + ) + }) { + return false; + } + + true +} + pub fn verify_credential( params: &Parameters, verification_key: &VerificationKey, @@ -284,6 +317,15 @@ mod tests { use super::*; + #[test] + fn vk_pairing() { + let params = setup(2).unwrap(); + let vk = keygen(¶ms).verification_key(); + let mut dkg_values = vk.beta_g2.clone(); + dkg_values.push(vk.alpha); + assert!(check_vk_pairing(¶ms, &dkg_values, &vk)); + } + #[test] fn theta_bytes_roundtrip() { let params = setup(2).unwrap(); diff --git a/common/nymcoconut/src/tests/helpers.rs b/common/nymcoconut/src/tests/helpers.rs index a9be6b885c..ee7e418788 100644 --- a/common/nymcoconut/src/tests/helpers.rs +++ b/common/nymcoconut/src/tests/helpers.rs @@ -3,6 +3,7 @@ use crate::*; use itertools::izip; +use std::fmt::Debug; pub fn theta_from_keys_and_attributes( params: &Parameters, @@ -87,13 +88,26 @@ pub fn theta_from_keys_and_attributes( Ok(theta) } +pub fn transpose_matrix(matrix: Vec>) -> Vec> { + let len = matrix[0].len(); + let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect(); + (0..len) + .map(|_| { + iters + .iter_mut() + .map(|it| it.next().unwrap()) + .collect::>() + }) + .collect::>() +} + #[cfg(test)] pub mod tests { + use super::*; use crate::{KeyPair, Parameters, SecretKey}; use bls12_381::Scalar; use dkg::{bte::decrypt_share, combine_shares, Dealing, NodeIndex}; use rand_chacha::rand_core::SeedableRng; - use std::fmt::Debug; pub fn generate_dkg_secrets(node_indices: &[NodeIndex]) -> Vec { let dummy_seed = [42u8; 32]; @@ -148,19 +162,4 @@ pub mod tests { }) .collect() } - - pub(crate) fn transpose_matrix(matrix: Vec>) -> Vec> { - let len = matrix[0].len(); - let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect(); - (0..len) - .map(|_| { - iters - .iter_mut() - .map(|it| it.next().unwrap()) - .collect::>() - }) - .collect::>() - .try_into() - .unwrap() - } } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index b32e8092f6..d2fc273d22 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -210,6 +210,8 @@ version = "0.1.0" dependencies = [ "contracts-common", "cosmwasm-std", + "cw-utils", + "multisig-contract-common", "schemars", "serde", ] @@ -221,6 +223,8 @@ dependencies = [ "bandwidth-claim-contract", "coconut-bandwidth", "coconut-bandwidth-contract-common", + "coconut-dkg", + "coconut-dkg-common", "cosmwasm-std", "cosmwasm-storage", "cw-controllers", @@ -228,6 +232,7 @@ dependencies = [ "cw-storage-plus", "cw-utils", "cw3-flex-multisig", + "cw4", "cw4-group", "multisig-contract-common", "schemars", @@ -409,9 +414,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbea57e5be4a682268a5eca1a57efece57a54ff216bfd87603d5e864aad40e12" +checksum = "a3f9a8ab7c3c29ec93cb7a39ce4b14a05e053153b4a17ef7cf2246af1b7c087e" dependencies = [ "anyhow", "cosmwasm-std", @@ -523,7 +528,7 @@ dependencies = [ [[package]] name = "cw4-group" -version = "0.13.1" +version = "0.13.4" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -998,6 +1003,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -1746,9 +1752,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "x25519-dalek" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -1757,9 +1763,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/contracts/coconut-dkg/src/constants.rs b/contracts/coconut-dkg/src/constants.rs index e250c889c0..bfa7d44aef 100644 --- a/contracts/coconut-dkg/src/constants.rs +++ b/contracts/coconut-dkg/src/constants.rs @@ -5,3 +5,5 @@ use cosmwasm_std::Uint128; // to be determined whether those should be constants or exist as contract state pub(crate) const MINIMUM_DEPOSIT: Uint128 = Uint128::new(1_000_000_000); +// Wait time for the verification to take place (currently 24h) +pub(crate) const BLOCK_TIME_FOR_VERIFICATION_SECS: u64 = 86400; diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index c7a0411f8c..7ad730e5ab 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -65,6 +65,7 @@ pub fn try_add_dealer( mut deps: DepsMut<'_>, info: MessageInfo, bte_key_with_proof: EncodedBTEPublicKeyWithProof, + announce_address: String, ) -> Result { check_epoch_state(deps.storage, EpochState::PublicKeySubmission)?; let state = STATE.load(deps.storage)?; @@ -94,6 +95,7 @@ pub fn try_add_dealer( let dealer_details = DealerDetails { address: info.sender.clone(), bte_public_key_with_proof: bte_key_with_proof, + announce_address, assigned_index: node_index, deposit, }; diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs index 7b2b132254..73e067e0d6 100644 --- a/contracts/coconut-dkg/src/dealings/transactions.rs +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -14,7 +14,7 @@ pub fn try_commit_dealings( dealing_bytes: ContractSafeBytes, ) -> Result { check_epoch_state(deps.storage, EpochState::DealingExchange)?; - // ensure the sender is a dealer for the current epoch + // ensure the sender is a dealer if dealers_storage::current_dealers() .may_load(deps.storage, &info.sender)? .is_none() @@ -31,5 +31,7 @@ pub fn try_commit_dealings( } } - Err(ContractError::AlreadyCommitted) + Err(ContractError::AlreadyCommitted { + commitment: String::from("dealing"), + }) } diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs index a29ffd8223..ad3a3e74ef 100644 --- a/contracts/coconut-dkg/src/error.rs +++ b/contracts/coconut-dkg/src/error.rs @@ -60,6 +60,9 @@ pub enum ContractError { #[error("This sender is not a dealer for the current epoch")] NotADealer, - #[error("This dealer has already commited dealing for this epoch")] - AlreadyCommitted, + #[error("This dealer has already committed {commitment}")] + AlreadyCommitted { commitment: String }, + + #[error("No verification key committed for owner {owner}")] + NoCommitForOwner { owner: String }, } diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index 0151770d1f..7787776306 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -8,7 +8,8 @@ use crate::dealers::queries::{ use crate::dealings::queries::query_dealings_paged; use crate::epoch_state::queries::query_current_epoch_state; use crate::error::ContractError; -use crate::state::{State, ADMIN, STATE}; +use crate::state::{State, ADMIN, MULTISIG, STATE}; +use crate::verification_key_shares::queries::query_vk_shares_paged; use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use coconut_dkg_common::types::{EpochState, MinimumDepositResponse}; use cosmwasm_std::{ @@ -23,6 +24,7 @@ mod dealings; mod epoch_state; mod error; mod state; +mod verification_key_shares; /// Instantiate the contract. /// @@ -37,7 +39,9 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let admin_addr = deps.api.addr_validate(&msg.admin)?; + let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?; ADMIN.set(deps.branch(), Some(admin_addr))?; + MULTISIG.set(deps.branch(), Some(multisig_addr.clone()))?; let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { ContractError::InvalidGroup { @@ -47,6 +51,7 @@ pub fn instantiate( let state = State { group_addr, + multisig_addr, mix_denom: msg.mix_denom, }; STATE.save(deps.storage, &state)?; @@ -65,12 +70,25 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::RegisterDealer { bte_key_with_proof } => { - dealers::transactions::try_add_dealer(deps, info, bte_key_with_proof) + ExecuteMsg::RegisterDealer { + bte_key_with_proof, + announce_address, + } => { + dealers::transactions::try_add_dealer(deps, info, bte_key_with_proof, announce_address) } ExecuteMsg::CommitDealing { dealing_bytes } => { dealings::transactions::try_commit_dealings(deps, info, dealing_bytes) } + ExecuteMsg::CommitVerificationKeyShare { share } => { + verification_key_shares::transactions::try_commit_verification_key_share( + deps, env, info, share, + ) + } + ExecuteMsg::VerifyVerificationKeyShare { owner } => { + verification_key_shares::transactions::try_verify_verification_key_share( + deps, info, owner, + ) + } ExecuteMsg::DebugUnsafeResetAll { init_msg } => { reset_contract_state(deps, env, info, init_msg) } @@ -140,6 +158,9 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?, + QueryMsg::GetVerificationKeys { limit, start_after } => { + to_binary(&query_vk_shares_paged(deps, start_after, limit)?)? + } }; Ok(response) @@ -161,8 +182,9 @@ mod tests { let env = mock_env(); let msg = InstantiateMsg { group_addr: "group_addr".to_string(), - mix_denom: "nym".to_string(), + multisig_addr: "multisig_addr".to_string(), admin: "admin".to_string(), + mix_denom: "nym".to_string(), }; let info = mock_info("creator", &[]); diff --git a/contracts/coconut-dkg/src/state/mod.rs b/contracts/coconut-dkg/src/state/mod.rs index cc5e965839..729618ec60 100644 --- a/contracts/coconut-dkg/src/state/mod.rs +++ b/contracts/coconut-dkg/src/state/mod.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use cosmwasm_std::Addr; use cw4::Cw4Contract; use cw_controllers::Admin; use cw_storage_plus::Item; @@ -10,9 +11,11 @@ use serde::{Deserialize, Serialize}; // unique items pub const STATE: Item = Item::new("state"); pub const ADMIN: Admin = Admin::new("admin"); +pub const MULTISIG: Admin = Admin::new("multisig"); #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] pub struct State { pub mix_denom: String, + pub multisig_addr: Addr, pub group_addr: Cw4Contract, } diff --git a/contracts/coconut-dkg/src/verification_key_shares/mod.rs b/contracts/coconut-dkg/src/verification_key_shares/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/coconut-dkg/src/verification_key_shares/queries.rs b/contracts/coconut-dkg/src/verification_key_shares/queries.rs new file mode 100644 index 0000000000..6739577abb --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/queries.rs @@ -0,0 +1,38 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::verification_key_shares::storage; +use crate::verification_key_shares::storage::VK_SHARES; +use coconut_dkg_common::verification_key::PagedVKSharesResponse; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +pub fn query_vk_shares_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(storage::VERIFICATION_KEY_SHARES_PAGE_DEFAULT_LIMIT) + .min(storage::VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let shares = VK_SHARES + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(_, share)| share)) + .collect::>>()?; + + let start_next_after = shares.last().map(|share| share.owner.clone()); + + Ok(PagedVKSharesResponse { + shares, + per_page: limit, + start_next_after, + }) +} diff --git a/contracts/coconut-dkg/src/verification_key_shares/storage.rs b/contracts/coconut-dkg/src/verification_key_shares/storage.rs new file mode 100644 index 0000000000..761736367c --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/storage.rs @@ -0,0 +1,15 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::verification_key::ContractVKShare; +use cosmwasm_std::Addr; +use cw_storage_plus::Map; + +pub(crate) const VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const VERIFICATION_KEY_SHARES_PAGE_DEFAULT_LIMIT: u32 = 50; + +type VKShareKey<'a> = &'a Addr; + +pub(crate) const VK_SHARES: Map<'_, VKShareKey<'_>, ContractVKShare> = Map::new("vks"); diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs new file mode 100644 index 0000000000..d86748e7f4 --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -0,0 +1,71 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::BLOCK_TIME_FOR_VERIFICATION_SECS; +use crate::dealers::storage as dealers_storage; +use crate::epoch_state::utils::check_epoch_state; +use crate::error::ContractError; +use crate::state::{MULTISIG, STATE}; +use crate::verification_key_shares::storage::VK_SHARES; +use coconut_dkg_common::types::EpochState; +use coconut_dkg_common::verification_key::{to_cosmos_msg, ContractVKShare, VerificationKeyShare}; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response}; + +pub fn try_commit_verification_key_share( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + share: VerificationKeyShare, +) -> Result { + check_epoch_state(deps.storage, EpochState::VerificationKeySubmission)?; + // ensure the sender is a dealer + let details = dealers_storage::current_dealers() + .load(deps.storage, &info.sender) + .map_err(|_| ContractError::NotADealer)?; + if VK_SHARES.may_load(deps.storage, &info.sender)?.is_some() { + return Err(ContractError::AlreadyCommitted { + commitment: String::from("verification key share"), + }); + } + + let data = ContractVKShare { + share, + node_index: details.assigned_index, + announce_address: details.announce_address, + owner: info.sender.clone(), + verified: false, + }; + VK_SHARES.save(deps.storage, &info.sender, &data)?; + + let msg = to_cosmos_msg( + info.sender, + env.contract.address.to_string(), + STATE.load(deps.storage)?.multisig_addr.to_string(), + env.block + .time + .plus_seconds(BLOCK_TIME_FOR_VERIFICATION_SECS), + )?; + + Ok(Response::new().add_message(msg)) +} + +pub fn try_verify_verification_key_share( + deps: DepsMut<'_>, + info: MessageInfo, + owner: Addr, +) -> Result { + check_epoch_state(deps.storage, EpochState::VerificationKeyFinalization)?; + MULTISIG.assert_admin(deps.as_ref(), &info.sender)?; + VK_SHARES.update(deps.storage, &owner, |vk_share| { + vk_share + .map(|mut share| { + share.verified = true; + share + }) + .ok_or(ContractError::NoCommitForOwner { + owner: owner.to_string(), + }) + })?; + + Ok(Response::default()) +} diff --git a/contracts/coconut-test/Cargo.toml b/contracts/coconut-test/Cargo.toml index 5955391250..3aff6fc368 100644 --- a/contracts/coconut-test/Cargo.toml +++ b/contracts/coconut-test/Cargo.toml @@ -8,10 +8,12 @@ edition = "2021" [dependencies] bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" } coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" } multisig-contract-common = { path = "../../common/cosmwasm-smart-contracts/multisig-contract" } cosmwasm-std = "1.0.0" cosmwasm-storage = "1.0.0" +cw4 = "0.13.4" cw-storage-plus = "0.13.4" cw-controllers = "0.13.4" cw-utils = "0.13.4" @@ -21,7 +23,8 @@ serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = "1.0.23" coconut-bandwidth = { path = "../coconut-bandwidth" } -cw-multi-test = { version = "0.13.2" } +coconut-dkg = { path = "../coconut-dkg" } +cw-multi-test = { version = "0.13.4" } cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig" } cw4-group = { path = "../multisig/cw4-group" } diff --git a/contracts/coconut-test/src/helpers.rs b/contracts/coconut-test/src/helpers.rs index 2a8c0b24ed..b8e8ad6d03 100644 --- a/contracts/coconut-test/src/helpers.rs +++ b/contracts/coconut-test/src/helpers.rs @@ -8,6 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub const OWNER: &str = "admin0001"; +pub const MEMBER1: &str = "member1"; pub const MULTISIG_CONTRACT: &str = "multisig contract address"; pub const POOL_CONTRACT: &str = "mix pool contract address"; pub const RANDOM_ADDRESS: &str = "random address"; @@ -16,6 +17,7 @@ pub const RANDOM_ADDRESS: &str = "random address"; #[serde(rename_all = "snake_case")] pub struct MigrateMsg { pub coconut_bandwidth_address: String, + pub coconut_dkg_address: String, } #[entry_point] @@ -32,8 +34,20 @@ pub fn mock_app(init_funds: &[Coin]) -> App { .bank .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec()) .unwrap(); + router + .bank + .init_balance(storage, &Addr::unchecked(MEMBER1), init_funds.to_vec()) + .unwrap(); }) } +pub fn contract_dkg() -> Box> { + let contract = ContractWrapper::new( + coconut_dkg::execute, + coconut_dkg::instantiate, + coconut_dkg::query, + ); + Box::new(contract) +} pub fn contract_bandwidth() -> Box> { let contract = ContractWrapper::new( diff --git a/contracts/coconut-test/src/spend_credential_creates_proposal.rs b/contracts/coconut-test/src/spend_credential_creates_proposal.rs index 7d07905b97..3d382f493b 100644 --- a/contracts/coconut-test/src/spend_credential_creates_proposal.rs +++ b/contracts/coconut-test/src/spend_credential_creates_proposal.rs @@ -15,6 +15,7 @@ use multisig_contract_common::msg::InstantiateMsg as MultisigInstantiateMsg; pub const TEST_COIN_DENOM: &str = "unym"; pub const TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const TEST_COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; #[test] fn spend_credential_creates_proposal() { @@ -46,6 +47,7 @@ fn spend_credential_creates_proposal() { }, max_voting_period: Duration::Height(1000), coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let multisig_contract_addr = app .instantiate_contract( @@ -77,6 +79,7 @@ fn spend_credential_creates_proposal() { let msg = MigrateMsg { coconut_bandwidth_address: coconut_bandwidth_contract_addr.to_string(), + coconut_dkg_address: "".to_string(), }; app.migrate_contract( Addr::unchecked(OWNER), diff --git a/contracts/coconut-test/src/submit_vk_creates_proposal.rs b/contracts/coconut-test/src/submit_vk_creates_proposal.rs new file mode 100644 index 0000000000..a556bbe35f --- /dev/null +++ b/contracts/coconut-test/src/submit_vk_creates_proposal.rs @@ -0,0 +1,142 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::{ + contract_dkg, contract_group, contract_multisig, mock_app, MigrateMsg, MEMBER1, OWNER, +}; +use crate::spend_credential_creates_proposal::{ + TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS, TEST_COCONUT_DKG_CONTRACT_ADDRESS, TEST_COIN_DENOM, +}; +use coconut_dkg_common::msg::ExecuteMsg::{ + AdvanceEpochState, CommitVerificationKeyShare, RegisterDealer, +}; +use coconut_dkg_common::msg::InstantiateMsg as DkgInstantiateMsg; +use cosmwasm_std::{coins, Addr, Decimal}; +use cw4::Member; +use cw4_group::msg::InstantiateMsg as GroupInstantiateMsg; +use cw_multi_test::Executor; +use cw_utils::{Duration, Threshold}; +use multisig_contract_common::msg::InstantiateMsg as MultisigInstantiateMsg; + +#[test] +fn dkg_create_proposal() { + let init_funds = coins(10000000000, TEST_COIN_DENOM); + let mut app = mock_app(&init_funds); + let member1 = Member { + addr: MEMBER1.to_string(), + weight: 10, + }; + + let group_code_id = app.store_code(contract_group()); + let msg = GroupInstantiateMsg { + admin: Some(OWNER.to_string()), + members: vec![member1], + }; + let group_contract_addr = app + .instantiate_contract( + group_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "group", + None, + ) + .unwrap(); + + let multisig_code_id = app.store_code(contract_multisig()); + let msg = MultisigInstantiateMsg { + group_addr: group_contract_addr.to_string(), + threshold: Threshold::AbsolutePercentage { + percentage: Decimal::from_ratio(2u128, 3u128), + }, + max_voting_period: Duration::Time(1000), + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), + }; + let multisig_contract_addr = app + .instantiate_contract( + multisig_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "multisig", + Some(OWNER.to_string()), + ) + .unwrap(); + + let coconut_dkg_code_id = app.store_code(contract_dkg()); + let msg = DkgInstantiateMsg { + group_addr: group_contract_addr.to_string(), + multisig_addr: multisig_contract_addr.to_string(), + admin: Addr::unchecked(OWNER).to_string(), + mix_denom: TEST_COIN_DENOM.to_string(), + }; + let coconut_dkg_contract_addr = app + .instantiate_contract( + coconut_dkg_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "coconut dkg", + None, + ) + .unwrap(); + + let msg = MigrateMsg { + coconut_bandwidth_address: coconut_dkg_contract_addr.to_string(), + coconut_dkg_address: coconut_dkg_contract_addr.to_string(), + }; + app.migrate_contract( + Addr::unchecked(OWNER), + multisig_contract_addr, + &msg, + multisig_code_id, + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(MEMBER1), + coconut_dkg_contract_addr.clone(), + &RegisterDealer { + bte_key_with_proof: "bte_key_with_proof".to_string(), + announce_address: "127.0.0.1:8000".to_string(), + }, + &coins(1000000000, TEST_COIN_DENOM), + ) + .unwrap(); + + for _ in 0..2 { + app.execute_contract( + Addr::unchecked(OWNER), + coconut_dkg_contract_addr.clone(), + &AdvanceEpochState {}, + &vec![], + ) + .unwrap(); + } + + let msg = CommitVerificationKeyShare { + share: "share".to_string(), + }; + let res = app + .execute_contract( + Addr::unchecked(MEMBER1), + coconut_dkg_contract_addr.clone(), + &msg, + &vec![], + ) + .unwrap(); + let proposal_id = res + .events + .into_iter() + .find(|e| &e.ty == "wasm") + .unwrap() + .attributes + .into_iter() + .find(|attr| &attr.key == "proposal_id") + .unwrap() + .value + .parse::() + .unwrap(); + assert_eq!(1, proposal_id); +} diff --git a/contracts/coconut-test/src/tests.rs b/contracts/coconut-test/src/tests.rs index 575a2c2ac1..2ba5f4b2e8 100644 --- a/contracts/coconut-test/src/tests.rs +++ b/contracts/coconut-test/src/tests.rs @@ -4,3 +4,4 @@ mod deposit_and_release; mod helpers; mod spend_credential_creates_proposal; +mod submit_vk_creates_proposal; diff --git a/contracts/multisig/cw3-flex-multisig/Cargo.toml b/contracts/multisig/cw3-flex-multisig/Cargo.toml index 0168eec05f..8afa71f251 100644 --- a/contracts/multisig/cw3-flex-multisig/Cargo.toml +++ b/contracts/multisig/cw3-flex-multisig/Cargo.toml @@ -17,12 +17,12 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -cw-utils = { version = "0.13.2" } -cw2 = { version = "0.13.2" } -cw3 = { version = "0.13.2" } -cw3-fixed-multisig = { version = "0.13.2", features = ["library"] } -cw4 = { version = "0.13.2" } -cw-storage-plus = { version = "0.13.2" } +cw-utils = { version = "0.13.4" } +cw2 = { version = "0.13.4" } +cw3 = { version = "0.13.4" } +cw3-fixed-multisig = { version = "0.13.4", features = ["library"] } +cw4 = { version = "0.13.4" } +cw-storage-plus = { version = "0.13.4" } cosmwasm-std = { version = "1.0.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } @@ -32,5 +32,5 @@ multisig-contract-common = { path= "../../../common/cosmwasm-smart-contracts/mul [dev-dependencies] cosmwasm-schema = { version = "1.0.0" } -cw4-group = { path = "../cw4-group", version = "0.13.1" } -cw-multi-test = { version = "0.13.1" } +cw4-group = { path = "../cw4-group", version = "0.13.4" } +cw-multi-test = { version = "0.13.4" } diff --git a/contracts/multisig/cw3-flex-multisig/src/contract.rs b/contracts/multisig/cw3-flex-multisig/src/contract.rs index c85d1362a7..5a56ea9745 100644 --- a/contracts/multisig/cw3-flex-multisig/src/contract.rs +++ b/contracts/multisig/cw3-flex-multisig/src/contract.rs @@ -37,11 +37,12 @@ pub fn instantiate( addr: msg.group_addr.clone(), } })?); - // This might need to be changed via a migration, due to circular dependency + // Those might need to be changed via a migration, due to circular dependency // of deploying the two contracts let coconut_bandwidth_addr = deps .api .addr_validate(&msg.coconut_bandwidth_contract_address)?; + let coconut_dkg_addr = deps.api.addr_validate(&msg.coconut_dkg_contract_address)?; let total_weight = group_addr.total_weight(&deps.querier)?; msg.threshold.validate(total_weight)?; @@ -52,6 +53,7 @@ pub fn instantiate( max_voting_period: msg.max_voting_period, group_addr, coconut_bandwidth_addr, + coconut_dkg_addr, }; CONFIG.save(deps.storage, &cfg)?; @@ -62,6 +64,7 @@ pub fn instantiate( pub fn migrate(deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result { let mut cfg = CONFIG.load(deps.storage)?; cfg.coconut_bandwidth_addr = deps.api.addr_validate(&msg.coconut_bandwidth_address)?; + cfg.coconut_dkg_addr = deps.api.addr_validate(&msg.coconut_dkg_address)?; CONFIG.save(deps.storage, &cfg)?; Ok(Default::default()) } @@ -102,8 +105,8 @@ pub fn execute_propose( // only members of the multisig can create a proposal let cfg = CONFIG.load(deps.storage)?; - // Only the coconut bandwidth contract can create proposals - if info.sender != cfg.coconut_bandwidth_addr { + // Only the coconut bandwidth or dkg contracts can create proposals + if info.sender != cfg.coconut_bandwidth_addr && info.sender != cfg.coconut_dkg_addr { return Err(ContractError::Unauthorized {}); } // The contract doesn't have any say in the voting outcome @@ -460,6 +463,7 @@ mod tests { const SOMEBODY: &str = "somebody"; const TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + const TEST_COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; fn member>(addr: T, weight: u64) -> Member { Member { @@ -539,6 +543,7 @@ mod tests { threshold, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "flex", None) .unwrap() @@ -655,6 +660,7 @@ mod tests { }, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let err = app .instantiate_contract( @@ -677,6 +683,7 @@ mod tests { threshold: Threshold::AbsoluteCount { weight: 100 }, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let err = app .instantiate_contract( @@ -699,6 +706,7 @@ mod tests { threshold: Threshold::AbsoluteCount { weight: 1 }, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let flex_addr = app .instantiate_contract( diff --git a/contracts/multisig/cw3-flex-multisig/src/state.rs b/contracts/multisig/cw3-flex-multisig/src/state.rs index 96e6146c2f..0826f1014e 100644 --- a/contracts/multisig/cw3-flex-multisig/src/state.rs +++ b/contracts/multisig/cw3-flex-multisig/src/state.rs @@ -13,6 +13,7 @@ pub struct Config { // Total weight and voters are queried from this contract pub group_addr: Cw4Contract, pub coconut_bandwidth_addr: Addr, + pub coconut_dkg_addr: Addr, } // unique items diff --git a/contracts/multisig/cw4-group/Cargo.toml b/contracts/multisig/cw4-group/Cargo.toml index ecf5ff366c..235cfeed70 100644 --- a/contracts/multisig/cw4-group/Cargo.toml +++ b/contracts/multisig/cw4-group/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cw4-group" -version = "0.13.1" +version = "0.13.4" authors = ["Ethan Frey "] edition = "2018" description = "Simple cw4 implementation of group membership controlled by admin " @@ -24,11 +24,11 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -cw-utils = { version = "0.13.2" } -cw2 = { version = "0.13.2" } -cw4 = { version = "0.13.2" } -cw-controllers = { version = "0.13.2" } -cw-storage-plus = { version = "0.13.2" } +cw-utils = { version = "0.13.4" } +cw2 = { version = "0.13.4" } +cw4 = { version = "0.13.4" } +cw-controllers = { version = "0.13.4" } +cw-storage-plus = { version = "0.13.4" } cosmwasm-std = { version = "1.0.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs index 19c8e426b4..e83c23e5e9 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -8,11 +8,11 @@ use log::*; use coconut_interface::{Credential, VerificationKey}; use validator_client::{ nymd::{ - cosmwasm_client::logs::{find_attribute, PROPOSAL_ID}, + cosmwasm_client::logs::{find_attribute, BANDWIDTH_PROPOSAL_ID}, traits::{CoconutBandwidthSigningClient, MultisigQueryClient, MultisigSigningClient}, - Coin, Fee, NymdClient, SigningNymdClient, + Coin, Fee, SigningNymdClient, }, - ApiClient, + Client, CoconutApiClient, }; use super::authenticated::RequestHandlingError; @@ -21,16 +21,16 @@ const ONE_HOUR_SEC: u64 = 3600; const MAX_FEEGRANT_UNYM: u128 = 10000; pub(crate) struct CoconutVerifier { - api_clients: Vec, - nymd_client: NymdClient, + api_clients: Vec, + nymd_client: Client, mix_denom_base: String, aggregated_verification_key: VerificationKey, } impl CoconutVerifier { pub fn new( - api_clients: Vec, - nymd_client: NymdClient, + api_clients: Vec, + nymd_client: Client, mix_denom_base: String, aggregated_verification_key: VerificationKey, ) -> Result { @@ -59,17 +59,18 @@ impl CoconutVerifier { let res = self .nymd_client + .nymd .spend_credential( Coin::new( credential.voucher_value().into(), self.mix_denom_base.clone(), ), credential.blinded_serial_number(), - self.nymd_client.address().to_string(), + self.nymd_client.nymd.address().to_string(), None, ) .await?; - let proposal_id = find_attribute(&res.logs, "wasm", PROPOSAL_ID) + let proposal_id = find_attribute(&res.logs, "wasm", BANDWIDTH_PROPOSAL_ID) .ok_or(RequestHandlingError::ProposalIdError { reason: String::from("proposal id not found"), })? @@ -79,7 +80,7 @@ impl CoconutVerifier { reason: String::from("proposal id could not be parsed to u64"), })?; - let proposal = self.nymd_client.get_proposal(proposal_id).await?; + let proposal = self.nymd_client.nymd.get_proposal(proposal_id).await?; if !credential.has_blinded_serial_number(&proposal.description)? { return Err(RequestHandlingError::ProposalIdError { reason: String::from("proposal has different serial number"), @@ -89,13 +90,13 @@ impl CoconutVerifier { let req = validator_api_requests::coconut::VerifyCredentialBody::new( credential.clone(), proposal_id, - self.nymd_client.address().clone(), + self.nymd_client.nymd.address().clone(), ); for client in self.api_clients.iter() { - let api_cosmos_addr = client.get_cosmos_address().await?.addr; self.nymd_client + .nymd .grant_allowance( - &api_cosmos_addr, + &client.cosmos_address, vec![Coin::new(MAX_FEEGRANT_UNYM, self.mix_denom_base.clone())], SystemTime::now().checked_add(Duration::from_secs(ONE_HOUR_SEC)), // It would be nice to be able to filter deeper, but for now only the msg type filter is avaialable @@ -104,20 +105,24 @@ impl CoconutVerifier { None, ) .await?; - let ret = client.verify_bandwidth_credential(&req).await; + let ret = client.api_client.verify_bandwidth_credential(&req).await; self.nymd_client + .nymd .revoke_allowance( - &api_cosmos_addr, + &client.cosmos_address, "Cleanup the previous allowance for releasing funds".to_string(), revoke_fee.clone(), ) .await?; if !ret?.verification_result { - debug!("Validator {} didn't accept the credential. It will probably vote No on the spending proposal", client.validator_api.current_url()); + debug!("Validator {} didn't accept the credential. It will probably vote No on the spending proposal", client.api_client.validator_api.current_url()); } } - self.nymd_client.execute_proposal(proposal_id, None).await?; + self.nymd_client + .nymd + .execute_proposal(proposal_id, None) + .await?; Ok(()) } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index fd31af1af7..8c2a541f12 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -25,9 +25,9 @@ use crate::config::persistence::pathfinder::GatewayPathfinder; #[cfg(feature = "coconut")] use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; #[cfg(feature = "coconut")] -use credentials::obtain_aggregate_verification_key; +use credentials::coconut::utils::obtain_aggregate_verification_key; #[cfg(feature = "coconut")] -use validator_client::nymd; +use validator_client::{Client, CoconutApiClient}; use self::storage::PersistentStorage; @@ -242,32 +242,24 @@ where #[cfg(feature = "coconut")] fn random_nymd_client( &self, - ) -> validator_client::nymd::NymdClient { + ) -> validator_client::Client { let endpoints = self.config.get_validator_nymd_endpoints(); let validator_nymd = endpoints .choose(&mut thread_rng()) .expect("The list of validators is empty"); let network_details = NymNetworkDetails::new_from_env(); - let client_config = nymd::Config::try_from_nym_network_details(&network_details) - .expect("failed to construct valid validator client config with the provided network"); - - validator_client::nymd::NymdClient::connect_with_mnemonic( - client_config, - validator_nymd.as_ref(), - self.config.get_cosmos_mnemonic(), - None, + let client_config = validator_client::Config::try_from_nym_network_details( + &network_details, ) - .expect("Could not connect with mnemonic") - } + .expect("failed to construct valid validator client config with the provided network"); - #[cfg(feature = "coconut")] - fn all_api_clients(&self) -> Vec { - self.config - .get_validator_api_endpoints() - .into_iter() - .map(validator_client::ApiClient::new) - .collect() + let mut client = Client::new_signing(client_config, self.config.get_cosmos_mnemonic()) + .expect("Could not connect with mnemonic"); + client + .change_nymd(validator_nymd.clone()) + .expect("Could not use the random nymd URL"); + client } // TODO: ask DH whether this function still makes sense in ^0.10 @@ -306,20 +298,23 @@ where } #[cfg(feature = "coconut")] - let validators_verification_key = - obtain_aggregate_verification_key(&self.config.get_validator_api_endpoints()) + let coconut_verifier = { + let nymd_client = self.random_nymd_client(); + let api_clients = CoconutApiClient::all_coconut_api_clients(&nymd_client) + .await + .expect("Could not query all api clients"); + let validators_verification_key = obtain_aggregate_verification_key(&api_clients) .await .expect("failed to contact validators to obtain their verification keys"); - #[cfg(feature = "coconut")] - let nymd_client = self.random_nymd_client(); - #[cfg(feature = "coconut")] - let coconut_verifier = CoconutVerifier::new( - self.all_api_clients(), - nymd_client, - std::env::var(network_defaults::var_names::MIX_DENOM).expect("mix denom base not set"), - validators_verification_key, - ) - .expect("Could not create coconut verifier"); + CoconutVerifier::new( + api_clients, + nymd_client, + std::env::var(network_defaults::var_names::MIX_DENOM) + .expect("mix denom base not set"), + validators_verification_key, + ) + .expect("Could not create coconut verifier") + }; let mix_forwarding_channel = self.start_packet_forwarder(); diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 90574cb063..4fb4413f99 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -72,6 +72,7 @@ credential-storage = { path = "../common/credential-storage" } credentials = { path = "../common/credentials", optional = true } crypto = { path = "../common/crypto" } logging = { path = "../common/logging"} +cw3 = { version = "0.13.4", optional = true } dkg = { path = "../common/crypto/dkg", optional = true } gateway-client = { path = "../common/client-libs/gateway-client" } inclusion-probability = { path = "../common/inclusion-probability" } @@ -93,6 +94,7 @@ version-checker = { path = "../common/version-checker" } coconut = [ "coconut-interface", "credentials", + "cw3", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut", @@ -121,5 +123,5 @@ vergen = { version = "7", default-features = false, features = [ ] } [dev-dependencies] -cw3 = "0.13.2" -cw-utils = "0.13.2" +cw3 = "0.13.4" +cw-utils = "0.13.4" diff --git a/validator-api/src/coconut/client.rs b/validator-api/src/coconut/client.rs index a09215a9ff..41fffd4856 100644 --- a/validator-api/src/coconut/client.rs +++ b/validator-api/src/coconut/client.rs @@ -5,8 +5,9 @@ use crate::coconut::error::Result; use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; -use multisig_contract_common::msg::ProposalResponse; +use cw3::ProposalResponse; use validator_client::nymd::cosmwasm_client::types::ExecuteResult; use validator_client::nymd::{AccountId, Fee, TxResponse}; @@ -15,6 +16,7 @@ pub trait Client { async fn address(&self) -> AccountId; async fn get_tx(&self, tx_hash: &str) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; + async fn list_proposals(&self) -> Result>; async fn get_spent_credential( &self, blinded_serial_number: String, @@ -23,9 +25,18 @@ pub trait Client { async fn get_self_registered_dealer_details(&self) -> Result; async fn get_current_dealers(&self) -> Result>; async fn get_dealings(&self, idx: usize) -> Result>; + async fn get_verification_key_shares(&self) -> Result>; async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; - async fn register_dealer(&self, bte_key: EncodedBTEPublicKeyWithProof) - -> Result; + async fn execute_proposal(&self, proposal_id: u64) -> Result<()>; + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, + ) -> Result; async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result; + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + ) -> Result; } diff --git a/validator-api/src/coconut/comm.rs b/validator-api/src/coconut/comm.rs index e6f7b8a1ac..42a428f153 100644 --- a/validator-api/src/coconut/comm.rs +++ b/validator-api/src/coconut/comm.rs @@ -2,28 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::coconut::error::Result; +use crate::nymd_client::Client; use coconut_interface::VerificationKey; -use credentials::obtain_aggregate_verification_key; -use url::Url; +use credentials::coconut::utils::obtain_aggregate_verification_key; +use validator_client::nymd::SigningNymdClient; +use validator_client::CoconutApiClient; #[async_trait] pub trait APICommunicationChannel { async fn aggregated_verification_key(&self) -> Result; } -pub struct QueryCommunicationChannel { - validator_apis: Vec, +pub(crate) struct QueryCommunicationChannel { + nymd_client: Client, } impl QueryCommunicationChannel { - pub fn new(validator_apis: Vec) -> Self { - QueryCommunicationChannel { validator_apis } + pub fn new(nymd_client: Client) -> Self { + QueryCommunicationChannel { nymd_client } } } #[async_trait] impl APICommunicationChannel for QueryCommunicationChannel { async fn aggregated_verification_key(&self) -> Result { - Ok(obtain_aggregate_verification_key(&self.validator_apis).await?) + let client = self.nymd_client.0.read().await; + let coconut_api_clients = CoconutApiClient::all_coconut_api_clients(&client).await?; + let vk = obtain_aggregate_verification_key(&coconut_api_clients).await?; + Ok(vk) } } diff --git a/validator-api/src/coconut/dkg/client.rs b/validator-api/src/coconut/dkg/client.rs index 87d5c926be..6850dabc1c 100644 --- a/validator-api/src/coconut/dkg/client.rs +++ b/validator-api/src/coconut/dkg/client.rs @@ -5,8 +5,11 @@ use crate::coconut::client::Client; use crate::coconut::error::CoconutError; use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState, NodeIndex}; +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; +use cw3::ProposalResponse; use validator_client::nymd::cosmwasm_client::logs::{find_attribute, NODE_INDEX}; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; use validator_client::nymd::AccountId; pub(crate) struct DkgClient { @@ -14,6 +17,10 @@ pub(crate) struct DkgClient { } impl DkgClient { + // 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; + pub(crate) fn new(nymd_client: C) -> Self where C: Client + Send + Sync + 'static, @@ -28,7 +35,14 @@ impl DkgClient { } pub(crate) async fn get_current_epoch_state(&self) -> Result { - self.inner.get_current_epoch_state().await + let mut ret = self.inner.get_current_epoch_state().await; + for _ in 0..Self::RETRIES { + if ret.is_ok() { + return ret; + } + ret = self.inner.get_current_epoch_state().await; + } + ret } pub(crate) async fn get_self_registered_dealer_details( @@ -45,14 +59,35 @@ impl DkgClient { &self, idx: usize, ) -> Result, CoconutError> { - self.inner.get_dealings(idx).await + let mut ret = self.inner.get_dealings(idx).await; + for _ in 0..Self::RETRIES { + if ret.is_ok() { + return ret; + } + ret = self.inner.get_dealings(idx).await; + } + ret + } + + pub(crate) async fn get_verification_key_shares( + &self, + ) -> Result, CoconutError> { + self.inner.get_verification_key_shares().await + } + + pub(crate) async fn list_proposals(&self) -> Result, CoconutError> { + self.inner.list_proposals().await } pub(crate) async fn register_dealer( &self, bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, ) -> Result { - let res = self.inner.register_dealer(bte_key).await?; + let res = self + .inner + .register_dealer(bte_key, announce_address) + .await?; let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) .ok_or(CoconutError::NodeIndexRecoveryError { reason: String::from("node index not found"), @@ -73,4 +108,39 @@ impl DkgClient { self.inner.submit_dealing(dealing_bytes).await?; Ok(()) } + + pub(crate) async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + ) -> Result { + let mut ret = self + .inner + .submit_verification_key_share(share.clone()) + .await; + for _ in 0..Self::RETRIES { + if let Ok(res) = ret { + return Ok(res); + } + ret = self + .inner + .submit_verification_key_share(share.clone()) + .await; + } + ret + } + + pub(crate) async fn vote_verification_key_share( + &self, + proposal_id: u64, + vote_yes: bool, + ) -> Result<(), CoconutError> { + self.inner.vote_proposal(proposal_id, vote_yes, None).await + } + + pub(crate) async fn execute_verification_key_share( + &self, + proposal_id: u64, + ) -> Result<(), CoconutError> { + self.inner.execute_proposal(proposal_id).await + } } diff --git a/validator-api/src/coconut/dkg/complaints.rs b/validator-api/src/coconut/dkg/complaints.rs index dbb44a8497..8a95645a30 100644 --- a/validator-api/src/coconut/dkg/complaints.rs +++ b/validator-api/src/coconut/dkg/complaints.rs @@ -9,38 +9,3 @@ pub(crate) enum ComplaintReason { MalformedDealing(DkgError), DealingVerificationError(DkgError), } - -// pub(crate) async fn complaint_period( -// dkg_client: &DkgClient, -// state: &mut State, -// ) -> Result<(), CoconutError> { -// let dkg_params = dkg::bte::setup(); -// let threshold = state -// .threshold() -// .expect("We should have a tentative threshold by now"); -// let dealings = dkg_client.get_dealings().await?; -// for contract_dealing in dealings { -// match Dealing::try_from_bytes(&contract_dealing.dealing) { -// Ok(dealing) => { -// if let Err(err) = -// dealing.verify(&dkg_params, threshold, &state.current_receivers(), None) -// { -// state.remove_good_dealer(&contract_dealing.dealer); -// state.add_bad_dealer( -// contract_dealing.dealer, -// ComplaintReason::DealingVerificationError(err), -// ); -// } -// } -// Err(err) => { -// state.remove_good_dealer(&contract_dealing.dealer); -// state.add_bad_dealer( -// contract_dealing.dealer, -// ComplaintReason::MalformedDealing(err), -// ); -// } -// } -// } -// -// Ok(()) -// } diff --git a/validator-api/src/coconut/dkg/controller.rs b/validator-api/src/coconut/dkg/controller.rs index 26ddc308ed..9058dc558b 100644 --- a/validator-api/src/coconut/dkg/controller.rs +++ b/validator-api/src/coconut/dkg/controller.rs @@ -3,6 +3,9 @@ use crate::coconut::dkg::client::DkgClient; use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::dkg::verification_key::{ + verification_key_finalization, verification_key_validation, +}; use crate::coconut::dkg::{ dealing::dealing_exchange, public_key::public_key_submission, verification_key::verification_key_submission, @@ -14,6 +17,7 @@ use coconut_dkg_common::types::EpochState; use dkg::bte::keys::KeyPair as DkgKeyPair; use rand::rngs::OsRng; use rand::RngCore; +use std::path::PathBuf; use std::time::Duration; use task::ShutdownListener; use tokio::time::interval; @@ -35,13 +39,15 @@ pub(crate) fn init_keypair(config: &Config) -> Result<()> { pub(crate) struct DkgController { dkg_client: DkgClient, + secret_key_path: PathBuf, + verification_key_path: PathBuf, state: State, rng: R, polling_rate: Duration, } impl DkgController { - pub(crate) fn new( + pub(crate) async fn new( config: &Config, nymd_client: nymd_client::Client, coconut_keypair: CoconutKeyPair, @@ -51,20 +57,28 @@ impl DkgController { config.decryption_key_path(), config.public_key_with_proof_path(), ))?; + if let Ok(coconut_keypair_value) = pemstore::load_keypair(&pemstore::KeyPairPath::new( + config.secret_key_path(), + config.verification_key_path(), + )) { + coconut_keypair.set(coconut_keypair_value).await; + } Ok(DkgController { dkg_client: DkgClient::new(nymd_client), - state: State::new(dkg_keypair, coconut_keypair), + secret_key_path: config.secret_key_path(), + verification_key_path: config.verification_key_path(), + state: State::new(config.get_announce_address(), dkg_keypair, coconut_keypair), rng, polling_rate: config.get_dkg_contract_polling_rate(), }) } - async fn handle_epoch_state(&mut self) -> bool { + async fn handle_epoch_state(&mut self) { match self.dkg_client.get_current_epoch_state().await { Err(e) => warn!("Could not get current epoch state {}", e), Ok(epoch_state) => { - if let Err(e) = self.state.is_consistent(epoch_state) { + if let Err(e) = self.state.is_consistent(epoch_state).await { error!( "Epoch state is corrupted - {}, the process should be terminated", e @@ -78,28 +92,38 @@ impl DkgController { dealing_exchange(&self.dkg_client, &mut self.state, self.rng.clone()).await } EpochState::VerificationKeySubmission => { - verification_key_submission(&self.dkg_client, &mut self.state).await + let keypair_path = pemstore::KeyPairPath::new( + self.secret_key_path.clone(), + self.verification_key_path.clone(), + ); + verification_key_submission( + &self.dkg_client, + &mut self.state, + &keypair_path, + ) + .await } - EpochState::InProgress => return true, + EpochState::VerificationKeyValidation => { + verification_key_validation(&self.dkg_client, &mut self.state).await + } + EpochState::VerificationKeyFinalization => { + verification_key_finalization(&self.dkg_client, &mut self.state).await + } + // Just wait, in case we need to redo dkg at some point + EpochState::InProgress => Ok(()), }; if let Err(e) = ret { warn!("Could not handle this iteration for the epoch state: {}", e); } } } - false } pub(crate) async fn run(mut self, mut shutdown: ShutdownListener) { let mut interval = interval(self.polling_rate); while !shutdown.is_shutdown() { tokio::select! { - _ = interval.tick() => { - if self.handle_epoch_state().await { - // If dkg finished, we can finish this task - break; - } - } + _ = interval.tick() => self.handle_epoch_state().await, _ = shutdown.recv() => { trace!("DkgController: Received shutdown"); } diff --git a/validator-api/src/coconut/dkg/dealing.rs b/validator-api/src/coconut/dkg/dealing.rs index 647df75797..fea2d4e98f 100644 --- a/validator-api/src/coconut/dkg/dealing.rs +++ b/validator-api/src/coconut/dkg/dealing.rs @@ -16,7 +16,6 @@ pub(crate) async fn dealing_exchange( rng: impl RngCore + Clone, ) -> Result<(), CoconutError> { if state.receiver_index().is_some() { - info!("Already have index {}", state.receiver_index().unwrap()); return Ok(()); } @@ -46,7 +45,7 @@ pub(crate) async fn dealing_exchange( .await?; } - info!("Setting index to {}", receiver_index.unwrap()); + info!("DKG: Finished submitting dealing"); state.set_receiver_index(receiver_index); Ok(()) diff --git a/validator-api/src/coconut/dkg/public_key.rs b/validator-api/src/coconut/dkg/public_key.rs index de1c8c86e6..6669db09ca 100644 --- a/validator-api/src/coconut/dkg/public_key.rs +++ b/validator-api/src/coconut/dkg/public_key.rs @@ -21,10 +21,12 @@ pub(crate) async fn public_key_submission( { details.assigned_index } else { - dkg_client.register_dealer(bte_key).await? + dkg_client + .register_dealer(bte_key, state.announce_address().to_string()) + .await? }; state.set_node_index(index); - info!("Starting dkg protocol with index {}", index); + info!("DKG: Using node index {}", index); Ok(()) } diff --git a/validator-api/src/coconut/dkg/state.rs b/validator-api/src/coconut/dkg/state.rs index 43098d771b..f6be2bc8a7 100644 --- a/validator-api/src/coconut/dkg/state.rs +++ b/validator-api/src/coconut/dkg/state.rs @@ -8,8 +8,9 @@ use coconut_dkg_common::dealer::DealerDetails; use coconut_dkg_common::types::EpochState; use cosmwasm_std::Addr; use dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof}; -use dkg::{NodeIndex, Threshold}; +use dkg::{NodeIndex, RecoveredVerificationKeys, Threshold}; use std::collections::BTreeMap; +use url::Url; // note: each dealer is also a receiver which simplifies some logic significantly #[derive(Debug)] @@ -37,40 +38,50 @@ impl TryFrom for DkgParticipant { } } +#[async_trait] pub(crate) trait ConsistentState { fn node_index_value(&self) -> Result; fn receiver_index_value(&self) -> Result; fn threshold(&self) -> Result; - fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> { + async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError>; + fn proposal_id_value(&self) -> Result; + async fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> { match epoch_state { EpochState::PublicKeySubmission => {} EpochState::DealingExchange => { self.node_index_value()?; } EpochState::VerificationKeySubmission => { - self.node_index_value()?; self.receiver_index_value()?; self.threshold()?; } - EpochState::InProgress => { - self.node_index_value()?; - self.receiver_index_value()?; - self.threshold()?; + EpochState::VerificationKeyValidation => { + self.coconut_keypair_is_some().await?; } + EpochState::VerificationKeyFinalization => { + self.proposal_id_value()?; + } + EpochState::InProgress => {} } Ok(()) } } pub(crate) struct State { + announce_address: Url, dkg_keypair: DkgKeyPair, coconut_keypair: CoconutKeyPair, node_index: Option, dealers: BTreeMap>, receiver_index: Option, threshold: Option, + recovered_vks: Vec, + proposal_id: Option, + voted_vks: bool, + executed_proposal: bool, } +#[async_trait] impl ConsistentState for State { fn node_index_value(&self) -> Result { self.node_index.ok_or(CoconutError::UnrecoverableState { @@ -98,20 +109,49 @@ impl ConsistentState for State { Ok(threshold) } } + + async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError> { + if self.coconut_keypair_is_some().await { + Ok(()) + } else { + Err(CoconutError::UnrecoverableState { + reason: String::from("Coconut keypair should have been set"), + }) + } + } + + fn proposal_id_value(&self) -> Result { + self.proposal_id.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Proposal id should have benn set"), + }) + } } impl State { - pub fn new(dkg_keypair: DkgKeyPair, coconut_keypair: CoconutKeyPair) -> Self { + pub fn new( + announce_address: Url, + dkg_keypair: DkgKeyPair, + coconut_keypair: CoconutKeyPair, + ) -> Self { State { + announce_address, dkg_keypair, coconut_keypair, node_index: None, dealers: BTreeMap::new(), receiver_index: None, threshold: None, + recovered_vks: vec![], + proposal_id: None, + voted_vks: false, + executed_proposal: false, } } + pub fn announce_address(&self) -> &Url { + &self.announce_address + } + pub fn dkg_keypair(&self) -> &DkgKeyPair { &self.dkg_keypair } @@ -154,6 +194,22 @@ impl State { .collect() } + pub fn recovered_vks(&self) -> &Vec { + &self.recovered_vks + } + + pub fn voted_vks(&self) -> bool { + self.voted_vks + } + + pub fn executed_proposal(&self) -> bool { + self.executed_proposal + } + + pub fn set_recovered_vks(&mut self, recovered_vks: Vec) { + self.recovered_vks = recovered_vks; + } + pub async fn set_coconut_keypair(&mut self, coconut_keypair: coconut_interface::KeyPair) { self.coconut_keypair.set(coconut_keypair).await } @@ -187,4 +243,16 @@ impl State { pub fn set_threshold(&mut self, threshold: Threshold) { self.threshold = Some(threshold); } + + pub fn set_proposal_id(&mut self, proposal_id: u64) { + self.proposal_id = Some(proposal_id); + } + + pub fn set_voted_vks(&mut self) { + self.voted_vks = true; + } + + pub fn set_executed_proposal(&mut self) { + self.executed_proposal = true; + } } diff --git a/validator-api/src/coconut/dkg/verification_key.rs b/validator-api/src/coconut/dkg/verification_key.rs index a900708fd2..08c352255d 100644 --- a/validator-api/src/coconut/dkg/verification_key.rs +++ b/validator-api/src/coconut/dkg/verification_key.rs @@ -5,41 +5,34 @@ use crate::coconut::dkg::client::DkgClient; use crate::coconut::dkg::complaints::ComplaintReason; use crate::coconut::dkg::state::{ConsistentState, State}; use crate::coconut::error::CoconutError; +use coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS}; +use coconut_dkg_common::verification_key::owner_from_cosmos_msgs; use coconut_interface::KeyPair as CoconutKeyPair; use cosmwasm_std::Addr; use credentials::coconut::bandwidth::{PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; +use cw3::{ProposalResponse, Status}; use dkg::bte::{decrypt_share, setup}; -use dkg::{combine_shares, Dealing}; -use nymcoconut::{KeyPair, Parameters, SecretKey}; +use dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold}; +use nymcoconut::tests::helpers::transpose_matrix; +use nymcoconut::{check_vk_pairing, Base58, KeyPair, Parameters, SecretKey, VerificationKey}; +use pemstore::KeyPairPath; use std::collections::BTreeMap; +use validator_client::nymd::cosmwasm_client::logs::find_attribute; // Filter the dealers based on what dealing they posted (or not) in the contract async fn deterministic_filter_dealers( dkg_client: &DkgClient, state: &mut State, + threshold: Threshold, ) -> Result>, CoconutError> { let mut dealings_maps = vec![]; let initial_dealers_by_addr = state.current_dealers_by_addr(); let initial_receivers = state.current_dealers_by_idx(); - let threshold = state.threshold()?; let params = setup(); - let retries = 3; for idx in 0..TOTAL_DEALINGS { - let mut try_no = 0; - let dealings = loop { - // this is a really ugly way to get the dealings, but for some reason the first query - // always fails with a RPC error. - try_no += 1; - if let Ok(dealings) = dkg_client.get_dealings(idx).await { - break dealings; - } else if try_no == retries { - return Err(CoconutError::UnrecoverableState { - reason: String::from("Could not get dealings"), - }); - } - }; + let dealings = dkg_client.get_dealings(idx).await?; let dealings_map = BTreeMap::from_iter(dealings.into_iter().filter_map(|contract_dealing| { match Dealing::try_from(&contract_dealing.dealing) { @@ -84,7 +77,8 @@ async fn deterministic_filter_dealers( } fn derive_partial_keypair( - state: &State, + state: &mut State, + threshold: Threshold, dealings_maps: Vec>, ) -> Result { let filtered_receivers_by_idx = state.current_dealers_by_idx(); @@ -92,6 +86,7 @@ fn derive_partial_keypair( let dk = state.dkg_keypair().private_key(); let node_index_value = state.receiver_index_value()?; let mut scalars = vec![]; + let mut recovered_vks = vec![]; for dealings_map in dealings_maps.into_iter() { let filtered_dealings: Vec<_> = dealings_map .into_iter() @@ -103,6 +98,13 @@ fn derive_partial_keypair( } }) .collect(); + let recovered = try_recover_verification_keys( + &filtered_dealings, + threshold, + &filtered_receivers_by_idx, + )?; + recovered_vks.push(recovered); + let shares = filtered_dealings .iter() .map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None)) @@ -116,6 +118,7 @@ fn derive_partial_keypair( )?; scalars.push(scalar); } + state.set_recovered_vks(recovered_vks); let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; let x = scalars.pop().unwrap(); @@ -128,14 +131,114 @@ fn derive_partial_keypair( pub(crate) async fn verification_key_submission( dkg_client: &DkgClient, state: &mut State, + keypair_path: &KeyPairPath, ) -> Result<(), CoconutError> { if state.coconut_keypair_is_some().await { return Ok(()); } - let dealings_maps = deterministic_filter_dealers(dkg_client, state).await?; - let coconut_keypair = derive_partial_keypair(state, dealings_maps)?; + let threshold = state.threshold()?; + let dealings_maps = deterministic_filter_dealers(dkg_client, state, threshold).await?; + let coconut_keypair = derive_partial_keypair(state, threshold, dealings_maps)?; + let vk_share = coconut_keypair.verification_key().to_bs58(); + pemstore::store_keypair(&coconut_keypair, keypair_path)?; + let res = dkg_client.submit_verification_key_share(vk_share).await?; + let proposal_id = find_attribute(&res.logs, "wasm", DKG_PROPOSAL_ID) + .ok_or(CoconutError::ProposalIdError { + reason: String::from("proposal id not found"), + })? + .value + .parse::() + .map_err(|_| CoconutError::ProposalIdError { + reason: String::from("proposal id could not be parsed to u64"), + })?; + state.set_proposal_id(proposal_id); state.set_coconut_keypair(coconut_keypair).await; + info!("DKG: Submitted own verification key"); + + Ok(()) +} + +fn validate_proposal(proposal: &ProposalResponse) -> Option<(Addr, u64)> { + if proposal.status == Status::Open { + if let Some(owner) = owner_from_cosmos_msgs(&proposal.msgs) { + return Some((owner, proposal.id)); + } + } + None +} + +pub(crate) async fn verification_key_validation( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.voted_vks() { + return Ok(()); + } + + let vk_shares = dkg_client.get_verification_key_shares().await?; + let proposal_ids = BTreeMap::from_iter( + dkg_client + .list_proposals() + .await? + .iter() + .filter_map(validate_proposal), + ); + let filtered_receivers_by_idx: Vec<_> = + state.current_dealers_by_idx().keys().copied().collect(); + let recovered_partials: Vec<_> = state + .recovered_vks() + .iter() + .map(|recovered_vk| recovered_vk.recovered_partials.clone()) + .collect(); + let recovered_partials = transpose_matrix(recovered_partials); + let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; + for contract_share in vk_shares { + if let Some(proposal_id) = proposal_ids.get(&contract_share.owner).copied() { + match VerificationKey::try_from_bs58(contract_share.share) { + Ok(vk) => { + if let Some(idx) = filtered_receivers_by_idx + .iter() + .position(|node_index| contract_share.node_index == *node_index) + { + if !check_vk_pairing(¶ms, &recovered_partials[idx], &vk) { + dkg_client + .vote_verification_key_share(proposal_id, false) + .await?; + } else { + dkg_client + .vote_verification_key_share(proposal_id, true) + .await?; + } + } + } + Err(_) => { + dkg_client + .vote_verification_key_share(proposal_id, false) + .await? + } + } + } + } + state.set_voted_vks(); + info!("DKG: Validated the other verification keys"); + Ok(()) +} + +pub(crate) async fn verification_key_finalization( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.executed_proposal() { + return Ok(()); + } + + let proposal_id = state.proposal_id_value()?; + dkg_client + .execute_verification_key_share(proposal_id) + .await?; + state.set_executed_proposal(); + info!("DKG: Finalized own verification key on chain"); Ok(()) } diff --git a/validator-api/src/coconut/error.rs b/validator-api/src/coconut/error.rs index 4bf9ba0343..9b6fadf957 100644 --- a/validator-api/src/coconut/error.rs +++ b/validator-api/src/coconut/error.rs @@ -20,6 +20,9 @@ pub type Result = std::result::Result; #[derive(Debug, Error)] pub enum CoconutError { + #[error("{0}")] + IOError(#[from] std::io::Error), + #[error("Could not parse Ed25519 data")] Ed25519ParseError(#[from] Ed25519RecoveryError), @@ -93,6 +96,9 @@ pub enum CoconutError { #[error("DKG has not finished yet in order to derive the coconut key")] KeyPairNotDerivedYet, + + #[error("There was a problem with the proposal id: {reason}")] + ProposalIdError { reason: String }, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index 7902aea36a..94bc96608d 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -31,8 +31,7 @@ use crypto::shared_key::new_ephemeral_shared_key; use crypto::symmetric::stream_cipher; use keypair::KeyPair; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_client::nymd::{Coin, Fee}; use validator_client::validator_api::routes::{BANDWIDTH, COCONUT_ROUTES}; @@ -186,8 +185,6 @@ impl InternalSignRequest { ), routes![ post_blind_sign, - get_verification_key, - get_cosmos_address, post_partial_bandwidth_credential, verify_bandwidth_credential ], @@ -258,26 +255,6 @@ pub async fn post_partial_bandwidth_credential( Ok(Json(v)) } -#[get("/verification-key")] -pub async fn get_verification_key( - state: &RocketState, -) -> Result> { - state - .key_pair - .get() - .await - .as_ref() - .map(|keypair| Json(VerificationKeyResponse::new(keypair.verification_key()))) - .ok_or(CoconutError::KeyPairNotDerivedYet) -} - -#[get("/cosmos-address")] -pub async fn get_cosmos_address(state: &RocketState) -> Result> { - Ok(Json(CosmosAddressResponse::new( - state.client.address().await, - ))) -} - #[post("/verify-bandwidth-credential", data = "")] pub async fn verify_bandwidth_credential( verify_credential_body: Json, diff --git a/validator-api/src/coconut/tests.rs b/validator-api/src/coconut/tests.rs index 2f37a5ab0c..842194b913 100644 --- a/validator-api/src/coconut/tests.rs +++ b/validator-api/src/coconut/tests.rs @@ -19,21 +19,18 @@ use credentials::coconut::params::{ }; use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; -use multisig_contract_common::msg::ProposalResponse; use nymcoconut::tests::helpers::theta_from_keys_and_attributes; use nymcoconut::{ - prepare_blind_sign, ttp_keygen, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, + prepare_blind_sign, ttp_keygen, Base58, BlindSignRequest, BlindedSignature, Parameters, }; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_client::nymd::Coin; use validator_client::nymd::{tx::Hash, AccountId, DeliverTx, Event, Fee, Tag, TxResponse}; use validator_client::validator_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_COSMOS_ADDRESS, - COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, COCONUT_ROUTES, COCONUT_VERIFICATION_KEY, - COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, + API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, + COCONUT_ROUTES, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, }; use crate::coconut::State; @@ -41,8 +38,10 @@ use crate::ValidatorApiStorage; use async_trait::async_trait; use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use contracts_common::dealings::ContractSafeBytes; use crypto::asymmetric::{encryption, identity}; +use cw3::ProposalResponse; use rand_07::rngs::OsRng; use rocket::http::Status; use rocket::local::asynchronous::Client; @@ -107,6 +106,10 @@ impl super::client::Client for DummyClient { }) } + async fn list_proposals(&self) -> Result> { + todo!() + } + async fn get_spent_credential( &self, blinded_serial_number: String, @@ -133,7 +136,11 @@ impl super::client::Client for DummyClient { todo!() } - async fn get_dealings(&self, idx: usize) -> Result> { + async fn get_dealings(&self, _idx: usize) -> Result> { + todo!() + } + + async fn get_verification_key_shares(&self) -> Result> { todo!() } @@ -153,14 +160,26 @@ impl super::client::Client for DummyClient { Ok(()) } + async fn execute_proposal(&self, _proposal_id: u64) -> Result<()> { + todo!() + } + async fn register_dealer( &self, - bte_key: EncodedBTEPublicKeyWithProof, + _bte_key: EncodedBTEPublicKeyWithProof, + _announce_address: String, ) -> Result { todo!() } - async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result { + async fn submit_dealing(&self, _dealing_bytes: ContractSafeBytes) -> Result { + todo!() + } + + async fn submit_verification_key_share( + &self, + _share: VerificationKeyShare, + ) -> Result { todo!() } } @@ -205,65 +224,6 @@ pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse { } } -async fn check_signer_verif_key(key_pair: KeyPair) { - let verification_key = key_pair.verification_key(); - - let mut db_dir = std::env::temp_dir(); - db_dir.push(&verification_key.to_bs58()[..8]); - let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair.set(key_pair).await; - - let rocket = rocket::build().attach(InternalSignRequest::stage( - nymd_client, - TEST_COIN_DENOM.to_string(), - staged_key_pair, - comm_channel, - storage, - )); - - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let response = client - .get(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFICATION_KEY - )) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - - // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 - // assert!(response - // .into_json::() - // .await - // .is_some()); - let verification_key_response = - serde_json::from_str::(&response.into_string().await.unwrap()) - .unwrap(); - assert_eq!(verification_key_response.key, verification_key); -} - -#[tokio::test] -async fn multiple_verification_key() { - let params = Parameters::new(4).unwrap(); - let num_authorities = 4; - - let key_pairs = ttp_keygen(¶ms, num_authorities, num_authorities).unwrap(); - for key_pair in key_pairs.into_iter() { - check_signer_verif_key(key_pair).await; - } -} - #[tokio::test] async fn signed_before() { let tx_hash = @@ -697,49 +657,6 @@ async fn signature_test() { ); } -#[tokio::test] -async fn get_cosmos_address() { - let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - let nymd_client = DummyClient::new( - validator_address.clone(), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); - let mut db_dir = std::env::temp_dir(); - let key_pair = ttp_keygen(&Parameters::new(4).unwrap(), 1, 1) - .unwrap() - .remove(0); - db_dir.push(&key_pair.verification_key().to_bs58()[..8]); - let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); - let staged_key_pair = crate::coconut::KeyPair::new(); - staged_key_pair.set(key_pair).await; - let rocket = rocket::build().attach(InternalSignRequest::stage( - nymd_client, - TEST_COIN_DENOM.to_string(), - staged_key_pair, - comm_channel, - storage.clone(), - )); - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let response = client - .get(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_COSMOS_ADDRESS - )) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let cosmos_addr_response = - serde_json::from_str::(&response.into_string().await.unwrap()) - .unwrap(); - assert_eq!(validator_address, cosmos_addr_response.addr); -} - #[tokio::test] async fn verification_of_bandwidth_credential() { // Setup variables diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 6b6196eb41..e08b3bb20a 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::template::config_template; +use config::defaults::DEFAULT_VALIDATOR_API_PORT; use config::NymConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -93,6 +94,11 @@ pub struct Base { local_validator: Url, + /// Address announced to the directory server for the clients to connect to. + // It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + // later on by using name resolvable with a DNS query, such as `nymtech.net`. + announce_address: Url, + /// Address of the validator contract managing the network mixnet_contract_address: String, @@ -102,11 +108,17 @@ pub struct Base { impl Default for Base { fn default() -> Self { + let default_validator: Url = DEFAULT_LOCAL_VALIDATOR + .parse() + .expect("default local validator is malformed!"); + let mut default_announce_address = default_validator.clone(); + default_announce_address + .set_port(Some(DEFAULT_VALIDATOR_API_PORT)) + .expect("default local validator is malformed!"); Base { id: String::default(), - local_validator: DEFAULT_LOCAL_VALIDATOR - .parse() - .expect("default local validator is malformed!"), + local_validator: default_validator, + announce_address: default_announce_address, mixnet_contract_address: String::default(), mnemonic: "exact antique hybrid width raise anchor puzzle degree fee quit long crack net vague hip despair write put useless civil mechanic broom music day".to_string(), } @@ -270,8 +282,11 @@ pub struct CoconutSigner { /// Specifies whether rewarding service is enabled in this process. enabled: bool, - /// Path to the signing keypair - keypair_path: PathBuf, + /// Path to the coconut verification key. + verification_key_path: PathBuf, + + /// Path to the coconut secret key. + secret_key_path: PathBuf, /// Path to the dkg dealer decryption key. decryption_key_path: PathBuf, @@ -281,16 +296,21 @@ pub struct CoconutSigner { /// Duration of the interval for polling the dkg contract. dkg_contract_polling_rate: Duration, - - /// Specifies list of all validators on the network issuing coconut credentials. - /// A special care must be taken to ensure they are in correct order. - /// The list must also contain THIS validator that is running the test - all_validator_apis: Vec, } impl CoconutSigner { pub const DKG_DECRYPTION_KEY_FILE: &'static str = "dkg_decryption_key.pem"; pub const DKG_PUBLIC_KEY_WITH_PROOF_FILE: &'static str = "dkg_public_key_with_proof.pem"; + pub const COCONUT_VERIFICATION_KEY_FILE: &'static str = "coconut_verification_key.pem"; + pub const COCONUT_SECRET_KEY_FILE: &'static str = "coconut_secret_key.pem"; + + fn default_coconut_verification_key_path() -> PathBuf { + Config::default_data_directory(None).join(Self::COCONUT_VERIFICATION_KEY_FILE) + } + + fn default_coconut_secret_key_path() -> PathBuf { + Config::default_data_directory(None).join(Self::COCONUT_SECRET_KEY_FILE) + } fn default_dkg_decryption_key_path() -> PathBuf { Config::default_data_directory(None).join(Self::DKG_DECRYPTION_KEY_FILE) @@ -305,11 +325,11 @@ impl Default for CoconutSigner { fn default() -> Self { Self { enabled: Default::default(), - keypair_path: Default::default(), + verification_key_path: CoconutSigner::default_coconut_verification_key_path(), + secret_key_path: CoconutSigner::default_coconut_secret_key_path(), decryption_key_path: CoconutSigner::default_dkg_decryption_key_path(), public_key_with_proof_path: CoconutSigner::default_dkg_public_key_with_proof_path(), dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE, - all_validator_apis: Default::default(), } } } @@ -325,6 +345,10 @@ impl Config { Config::default_data_directory(Some(id)).join(NodeStatusAPI::DB_FILE); self.network_monitor.credentials_database_path = Config::default_data_directory(Some(id)).join(NetworkMonitor::DB_FILE); + self.coconut_signer.verification_key_path = Config::default_data_directory(Some(id)) + .join(CoconutSigner::COCONUT_VERIFICATION_KEY_FILE); + self.coconut_signer.secret_key_path = + Config::default_data_directory(Some(id)).join(CoconutSigner::COCONUT_SECRET_KEY_FILE); self.coconut_signer.decryption_key_path = Config::default_data_directory(Some(id)).join(CoconutSigner::DKG_DECRYPTION_KEY_FILE); self.coconut_signer.public_key_with_proof_path = Config::default_data_directory(Some(id)) @@ -358,6 +382,12 @@ impl Config { self } + #[cfg(feature = "coconut")] + pub fn with_announce_address(mut self, announce_address: Url) -> Self { + self.base.announce_address = announce_address; + self + } + pub fn with_custom_mixnet_contract>(mut self, mixnet_contract: S) -> Self { self.base.mixnet_contract_address = mixnet_contract.into(); self @@ -368,18 +398,6 @@ impl Config { self } - #[cfg(feature = "coconut")] - pub fn with_keypair_path(mut self, keypair_path: PathBuf) -> Self { - self.coconut_signer.keypair_path = keypair_path; - self - } - - #[cfg(feature = "coconut")] - pub fn with_custom_validator_apis(mut self, validator_api_urls: Vec) -> Self { - self.coconut_signer.all_validator_apis = validator_api_urls; - self - } - pub fn with_minimum_interval_monitor_threshold(mut self, threshold: u8) -> Self { self.rewarding.minimum_interval_monitor_threshold = threshold; self @@ -426,6 +444,11 @@ impl Config { self.base.local_validator.clone() } + #[cfg(feature = "coconut")] + pub fn get_announce_address(&self) -> Url { + self.base.announce_address.clone() + } + pub fn get_mixnet_contract_address(&self) -> String { self.base.mixnet_contract_address.clone() } @@ -487,8 +510,13 @@ impl Config { } #[cfg(feature = "coconut")] - pub fn _keypair_path(&self) -> PathBuf { - self.coconut_signer.keypair_path.clone() + pub fn verification_key_path(&self) -> PathBuf { + self.coconut_signer.verification_key_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn secret_key_path(&self) -> PathBuf { + self.coconut_signer.secret_key_path.clone() } #[cfg(feature = "coconut")] @@ -506,12 +534,6 @@ impl Config { self.coconut_signer.dkg_contract_polling_rate } - // fix dead code warnings as this method is only ever used with coconut feature - #[cfg(feature = "coconut")] - pub fn get_all_validator_api_endpoints(&self) -> Vec { - self.coconut_signer.all_validator_apis.clone() - } - // TODO: Remove if still unused #[allow(dead_code)] pub fn get_minimum_interval_monitor_threshold(&self) -> u8 { diff --git a/validator-api/src/config/template.rs b/validator-api/src/config/template.rs index fdb9ae6258..3d745f30ed 100644 --- a/validator-api/src/config/template.rs +++ b/validator-api/src/config/template.rs @@ -16,6 +16,11 @@ id = '{{ base.id }}' # Validator server to which the API will be getting information about the network. local_validator = '{{ base.local_validator }}' +# Address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net`. +announce_address = '{{ base.announce_address }}' + # Address of the validator contract managing the network. mixnet_contract_address = '{{ base.mixnet_contract_address }}' @@ -93,8 +98,11 @@ minimum_interval_monitor_threshold = {{ rewarding.minimum_interval_monitor_thres # Specifies whether rewarding service is enabled in this process. enabled = {{ coconut_signer.enabled }} -# Path to the signing keypair -keypair_path = '{{ coconut_signer.keypair_path }}' +# Path to the coconut verification key +verification_key_path = '{{ coconut_signer.verification_key_path }}' + +# Path to the coconut verification key +secret_key_path = '{{ coconut_signer.secret_key_path }}' # Path to the dkg dealer decryption key decryption_key_path = '{{ coconut_signer.decryption_key_path }}' @@ -102,14 +110,5 @@ decryption_key_path = '{{ coconut_signer.decryption_key_path }}' # Path to the dkg dealer public key with proof public_key_with_proof_path = '{{ coconut_signer.public_key_with_proof_path }}' -# Specifies list of all validators on the network issuing coconut credentials. -# A special care must be taken to ensure they are in correct order. -# The list must also contain THIS validator that is running the test -all_validator_apis = [ - {{#each coconut_signer.all_validator_apis }} - '{{this}}', - {{/each}} -] - "# } diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index ff44f6057e..5116896858 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -12,8 +12,6 @@ use crate::nymd_client::Client; use crate::storage::ValidatorApiStorage; use ::config::defaults::mainnet::read_var_if_not_default; use ::config::defaults::setup_env; -#[cfg(feature = "coconut")] -use ::config::defaults::var_names::API_VALIDATOR; use ::config::defaults::var_names::{CONFIGURED, MIXNET_CONTRACT_ADDRESS, MIX_DENOM}; use ::config::NymConfig; use anyhow::Result; @@ -35,6 +33,8 @@ use std::time::Duration; use std::{fs, process}; use task::ShutdownNotifier; use tokio::sync::Notify; +#[cfg(feature = "coconut")] +use url::Url; use validator_client::nymd::SigningNymdClient; use crate::epoch_operations::RewardedSetUpdater; @@ -71,9 +71,7 @@ const NYMD_VALIDATOR_ARG: &str = "nymd-validator"; const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode"; #[cfg(feature = "coconut")] -const API_VALIDATORS_ARG: &str = "api-validators"; -#[cfg(feature = "coconut")] -const KEYPAIR_ARG: &str = "keypair"; +const ANNOUNCE_ADDRESS: &str = "announce-address"; #[cfg(feature = "coconut")] const COCONUT_ENABLED: &str = "enable-coconut"; @@ -193,21 +191,15 @@ fn parse_args() -> ArgMatches { #[cfg(feature = "coconut")] let base_app = base_app .arg( - Arg::with_name(KEYPAIR_ARG) - .help("Path to the secret key file") - .takes_value(true) - .long(KEYPAIR_ARG), - ) - .arg( - Arg::with_name(API_VALIDATORS_ARG) - .help("specifies list of all validators on the network issuing coconut credentials. Ensure they are properly ordered") - .long(API_VALIDATORS_ARG) - .takes_value(true) + Arg::with_name(ANNOUNCE_ADDRESS) + .help("Announced address where coconut clients will connect.") + .long(ANNOUNCE_ADDRESS) + .takes_value(true), ) .arg( Arg::with_name(COCONUT_ENABLED) .help("Flag to indicate whether coconut signer authority is enabled on this API") - .requires_all(&[KEYPAIR_ARG, MNEMONIC_ARG, API_VALIDATORS_ARG]) + .requires_all(&[MNEMONIC_ARG, ANNOUNCE_ADDRESS]) .long(COCONUT_ENABLED), ); base_app.get_matches() @@ -276,12 +268,10 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { } #[cfg(feature = "coconut")] - if let Some(raw_validators) = matches.value_of(API_VALIDATORS_ARG) { - config = config.with_custom_validator_apis(::config::parse_validators(raw_validators)); - } else if std::env::var(CONFIGURED).is_ok() { - if let Some(raw_validators) = read_var_if_not_default(API_VALIDATOR) { - config = config.with_custom_validator_apis(::config::parse_validators(&raw_validators)) - } + if let Some(announce_address) = matches.value_of(ANNOUNCE_ADDRESS) { + config = config.with_announce_address( + Url::parse(announce_address).expect("Could not parse announce address"), + ); } if let Some(raw_validator) = matches.value_of(NYMD_VALIDATOR_ARG) { @@ -338,11 +328,6 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { ) } - #[cfg(feature = "coconut")] - if let Some(keypair_path) = matches.value_of(KEYPAIR_ARG) { - config = config.with_keypair_path(keypair_path.into()) - } - if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) { config = config.with_disabled_credentials_mode(false) } @@ -385,6 +370,7 @@ fn setup_liftoff_notify(notify: Arc) -> AdHoc { fn setup_network_monitor<'a>( config: &'a Config, + _nymd_client: Client, system_version: &str, rocket: &Rocket, ) -> Option> { @@ -398,6 +384,7 @@ fn setup_network_monitor<'a>( Some(NetworkMonitorBuilder::new( config, + _nymd_client, system_version, node_status_storage, validator_cache, @@ -453,10 +440,10 @@ async fn setup_rocket( #[cfg(feature = "coconut")] let rocket = if config.get_coconut_signer_enabled() { rocket.attach(InternalSignRequest::stage( - _nymd_client, + _nymd_client.clone(), _mix_denom, coconut_keypair, - QueryCommunicationChannel::new(config.get_all_validator_api_endpoints()), + QueryCommunicationChannel::new(_nymd_client), storage.clone().unwrap(), )) } else { @@ -554,7 +541,12 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { coconut_keypair.clone(), ) .await?; - let monitor_builder = setup_network_monitor(&config, system_version, &rocket); + let monitor_builder = setup_network_monitor( + &config, + signing_nymd_client.clone(), + system_version, + &rocket, + ); let validator_cache = rocket.state::().unwrap().clone(); let node_status_cache = rocket.state::().unwrap().clone(); @@ -562,7 +554,8 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { #[cfg(feature = "coconut")] { let dkg_controller = - DkgController::new(&config, signing_nymd_client.clone(), coconut_keypair, OsRng)?; + DkgController::new(&config, signing_nymd_client.clone(), coconut_keypair, OsRng) + .await?; let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { dkg_controller.run(shutdown_listener).await }); } diff --git a/validator-api/src/network_monitor/mod.rs b/validator-api/src/network_monitor/mod.rs index 57e1d64e61..8b85d5fd5a 100644 --- a/validator-api/src/network_monitor/mod.rs +++ b/validator-api/src/network_monitor/mod.rs @@ -7,6 +7,7 @@ use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; use std::sync::Arc; use task::ShutdownNotifier; +use validator_client::nymd::SigningNymdClient; use crate::config::Config; use crate::contract_cache::ValidatorCache; @@ -20,6 +21,7 @@ use crate::network_monitor::monitor::receiver::{ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; +use crate::nymd_client::Client; use crate::storage::ValidatorApiStorage; pub(crate) mod chunker; @@ -32,6 +34,7 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a Config, + _nymd_client: Client, system_version: String, node_status_storage: ValidatorApiStorage, validator_cache: ValidatorCache, @@ -40,12 +43,14 @@ pub(crate) struct NetworkMonitorBuilder<'a> { impl<'a> NetworkMonitorBuilder<'a> { pub(crate) fn new( config: &'a Config, + _nymd_client: Client, system_version: &str, node_status_storage: ValidatorApiStorage, validator_cache: ValidatorCache, ) -> Self { NetworkMonitorBuilder { config, + _nymd_client, system_version: system_version.to_string(), node_status_storage, validator_cache, @@ -74,11 +79,18 @@ impl<'a> NetworkMonitorBuilder<'a> { ); #[cfg(feature = "coconut")] - let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_credentials_database_path()) - .await, - self.config.get_all_validator_api_endpoints(), - ); + let bandwidth_controller = { + let client = self._nymd_client.0.read().await; + let coconut_api_clients = + validator_client::CoconutApiClient::all_coconut_api_clients(&client) + .await + .expect("Could not query api clients"); + BandwidthController::new( + credential_storage::initialise_storage(self.config.get_credentials_database_path()) + .await, + coconut_api_clients, + ) + }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_credentials_database_path()) diff --git a/validator-api/src/network_monitor/monitor/sender.rs b/validator-api/src/network_monitor/monitor/sender.rs index 3689a4b6db..4ec0e9ce45 100644 --- a/validator-api/src/network_monitor/monitor/sender.rs +++ b/validator-api/src/network_monitor/monitor/sender.rs @@ -90,15 +90,6 @@ struct FreshGatewayClientData { gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc, gateway_response_timeout: Duration, - - // I guess in the future this struct will require aggregated verification key and.... - // ... something for obtaining actual credential - - // TODO: - // SECURITY: - // for coconut bandwidth credentials we currently have no double spending protection, just to - // get things running we're re-using the same credential for all gateways all the time. - // THIS IS VERY BAD!! bandwidth_controller: BandwidthController, disabled_credentials_mode: bool, } @@ -140,8 +131,6 @@ pub(crate) struct PacketSender { // behaviour is unlikely. active_gateway_clients: ActiveGatewayClients, - // I guess that will be required later on if credentials are got per gateway - // aggregated_verification_key: Arc, fresh_gateway_client_data: Arc, gateway_connection_timeout: Duration, max_concurrent_clients: usize, diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index d2fd06acdb..0abefc0875 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -30,9 +30,11 @@ use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsRe #[cfg(feature = "coconut")] use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; #[cfg(feature = "coconut")] +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +#[cfg(feature = "coconut")] use contracts_common::dealings::ContractSafeBytes; #[cfg(feature = "coconut")] -use multisig_contract_common::msg::ProposalResponse; +use cw3::ProposalResponse; #[cfg(feature = "coconut")] use validator_client::nymd::{ cosmwasm_client::types::ExecuteResult, @@ -291,6 +293,10 @@ where Ok(self.0.read().await.nymd.get_proposal(proposal_id).await?) } + async fn list_proposals(&self) -> crate::coconut::error::Result> { + Ok(self.0.read().await.get_all_nymd_proposals().await?) + } + async fn get_spent_credential( &self, blinded_serial_number: String, @@ -332,6 +338,17 @@ where Ok(self.0.read().await.get_all_nymd_epoch_dealings(idx).await?) } + async fn get_verification_key_shares( + &self, + ) -> crate::coconut::error::Result> { + Ok(self + .0 + .read() + .await + .get_all_nymd_verification_key_shares() + .await?) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -347,16 +364,27 @@ where Ok(()) } + async fn execute_proposal(&self, proposal_id: u64) -> crate::coconut::error::Result<()> { + self.0 + .read() + .await + .nymd + .execute_proposal(proposal_id, None) + .await?; + Ok(()) + } + async fn register_dealer( &self, bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, ) -> Result { Ok(self .0 .write() .await .nymd - .register_dealer(bte_key, None) + .register_dealer(bte_key, announce_address, None) .await?) } @@ -372,4 +400,17 @@ where .submit_dealing_bytes(dealing_bytes, None) .await?) } + + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + ) -> crate::coconut::error::Result { + Ok(self + .0 + .write() + .await + .nymd + .submit_verification_key_share(share, None) + .await?) + } } From 1cec2ddff0db0e16316cb27f4e8bc98cc95cd303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Tue, 22 Nov 2022 14:33:35 +0200 Subject: [PATCH 35/75] Remove debugging transaction (#1788) --- .../coconut-dkg/src/msg.rs | 6 --- contracts/coconut-dkg/src/lib.rs | 43 ------------------- 2 files changed, 49 deletions(-) diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index dd99dde2c0..dbff18b590 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -36,12 +36,6 @@ pub enum ExecuteMsg { }, AdvanceEpochState {}, - - // DEBUG ONLY TXs. THEY SHALL BE REMOVED BEFORE FINALISING THE CODE - // only exists for debugging purposes on local network to reset the entire state of the contract - DebugUnsafeResetAll { - init_msg: InstantiateMsg, - }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index 7787776306..4a7a7407d3 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -89,53 +89,10 @@ pub fn execute( deps, info, owner, ) } - ExecuteMsg::DebugUnsafeResetAll { init_msg } => { - reset_contract_state(deps, env, info, init_msg) - } ExecuteMsg::AdvanceEpochState {} => advance_epoch_state(deps, info), } } -fn reset_contract_state( - mut deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - init_msg: InstantiateMsg, -) -> Result { - ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - // this resets the epoch - instantiate(deps.branch(), env, info, init_msg)?; - - // clear all dealings, public keys, etc - let current = dealers::storage::current_dealers() - .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .collect::, _>>()?; - let past = dealers::storage::past_dealers() - .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .collect::, _>>()?; - let dealings = dealings::storage::DEALINGS_BYTES[0] - .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .collect::, _>>()?; - - for dealer in current { - dealers::storage::current_dealers().remove(deps.storage, &dealer)?; - } - - for dealer in past { - dealers::storage::past_dealers().remove(deps.storage, &dealer)?; - } - - for addr in dealings { - for map in dealings::storage::DEALINGS_BYTES { - map.remove(deps.storage, &addr); - } - } - - dealers::storage::NODE_INDEX_COUNTER.save(deps.storage, &0u64)?; - - Ok(Response::default()) -} - #[entry_point] pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { let response = match msg { From 2e2f2bb70257bc8535dfbcd175ed5ea609d3d093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Tue, 22 Nov 2022 15:27:34 +0100 Subject: [PATCH 36/75] atty is unmaintained --- deny.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/deny.toml b/deny.toml index 38d21db403..4bed0dce56 100644 --- a/deny.toml +++ b/deny.toml @@ -76,6 +76,7 @@ notice = "warn" ignore = [ "RUSTSEC-2020-0159", "RUSTSEC-2020-0071", + "RUSTSEC-2021-0145", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories From e8738451789436a64b71d7f97552e0271c6cd817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 10:09:22 +0100 Subject: [PATCH 37/75] Fix some client send unwraps encountered during use (#1787) --- .../real_messages_control/real_traffic_stream.rs | 2 +- clients/socks5/src/socks/mixnet_responses.rs | 11 +++++++++-- .../socks5/proxy-helpers/src/connection_controller.rs | 8 +++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 9f42232db5..d6f34a5460 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -253,7 +253,7 @@ where }; if let Err(err) = self.mix_tx.send(vec![next_message]).await { - log::error!("Failed to send - channel closed: {}", err); + log::error!("Failed to send: {}", err); } // notify ack controller about sending our message only after we actually managed to push it diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 5bd83bee85..760ec36d24 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -18,9 +18,16 @@ pub(crate) struct MixnetResponseListener { impl Drop for MixnetResponseListener { fn drop(&mut self) { - self.buffer_requester + if let Err(err) = self + .buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) - .expect("the buffer request failed!") + { + if self.shutdown.is_shutdown_poll() { + log::debug!("The buffer request failed: {}", err); + } else { + log::error!("The buffer request failed: {}", err); + } + } } } diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 16b370058d..d78915d2a5 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -137,7 +137,13 @@ impl Controller { self.recently_closed.insert(conn_id); // Announce closed connections, currently used by the `OutQueueControl`. - self.closed_connection_tx.unbounded_send(conn_id).unwrap(); + if let Err(err) = self.closed_connection_tx.unbounded_send(conn_id) { + if self.shutdown.is_shutdown_poll() { + log::debug!("Failed to send: {}", err); + } else { + log::error!("Failed to send: {}", err); + } + } } fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec, is_closed: bool) { From 236594f0c6d05c712bf9db743f6d4419c150ede4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 10:09:50 +0100 Subject: [PATCH 38/75] Fix clippy::derive-partial-eq-without-eq (#1790) --- common/crypto/dkg/src/bte/keys.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/crypto/dkg/src/bte/keys.rs b/common/crypto/dkg/src/bte/keys.rs index a7962c2797..9412790db8 100644 --- a/common/crypto/dkg/src/bte/keys.rs +++ b/common/crypto/dkg/src/bte/keys.rs @@ -135,7 +135,7 @@ impl PublicKeyWithProof { #[derive(Debug, Zeroize)] #[zeroize(drop)] -#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct DecryptionKey { // g1^rho pub(crate) a: G1Projective, From f4fb0d6d6cd9bb72a0f694ec25215e7e93e1d3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Wed, 23 Nov 2022 12:10:24 +0200 Subject: [PATCH 39/75] Remove required deposit from signers (#1791) --- .../src/nymd/traits/dkg_query_client.rs | 10 +---- .../src/nymd/traits/dkg_signing_client.rs | 4 +- .../coconut-dkg/src/dealer.rs | 9 ++-- .../coconut-dkg/src/msg.rs | 1 - .../coconut-dkg/src/types.rs | 12 ------ contracts/coconut-dkg/src/constants.rs | 4 -- .../coconut-dkg/src/dealers/transactions.rs | 41 +------------------ contracts/coconut-dkg/src/lib.rs | 9 +--- .../src/submit_vk_creates_proposal.rs | 2 +- 9 files changed, 10 insertions(+), 82 deletions(-) diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs index 5e4d5a7388..561b9f44ef 100644 --- a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs @@ -8,7 +8,7 @@ use coconut_dkg_common::dealer::{ DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse, }; use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; -use coconut_dkg_common::types::{EpochState, MinimumDepositResponse}; +use coconut_dkg_common::types::EpochState; use coconut_dkg_common::verification_key::PagedVKSharesResponse; use cosmrs::AccountId; @@ -30,7 +30,6 @@ pub trait DkgQueryClient { page_limit: Option, ) -> Result; - async fn get_deposit_amount(&self) -> Result; async fn get_dealings_paged( &self, idx: usize, @@ -95,13 +94,6 @@ where .await } - async fn get_deposit_amount(&self) -> Result { - let request = DkgQueryMsg::GetDepositAmount {}; - self.client - .query_contract_smart(self.coconut_dkg_contract_address(), &request) - .await - } - async fn get_dealings_paged( &self, idx: usize, diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs index d55664f0d3..b0d1dfaf08 100644 --- a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs @@ -3,7 +3,6 @@ use crate::nymd::cosmwasm_client::types::ExecuteResult; use crate::nymd::error::NymdError; -use crate::nymd::traits::dkg_query_client::DkgQueryClient; use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; use async_trait::async_trait; use coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg; @@ -48,7 +47,6 @@ where bte_key_with_proof: bte_key, announce_address, }; - let deposit = self.get_deposit_amount().await?; self.client .execute( @@ -57,7 +55,7 @@ where &req, fee.unwrap_or_default(), format!("registering {} as a dealer", self.address()), - vec![deposit.amount.into()], + vec![], ) .await } diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs index 0b60fec2e8..7b24be4869 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -2,17 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex}; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::Addr; use serde::{Deserialize, Serialize}; -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub struct DealerDetails { pub address: Addr, pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, pub announce_address: String, pub assigned_index: NodeIndex, - pub deposit: Coin, } #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] @@ -29,7 +28,7 @@ impl DealerType { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub struct DealerDetailsResponse { pub details: Option, @@ -45,7 +44,7 @@ impl DealerDetailsResponse { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub struct PagedDealerResponse { pub dealers: Vec, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs index dbff18b590..357e56b968 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -53,7 +53,6 @@ pub enum QueryMsg { limit: Option, start_after: Option, }, - GetDepositAmount {}, GetDealing { idx: u64, limit: Option, diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index d2696f2941..ffb1653919 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -79,15 +79,3 @@ impl EpochState { states } } - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -pub struct MinimumDepositResponse { - pub amount: Coin, -} - -impl MinimumDepositResponse { - pub fn new(amount: Coin) -> Self { - MinimumDepositResponse { amount } - } -} diff --git a/contracts/coconut-dkg/src/constants.rs b/contracts/coconut-dkg/src/constants.rs index bfa7d44aef..249a96c696 100644 --- a/contracts/coconut-dkg/src/constants.rs +++ b/contracts/coconut-dkg/src/constants.rs @@ -1,9 +1,5 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::Uint128; - -// to be determined whether those should be constants or exist as contract state -pub(crate) const MINIMUM_DEPOSIT: Uint128 = Uint128::new(1_000_000_000); // Wait time for the verification to take place (currently 24h) pub(crate) const BLOCK_TIME_FOR_VERIFICATION_SECS: u64 = 86400; diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs index 7ad730e5ab..74e7d61aee 100644 --- a/contracts/coconut-dkg/src/dealers/transactions.rs +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -1,12 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::MINIMUM_DEPOSIT; use crate::dealers::storage as dealers_storage; use crate::epoch_state::utils::check_epoch_state; use crate::{ContractError, State, STATE}; use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState}; -use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response}; +use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; // currently we only require that // a) it's part of the signer group @@ -27,40 +26,6 @@ fn verify_dealer(deps: DepsMut<'_>, state: &State, dealer: &Addr) -> Result<(), Ok(()) } -fn validate_dealer_deposit(state: &State, mut deposit: Vec) -> Result { - // check if anything was put as deposit - if deposit.is_empty() { - return Err(ContractError::NoDepositFound { - denom: state.mix_denom.clone(), - }); - } - - if deposit.len() > 1 { - return Err(ContractError::MultipleDenoms); - } - - // check that the denomination is correct - if deposit[0].denom != state.mix_denom { - return Err(ContractError::WrongDenom { - denom: state.mix_denom.clone(), - }); - } - - // check that we have at least MINIMUM_DEPOSIT coins in our deposit - if deposit[0].amount < MINIMUM_DEPOSIT { - return Err(ContractError::InsufficientDeposit { - received: deposit[0].amount.into(), - minimum: MINIMUM_DEPOSIT.into(), - }); - } - - // the unwrap would have been safe here under all circumstances, since we checked whether the vector is empty - // but in case something did change, change option into an error - deposit.pop().ok_or(ContractError::NoDepositFound { - denom: state.mix_denom.clone(), - }) -} - pub fn try_add_dealer( mut deps: DepsMut<'_>, info: MessageInfo, @@ -72,9 +37,6 @@ pub fn try_add_dealer( verify_dealer(deps.branch(), &state, &info.sender)?; - // validate and extract sent deposit - let deposit = validate_dealer_deposit(&state, info.funds)?; - // if it was already a dealer in the past, assign the same node index let node_index = if let Some(prior_details) = dealers_storage::past_dealers().may_load(deps.storage, &info.sender)? @@ -97,7 +59,6 @@ pub fn try_add_dealer( bte_public_key_with_proof: bte_key_with_proof, announce_address, assigned_index: node_index, - deposit, }; dealers_storage::current_dealers().save(deps.storage, &info.sender, &dealer_details)?; diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs index 4a7a7407d3..7255b96ae8 100644 --- a/contracts/coconut-dkg/src/lib.rs +++ b/contracts/coconut-dkg/src/lib.rs @@ -1,7 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::MINIMUM_DEPOSIT; use crate::dealers::queries::{ query_current_dealers_paged, query_dealer_details, query_past_dealers_paged, }; @@ -11,9 +10,9 @@ use crate::error::ContractError; use crate::state::{State, ADMIN, MULTISIG, STATE}; use crate::verification_key_shares::queries::query_vk_shares_paged; use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; -use coconut_dkg_common::types::{EpochState, MinimumDepositResponse}; +use coconut_dkg_common::types::EpochState; use cosmwasm_std::{ - entry_point, to_binary, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; use cw4::Cw4Contract; use epoch_state::storage::{advance_epoch_state, CURRENT_EPOCH_STATE}; @@ -106,10 +105,6 @@ pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { to_binary(&query_past_dealers_paged(deps, start_after, limit)?)? } - QueryMsg::GetDepositAmount {} => to_binary(&MinimumDepositResponse::new(Coin::new( - MINIMUM_DEPOSIT.u128(), - STATE.load(deps.storage)?.mix_denom, - )))?, QueryMsg::GetDealing { idx, limit, diff --git a/contracts/coconut-test/src/submit_vk_creates_proposal.rs b/contracts/coconut-test/src/submit_vk_creates_proposal.rs index a556bbe35f..dd8f59b2da 100644 --- a/contracts/coconut-test/src/submit_vk_creates_proposal.rs +++ b/contracts/coconut-test/src/submit_vk_creates_proposal.rs @@ -101,7 +101,7 @@ fn dkg_create_proposal() { bte_key_with_proof: "bte_key_with_proof".to_string(), announce_address: "127.0.0.1:8000".to_string(), }, - &coins(1000000000, TEST_COIN_DENOM), + &vec![], ) .unwrap(); From f3c1ff02e247f160c0441bedff4cb2ffd256438d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 12:03:58 +0100 Subject: [PATCH 40/75] Network-requester: throttle inbound connections (#1789) * Return and handle ClientRequest::LaneQueueLenghts * Pass lane queue lengths to inbound future * Remove unused self reference * Request lane queue lengths periodically for all open connections * Add timeouts * Rename to ConnectionCommandSender and Receiver * Rename to client_connection_tx/rx * Fix wasm build * Replace bool with enum --- .../src/client/real_messages_control/mod.rs | 6 +- .../real_traffic_stream.rs | 24 ++-- clients/native/src/client/mod.rs | 23 ++-- clients/native/src/websocket/handler.rs | 43 +++++-- .../native/websocket-requests/src/requests.rs | 27 +++++ clients/socks5/src/client/mod.rs | 16 +-- clients/socks5/src/socks/server.rs | 13 ++- clients/webassembly/src/client/mod.rs | 10 +- common/client-connections/src/lib.rs | 20 +++- .../src/connection_controller.rs | 49 ++++++-- .../proxy-helpers/src/proxy_runner/inbound.rs | 34 ++++-- .../network-requester/src/connection.rs | 4 +- .../network-requester/src/core.rs | 107 ++++++++++++++---- 13 files changed, 282 insertions(+), 94 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index d8b9dd34a6..ffeeea61e3 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -16,7 +16,7 @@ use crate::{ }, spawn_future, }; -use client_connections::{ClosedConnectionReceiver, LaneQueueLengths}; +use client_connections::{ConnectionCommandReceiver, LaneQueueLengths}; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; @@ -115,7 +115,7 @@ impl RealMessagesController { topology_access: TopologyAccessor, #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, lane_queue_lengths: LaneQueueLengths, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, ) -> Self { let rng = OsRng; @@ -166,7 +166,7 @@ impl RealMessagesController { config.self_recipient, topology_access, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ); RealMessagesController { diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index d6f34a5460..68b078e53c 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -5,7 +5,7 @@ use crate::client::mix_traffic::BatchMixMessageSender; use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender; use crate::client::topology_control::TopologyAccessor; use client_connections::{ - ClosedConnectionReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, + ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, }; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; @@ -134,7 +134,7 @@ where /// Incoming channel for being notified of closed connections, so that we can close lanes /// corresponding to connections. To avoid sending traffic unnecessary - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, /// Report queue lengths so that upstream can backoff sending data, and keep connections open. lane_queue_lengths: LaneQueueLengths, @@ -182,7 +182,7 @@ where our_full_destination: Recipient, topology_access: TopologyAccessor, lane_queue_lengths: LaneQueueLengths, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, ) -> Self { OutQueueControl { config, @@ -196,7 +196,7 @@ where rng, topology_access, transmission_buffer: Default::default(), - closed_connection_rx, + client_connection_rx, lane_queue_lengths, } } @@ -262,7 +262,7 @@ where self.sent_notify(fragment_id); } - // In addition to closing connections on receiving messages throught closed_connection_rx, + // In addition to closing connections on receiving messages throught client_connection_rx, // also close connections when sufficiently stale. self.transmission_buffer.prune_stale_connections(); @@ -335,8 +335,11 @@ where // Start by checking if we have any incoming messages about closed connections // NOTE: this feels a bit iffy, the `OutQueueControl` is getting ripe for a rewrite to // something simpler. - if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) { - self.on_close_connection(id); + if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) { + match id { + ConnectionCommand::Close(id) => self.on_close_connection(id), + ConnectionCommand::ActiveConnections(_) => panic!(), + } } if let Some(ref mut next_delay) = &mut self.next_delay { @@ -411,8 +414,11 @@ where fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll> { // Start by checking if we have any incoming messages about closed connections - if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) { - self.on_close_connection(id); + if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) { + match id { + ConnectionCommand::Close(id) => self.on_close_connection(id), + ConnectionCommand::ActiveConnections(_) => panic!(), + } } match Pin::new(&mut self.real_receiver).poll_recv(cx) { diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 357293d17e..5e16cf0a0f 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use client_connections::{ - ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths, TransmissionLane, + ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, }; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ @@ -123,7 +123,7 @@ impl NymClient { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, lane_queue_lengths: LaneQueueLengths, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, shutdown: ShutdownListener, ) { let mut controller_config = real_messages_control::Config::new( @@ -154,7 +154,7 @@ impl NymClient { topology_accessor, reply_key_storage, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ) .start_with_shutdown(shutdown); } @@ -299,15 +299,17 @@ impl NymClient { &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + shared_lane_queue_lengths: LaneQueueLengths, + client_connection_tx: ConnectionCommandSender, ) { info!("Starting websocket listener..."); let websocket_handler = websocket::Handler::new( msg_input, - closed_connection_tx, + client_connection_tx, buffer_requester, &self.as_mix_recipient(), + shared_lane_queue_lengths, ); websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler); @@ -449,11 +451,11 @@ impl NymClient { // Channels that the websocket listener can use to signal downstream to the real traffic // controller that connections are closed. - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); // Shared queue length data. Published by the `OutQueueController` in the client, and used // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) - let shared_lane_queue_length = LaneQueueLengths::new(); + let shared_lane_queue_lengths = LaneQueueLengths::new(); self.start_real_traffic_controller( shared_topology_accessor.clone(), @@ -461,8 +463,8 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - shared_lane_queue_length, - closed_connection_rx, + shared_lane_queue_lengths.clone(), + client_connection_rx, shutdown.subscribe(), ); @@ -482,7 +484,8 @@ impl NymClient { SocketType::WebSocket => self.start_websocket_listener( received_buffer_request_sender, input_sender, - closed_connection_tx, + shared_lane_queue_lengths, + client_connection_tx, ), SocketType::None => { // if we did not start the socket, it means we're running (supposedly) in the native mode diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 07b196354d..53e6a4d491 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::{ClosedConnectionSender, TransmissionLane}; +use client_connections::{ + ConnectionCommand, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, +}; use client_core::client::{ inbound_messages::{InputMessage, InputMessageSender}, received_buffer::{ @@ -35,11 +37,12 @@ impl Default for ReceivedResponseType { pub(crate) struct Handler { msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: Recipient, socket: Option>, received_response_type: ReceivedResponseType, + lane_queue_lengths: LaneQueueLengths, } // clone is used to use handler on a new connection, which initially is `None` @@ -47,11 +50,12 @@ impl Clone for Handler { fn clone(&self) -> Self { Handler { msg_input: self.msg_input.clone(), - closed_connection_tx: self.closed_connection_tx.clone(), + client_connection_tx: self.client_connection_tx.clone(), buffer_requester: self.buffer_requester.clone(), self_full_address: self.self_full_address, socket: None, received_response_type: Default::default(), + lane_queue_lengths: self.lane_queue_lengths.clone(), } } } @@ -67,17 +71,19 @@ impl Drop for Handler { impl Handler { pub(crate) fn new( msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: &Recipient, + lane_queue_lengths: LaneQueueLengths, ) -> Self { Handler { msg_input, - closed_connection_tx, + client_connection_tx, buffer_requester, self_full_address: *self_full_address, socket: None, received_response_type: Default::default(), + lane_queue_lengths, } } @@ -95,6 +101,15 @@ impl Handler { panic!(); } + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength(connection_id, queue_length)); + } + + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); None } @@ -120,12 +135,25 @@ impl Handler { } fn handle_closed_connection(&self, connection_id: u64) -> Option { - self.closed_connection_tx - .unbounded_send(connection_id) + self.client_connection_tx + .unbounded_send(ConnectionCommand::Close(connection_id)) .unwrap(); None } + fn handle_get_lane_queue_length(&self, connection_id: u64) -> Option { + let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { + log::warn!( + "Failed to get the lane queue length lock, not responding back with the current queue length" + ); + return None; + }; + + let lane = TransmissionLane::ConnectionId(connection_id); + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) + } + async fn handle_request(&mut self, request: ClientRequest) -> Option { match request { ClientRequest::Send { @@ -143,6 +171,7 @@ impl Handler { } => self.handle_reply(reply_surb, message).await, ClientRequest::SelfAddress => Some(self.handle_self_address()), ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id), + ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id), } } diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index f681b4264c..9b5af6e70d 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -23,6 +23,9 @@ pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02; /// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03; +/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] +pub const GET_LANE_QUEUE_LENGHT_TAG: u8 = 0x04; + #[allow(non_snake_case)] #[derive(Debug)] pub enum ClientRequest { @@ -39,6 +42,7 @@ pub enum ClientRequest { }, SelfAddress, ClosedConnection(u64), + GetLaneQueueLength(u64), } // we could have been parsing it directly TryFrom, but we want to retain @@ -241,6 +245,26 @@ impl ClientRequest { ClientRequest::ClosedConnection(connection_id) } + // GET_LANE_QUEUE_LENGHT_TAG + fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec { + let conn_id_bytes = connection_id.to_be_bytes(); + std::iter::once(GET_LANE_QUEUE_LENGHT_TAG) + .chain(conn_id_bytes.iter().copied()) + .collect() + } + + // GET_LANE_QUEUE_LENGHT_TAG + fn deserialize_get_lane_queue_length(b: &[u8]) -> Self { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); + + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + + ClientRequest::GetLaneQueueLength(connection_id) + } + pub fn serialize(self) -> Vec { match self { ClientRequest::Send { @@ -258,6 +282,8 @@ impl ClientRequest { ClientRequest::SelfAddress => Self::serialize_self_address(), ClientRequest::ClosedConnection(id) => Self::serialize_closed_connection(id), + + ClientRequest::GetLaneQueueLength(id) => Self::serialize_get_lane_queue_lengths(id), } } @@ -288,6 +314,7 @@ impl ClientRequest { REPLY_REQUEST_TAG => Self::deserialize_reply(b), SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)), + GET_LANE_QUEUE_LENGHT_TAG => Ok(Self::deserialize_get_lane_queue_length(b)), n => Err(error::Error::new( ErrorKind::UnknownRequest, format!("type {n}"), diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index ef07360e2b..c6b734a749 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -9,7 +9,7 @@ use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths}; +use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -120,7 +120,7 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { @@ -152,7 +152,7 @@ impl NymClient { topology_accessor, reply_key_storage, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ) .start_with_shutdown(shutdown); } @@ -297,7 +297,7 @@ impl NymClient { &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { @@ -316,7 +316,7 @@ impl NymClient { ); tokio::spawn(async move { sphinx_socks - .serve(msg_input, buffer_requester, closed_connection_tx) + .serve(msg_input, buffer_requester, client_connection_tx) .await }); } @@ -426,7 +426,7 @@ impl NymClient { // Channel for announcing closed (socks5) connections by the controller. // This will be forwarded to `OutQueueControl` - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); // Shared queue length data. Published by the `OutQueueController` in the client, and used // primarily to throttle incoming connections @@ -438,7 +438,7 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + client_connection_rx, shared_lane_queue_lengths.clone(), shutdown.subscribe(), ); @@ -458,7 +458,7 @@ impl NymClient { self.start_socks5_listener( received_buffer_request_sender, input_sender, - closed_connection_tx, + client_connection_tx, shared_lane_queue_lengths, shutdown.subscribe(), ); diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index af5a6f9fed..f61d6052a0 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -4,13 +4,13 @@ use super::{ mixnet_responses::MixnetResponseListener, types::{ResponseCode, SocksProxyError}, }; -use client_connections::{ClosedConnectionSender, LaneQueueLengths}; +use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, }; use log::*; use nymsphinx::addressing::clients::Recipient; -use proxy_helpers::connection_controller::Controller; +use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller}; use std::net::SocketAddr; use task::ShutdownListener; use tokio::net::TcpListener; @@ -55,14 +55,17 @@ impl SphinxSocksServer { &mut self, input_sender: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, ) -> Result<(), SocksProxyError> { let listener = TcpListener::bind(self.listening_address).await.unwrap(); info!("Serving Connections..."); // controller for managing all active connections - let (mut active_streams_controller, controller_sender) = - Controller::new(closed_connection_tx, self.shutdown.clone()); + let (mut active_streams_controller, controller_sender) = Controller::new( + client_connection_tx, + BroadcastActiveConnections::Off, + self.shutdown.clone(), + ); tokio::spawn(async move { active_streams_controller.run().await; }); diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 654989644e..608674a169 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; -use client_connections::{ClosedConnectionReceiver, LaneQueueLengths, TransmissionLane}; +use client_connections::{ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane}; use client_core::client::{ cover_traffic_stream::LoopCoverTrafficStream, inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, @@ -128,7 +128,7 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, lane_queue_lengths: LaneQueueLengths, ) { let mut controller_config = real_messages_control::Config::new( @@ -155,7 +155,7 @@ impl NymClient { mix_sender, topology_accessor, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ) .start(); } @@ -334,7 +334,7 @@ impl NymClient { // Channel that the real traffix controller can listed to for closing connections. // Currently unused in the wasm client. - let (_closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (_client_connection_tx, client_connection_rx) = mpsc::unbounded(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. @@ -364,7 +364,7 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + client_connection_rx, shared_lane_queue_lengths, ); diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index d1782e8d90..515938ed54 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -16,11 +16,21 @@ pub enum TransmissionLane { ConnectionId(ConnectionId), } -/// Announce connections that are closed, for whoever is interested. -/// One usecase is that the network-requester and socks5-client wants to know about this, so that -/// they can forward this to the `OutQueueControl` (via `ClientRequest` for the network-requester) -pub type ClosedConnectionSender = mpsc::UnboundedSender; -pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver; +/// Used by the connection controller to report current state for client connections. +pub type ConnectionCommandSender = mpsc::UnboundedSender; +pub type ConnectionCommandReceiver = mpsc::UnboundedReceiver; + +pub enum ConnectionCommand { + // Announce that at a connection was closed. E.g the `OutQueueControl` uses this to discard + // transmission lanes. + Close(ConnectionId), + + // In the network requester for example, we usually want to broadcast active connections + // regularly, so we know what connections we need to request lane queue lengths for from the + // client. + // In the socks5-client, this is not needed since have direct access to the lane queue lengths. + ActiveConnections(Vec), +} // The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down // if needed. diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index d78915d2a5..12fdba7dbb 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -1,14 +1,18 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::ClosedConnectionSender; +use client_connections::{ConnectionCommand, ConnectionCommandSender}; use futures::channel::mpsc; use futures::StreamExt; use log::*; use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData}; use socks5_requests::ConnectionId; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::Duration, +}; use task::ShutdownListener; +use tokio::time; /// A generic message produced after reading from a socket/connection. It includes data that was /// actually read alongside boolean indicating whether the connection got closed so that @@ -64,6 +68,12 @@ impl ActiveConnection { } } +#[derive(PartialEq, Eq)] +pub enum BroadcastActiveConnections { + On, + Off, +} + /// Controller represents a way of managing multiple open connections that are used for socks5 /// proxy. pub struct Controller { @@ -75,7 +85,11 @@ pub struct Controller { recently_closed: HashSet, // Broadcast closed connections - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, + + // The controller can broadcast active connections. This is useful in the network-requester + // where its used to query the client for lane queue lengths + broadcast_connections: BroadcastActiveConnections, // TODO: this can potentially be abused to ddos and kill provider. Not sure at this point // how to handle it more gracefully @@ -89,7 +103,8 @@ pub struct Controller { impl Controller { pub fn new( - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, + broadcast_connections: BroadcastActiveConnections, shutdown: ShutdownListener, ) -> (Self, ControllerSender) { let (sender, receiver) = mpsc::unbounded(); @@ -98,7 +113,8 @@ impl Controller { active_connections: HashMap::new(), receiver, recently_closed: HashSet::new(), - closed_connection_tx, + client_connection_tx, + broadcast_connections, pending_messages: HashMap::new(), shutdown, }, @@ -137,7 +153,10 @@ impl Controller { self.recently_closed.insert(conn_id); // Announce closed connections, currently used by the `OutQueueControl`. - if let Err(err) = self.closed_connection_tx.unbounded_send(conn_id) { + if let Err(err) = self + .client_connection_tx + .unbounded_send(ConnectionCommand::Close(conn_id)) + { if self.shutdown.is_shutdown_poll() { log::debug!("Failed to send: {}", err); } else { @@ -146,6 +165,15 @@ impl Controller { } } + fn broadcast_active_connections(&mut self) { + // What about the recently closed ones? Hopefully we can ignore them ... + let conn_ids = self.active_connections.keys().copied().collect(); + + self.client_connection_tx + .unbounded_send(ConnectionCommand::ActiveConnections(conn_ids)) + .unwrap(); + } + fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec, is_closed: bool) { if let Some(active_connection) = self.active_connections.get_mut(&conn_id) { if !payload.is_empty() { @@ -202,6 +230,8 @@ impl Controller { } pub async fn run(&mut self) { + let mut interval = time::interval(Duration::from_millis(500)); + loop { tokio::select! { command = self.receiver.next() => match command { @@ -216,7 +246,12 @@ impl Controller { log::trace!("SOCKS5 Controller: Stopping since channel closed"); break; } - } + }, + _ = interval.tick() => { + if self.broadcast_connections == BroadcastActiveConnections::On { + self.broadcast_active_connections(); + } + }, } } assert!(self.shutdown.is_shutdown_poll()); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index ef0a09336d..bb5cf98f37 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -106,23 +106,37 @@ where } async fn wait_until_lane_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { - wait_for_lane( - lane_queue_lengths, - connection_id, - 0, - Duration::from_millis(500), + if tokio::time::timeout( + Duration::from_secs(2 * 60), + wait_for_lane( + lane_queue_lengths, + connection_id, + 0, + Duration::from_millis(500), + ), ) .await + .is_err() + { + log::warn!("Wait until lane empty timed out"); + } } async fn wait_until_lane_almost_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { - wait_for_lane( - lane_queue_lengths, - connection_id, - 10, - Duration::from_millis(100), + if tokio::time::timeout( + Duration::from_secs(2 * 60), + wait_for_lane( + lane_queue_lengths, + connection_id, + 10, + Duration::from_millis(100), + ), ) .await + .is_err() + { + log::debug!("Wait until lane almost empty timed out"); + } } async fn wait_for_lane( diff --git a/service-providers/network-requester/src/connection.rs b/service-providers/network-requester/src/connection.rs index 6627fdbf4b..95bdac06a3 100644 --- a/service-providers/network-requester/src/connection.rs +++ b/service-providers/network-requester/src/connection.rs @@ -1,6 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use client_connections::LaneQueueLengths; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::ConnectionReceiver; use proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner}; @@ -40,6 +41,7 @@ impl Connection { &mut self, mix_receiver: ConnectionReceiver, mix_sender: MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let stream = self.conn.take().unwrap(); @@ -53,7 +55,7 @@ impl Connection { mix_receiver, mix_sender, connection_id, - None, + Some(lane_queue_lengths), shutdown, ) .run(move |conn_id, read_data, socket_closed| { diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 88d66d159d..6bea582581 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -7,14 +7,18 @@ use crate::error::NetworkRequesterError; use crate::statistics::ServiceStatisticsCollector; use crate::websocket; use crate::websocket::TSWebsocketStream; -use client_connections::ClosedConnectionReceiver; +use client_connections::{ + ConnectionCommand, ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane, +}; use futures::channel::mpsc; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::receiver::ReconstructedMessage; -use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender}; +use proxy_helpers::connection_controller::{ + BroadcastActiveConnections, Controller, ControllerCommand, ControllerSender, +}; use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender}; use socks5_requests::{ ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response, @@ -70,7 +74,7 @@ impl ServiceProvider { mut websocket_writer: SplitSink, mut mix_reader: MixProxyReader<(Socks5Message, Recipient)>, stats_collector: Option, - mut closed_connection_rx: ClosedConnectionReceiver, + mut client_connection_rx: ConnectionCommandReceiver, ) { loop { tokio::select! { @@ -108,17 +112,49 @@ impl ServiceProvider { break; } }, - Some(id) = closed_connection_rx.next() => { - let msg = ClientRequest::ClosedConnection(id); - let ws_msg = Message::Binary(msg.serialize()); - websocket_writer.send(ws_msg).await.unwrap(); - } + Some(command) = client_connection_rx.next() => { + match command { + ConnectionCommand::Close(id) => { + let msg = ClientRequest::ClosedConnection(id); + let ws_msg = Message::Binary(msg.serialize()); + websocket_writer.send(ws_msg).await.unwrap(); + } + ConnectionCommand::ActiveConnections(ids) => { + // We can optimize this by sending a single request, but this is + // usually in the low single digits, max a few tens, so we leave that + // for a rainy day. + // Also that means fiddling with the currently manual + // serialize/deserialize we do with ClientRequests ... bleh + for id in ids { + log::trace!("Requesting lane queue length for: {}", id); + let msg = ClientRequest::GetLaneQueueLength(id); + let ws_msg = Message::Binary(msg.serialize()); + websocket_writer.send(ws_msg).await.unwrap(); + } + } + } + }, } } } + fn handle_lane_queue_length_response( + lane_queue_lengths: &LaneQueueLengths, + lane: u64, + queue_length: usize, + ) { + log::trace!("Received LaneQueueLength lane: {lane}, queue_length: {queue_length}"); + if let Ok(mut lane_queue_lengths) = lane_queue_lengths.lock() { + let lane = TransmissionLane::ConnectionId(lane); + lane_queue_lengths.map.insert(lane, queue_length); + } else { + log::warn!("Unable to lock lane queue lengths, skipping updating received lane length") + } + } + async fn read_websocket_message( websocket_reader: &mut SplitStream, + lane_queue_lengths: LaneQueueLengths, ) -> Option { while let Some(msg) = websocket_reader.next().await { let data = msg @@ -139,6 +175,14 @@ impl ServiceProvider { let received = match deserialized_message { ServerResponse::Received(received) => received, + ServerResponse::LaneQueueLength(lane, queue_length) => { + Self::handle_lane_queue_length_response( + &lane_queue_lengths, + lane, + queue_length, + ); + continue; + } ServerResponse::Error(err) => { panic!("received error from native client! - {}", err) } @@ -155,6 +199,7 @@ impl ServiceProvider { return_address: Recipient, controller_sender: ControllerSender, mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let mut conn = match Connection::new(conn_id, remote_addr.clone(), return_address).await { @@ -196,7 +241,7 @@ impl ServiceProvider { ); // run the proxy on the connection - conn.run_proxy(mix_receiver, mix_input_sender, shutdown) + conn.run_proxy(mix_receiver, mix_input_sender, lane_queue_lengths, shutdown) .await; // proxy is done - remove the access channel from the controller @@ -212,10 +257,12 @@ impl ServiceProvider { ); } + #[allow(clippy::too_many_arguments)] async fn handle_proxy_connect( &mut self, controller_sender: &mut ControllerSender, mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, conn_id: ConnectionId, remote_addr: String, return_address: Recipient, @@ -250,6 +297,7 @@ impl ServiceProvider { return_address, controller_sender_clone, mix_input_sender_clone, + lane_queue_lengths, shutdown, ) .await @@ -257,7 +305,6 @@ impl ServiceProvider { } fn handle_proxy_send( - &self, controller_sender: &mut ControllerSender, conn_id: ConnectionId, data: Vec, @@ -273,6 +320,7 @@ impl ServiceProvider { raw_request: &[u8], controller_sender: &mut ControllerSender, mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, stats_collector: Option, shutdown: ShutdownListener, ) { @@ -296,6 +344,7 @@ impl ServiceProvider { self.handle_proxy_connect( controller_sender, mix_input_sender, + lane_queue_lengths, req.conn_id, req.remote_addr, req.return_address, @@ -319,7 +368,7 @@ impl ServiceProvider { .processed(remote_addr, data.len() as u32); } } - self.handle_proxy_send(controller_sender, conn_id, data, closed) + Self::handle_proxy_send(controller_sender, conn_id, data, closed) } }, Socks5Message::Response(_) | Socks5Message::NetworkRequesterResponse(_) => {} @@ -341,16 +390,23 @@ impl ServiceProvider { // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). let shutdown = task::ShutdownNotifier::default(); - // Channel for announcing closed (socks5) connections by the controller. - // The `mixnet_response_listener` will forward this info to the client using a - // `ClientRequest`. - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + // Channel for announcing client connection state by the controller. + // The `mixnet_response_listener` will use this to either report closed connection to the + // client or request lane queue lengths. + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); // Controller for managing all active connections. // We provide it with a ShutdownListener since it requires it, even though for the network // requester shutdown signalling is not yet fully implemented. - let (mut active_connections_controller, mut controller_sender) = - Controller::new(closed_connection_tx, shutdown.subscribe()); + let (mut active_connections_controller, mut controller_sender) = Controller::new( + client_connection_tx, + BroadcastActiveConnections::On, + shutdown.subscribe(), + ); tokio::spawn(async move { active_connections_controller.run().await; @@ -378,7 +434,7 @@ impl ServiceProvider { websocket_writer, mix_input_receiver, stats_collector_clone, - closed_connection_rx, + client_connection_rx, ) .await; }); @@ -386,12 +442,14 @@ impl ServiceProvider { println!("\nAll systems go. Press CTRL-C to stop the server."); // for each incoming message from the websocket... (which in 99.99% cases is going to be a mix message) loop { - let received = match Self::read_websocket_message(&mut websocket_reader).await { - Some(msg) => msg, - None => { - error!("The websocket stream has finished!"); - return Ok(()); - } + let Some(received) = Self::read_websocket_message( + &mut websocket_reader, + shared_lane_queue_lengths.clone() + ) + .await + else { + log::error!("The websocket stream has finished!"); + return Ok(()); }; let raw_message = received.message; @@ -401,6 +459,7 @@ impl ServiceProvider { &raw_message, &mut controller_sender, &mix_input_sender, + shared_lane_queue_lengths.clone(), stats_collector.clone(), shutdown.subscribe(), ) From 63ae568cc2e99e57b4e24d5228c1532c6896dabd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 15:51:51 +0100 Subject: [PATCH 41/75] rust: bump required version to 1.65 in some crates that need it --- clients/native/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 4f4cca6a85..ab22e76a9d 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -4,7 +4,7 @@ version = "1.1.0" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" -rust-version = "1.56" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index eb3cc1d229..8d81920af5 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -6,6 +6,7 @@ name = "nym-network-requester" version = "1.1.0" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2021" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 81f7457e0e59bbe38b20a676edc0c661aa7a9c07 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Wed, 23 Nov 2022 16:13:18 +0100 Subject: [PATCH 42/75] Add step to release GH actions (#1792) * feat: add a release step to nym contracts GH action * feat: add shrinking the size of wasm --- .github/workflows/contracts-build.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index 84428b01cd..a25806c1b7 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -1,10 +1,8 @@ name: Build release of Nym smart contracts on: workflow_dispatch: - -defaults: - run: - working-directory: contracts + release: + types: [created] jobs: build: @@ -21,7 +19,7 @@ jobs: components: rustfmt, clippy - name: Build release contracts - run: RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + run: make wasm - name: Upload Mixnet Contract Artifact uses: actions/upload-artifact@v3 @@ -36,3 +34,11 @@ jobs: name: vesting_contract.wasm path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm retention-days: 5 + + - name: Upload to release based on tag name + uses: softprops/action-gh-release@v1 + if: github.event_name == 'release' + with: + files: | + contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm + contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm From c8b454a085c4f04ff76f410cdceadc1323441588 Mon Sep 17 00:00:00 2001 From: cgi-bin/ <6095048+sven-hash@users.noreply.github.com> Date: Wed, 23 Nov 2022 16:14:43 +0100 Subject: [PATCH 43/75] Possibilty to change gateway ws listener (#1779) * add: set gatewayListener * Update types.ts * Update worker.ts --- sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts | 5 +++++ sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts b/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts index 8e984f66c2..36aacd5adb 100644 --- a/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts +++ b/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts @@ -27,6 +27,11 @@ export interface NymClientConfig { * Optional. The identity key of the preferred gateway to connect to. */ preferredGatewayIdentityKey?: string; + + /** + * Optional. The listener websocket of the preferred gateway to connect to. + */ + gatewayListener?: string; /** * Optional. Settings for the WASM client. diff --git a/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts b/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts index 61df19b408..cb1c8a32e4 100644 --- a/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts +++ b/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts @@ -133,7 +133,11 @@ wasm_bindgen(wasmUrl) config.validatorApiUrl, config.preferredGatewayIdentityKey, ); - + + // set a different gatewayListener in order to avoid workaround ws over https error + if (config.gatewayListener) + gatewayEndpoint.gateway_listener = config.gatewayListener; + // create the client, passing handlers for events wrapper.init( new wasm_bindgen.Config( From 9637afea852dd2c16094058615b0c890e4789dd4 Mon Sep 17 00:00:00 2001 From: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> Date: Wed, 23 Nov 2022 15:51:21 +0000 Subject: [PATCH 44/75] Update contracts-build.yml --- .github/workflows/contracts-build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index a25806c1b7..3921bd3a6d 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -9,6 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + + - name: Check the release tag starts with `nym-contracts-` + if: startsWith(github.ref, 'refs/tags/nym-contracts-') == false && github.event_name != 'workflow_dispatch' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Release tag did not start with nym-contracts-...') - name: Install Rust stable uses: actions-rs/toolchain@v1 From 0a41834fbe160780aa1b85ab412cd37ccd45a926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 16:56:09 +0100 Subject: [PATCH 45/75] real_traffic_stream: reduce frequency of status print (#1794) --- .../real_traffic_stream.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 68b078e53c..87368536a5 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -464,17 +464,23 @@ where let lanes = self.transmission_buffer.num_lanes(); let mult = self.sending_delay_controller.current_multiplier(); let delay = self.current_average_message_sending_delay().as_millis(); - if self.config.disable_poisson_packet_distribution { - log::info!( + let status_str = if self.config.disable_poisson_packet_distribution { + format!( "Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), no delay", backlog - ); + ) } else { - log::info!( + format!( "Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), avg delay: {}ms ({mult})", - backlog, - delay - ); + backlog, delay + ) + }; + if packets > 1000 { + log::warn!("{status_str}"); + } else if packets > 0 { + log::info!("{status_str}"); + } else { + log::debug!("{status_str}"); } } From 258fa412711ba606e13b0e6e5b6e3afafc58f0e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 20:59:12 +0100 Subject: [PATCH 46/75] Update wallet and connect lock files (#1793) --- nym-connect/Cargo.lock | 136 ++++++++++++++++++++++++++++--- nym-wallet/Cargo.lock | 176 +++++++++++++++++++++++++++++++---------- 2 files changed, 257 insertions(+), 55 deletions(-) diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 918cc02baf..cab899eacf 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -268,6 +268,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.8.1" @@ -334,11 +346,25 @@ dependencies = [ "digest 0.9.0", "ff 0.10.1", "group 0.10.0", - "pairing", + "pairing 0.20.0", "rand_core 0.6.4", "subtle 2.4.1", ] +[[package]] +name = "bls12_381" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" +dependencies = [ + "digest 0.9.0", + "ff 0.11.1", + "group 0.11.0", + "pairing 0.21.0", + "rand_core 0.6.4", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "brotli" version = "3.3.4" @@ -599,6 +625,7 @@ name = "client-connections" version = "0.1.0" dependencies = [ "futures", + "log", ] [[package]] @@ -689,6 +716,8 @@ version = "0.1.0" dependencies = [ "contracts-common", "cosmwasm-std", + "cw-utils", + "multisig-contract-common", "schemars", "serde", ] @@ -942,11 +971,10 @@ dependencies = [ name = "credentials" version = "0.1.0" dependencies = [ - "bls12_381", + "bls12_381 0.5.0", "coconut-interface", "crypto", "thiserror", - "url", "validator-api-requests", "validator-client", ] @@ -1136,9 +1164,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -1170,6 +1198,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw2" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + [[package]] name = "cw3" version = "0.13.4" @@ -1182,6 +1222,22 @@ dependencies = [ "serde", ] +[[package]] +name = "cw3-fixed-multisig" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw4" version = "0.13.4" @@ -1337,6 +1393,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec", + "bls12_381 0.6.0", + "bs58", + "ff 0.11.1", + "group 0.11.0", + "lazy_static", + "pemstore", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1660,6 +1737,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2116,6 +2199,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ + "byteorder", "ff 0.11.1", "rand_core 0.6.4", "subtle 2.4.1", @@ -3040,6 +3124,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -3292,6 +3377,7 @@ dependencies = [ "serde", "snafu", "socks5-requests", + "tap", "task", "thiserror", "tokio", @@ -3306,13 +3392,15 @@ dependencies = [ name = "nymcoconut" version = "0.5.0" dependencies = [ - "bls12_381", + "bls12_381 0.6.0", "bs58", "digest 0.9.0", - "ff 0.10.1", + "dkg", + "ff 0.11.1", "getrandom 0.2.7", - "group 0.10.0", + "group 0.11.0", "itertools", + "pemstore", "rand 0.8.5", "serde", "serde_derive", @@ -3570,6 +3658,15 @@ dependencies = [ "group 0.10.0", ] +[[package]] +name = "pairing" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" +dependencies = [ + "group 0.11.0", +] + [[package]] name = "pango" version = "0.15.10" @@ -4085,6 +4182,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -6772,6 +6875,15 @@ dependencies = [ "windows-implement", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.20.0" @@ -6795,9 +6907,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -6821,9 +6933,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 895eab810c..999378447f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -256,6 +256,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.5" @@ -319,16 +331,16 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bls12_381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" dependencies = [ "digest 0.9.0", - "ff 0.10.1", - "group 0.10.0", + "ff", + "group", "pairing", "rand_core 0.6.3", "subtle", + "zeroize", ] [[package]] @@ -601,6 +613,8 @@ version = "0.1.0" dependencies = [ "contracts-common", "cosmwasm-std", + "cw-utils", + "multisig-contract-common", "schemars", "serde", ] @@ -965,9 +979,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", @@ -976,10 +990,22 @@ dependencies = [ ] [[package]] -name = "cw3" -version = "0.13.2" +name = "cw2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f871854338a54c7bb094d16ffe17212b93b146d9659dbce4c9402a9b77e240ef" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" dependencies = [ "cosmwasm-std", "cw-utils", @@ -988,10 +1014,26 @@ dependencies = [ ] [[package]] -name = "cw4" -version = "0.13.2" +name = "cw3-fixed-multisig" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4476d6a7c13c46ed9ff260bd0e1cf648dc37b13f483822e1ff2a431f0f6ee52" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1142,6 +1184,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec", + "bls12_381", + "bs58", + "ff", + "group", + "lazy_static", + "pemstore", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1232,9 +1295,9 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "ff 0.11.0", + "ff", "generic-array 0.14.5", - "group 0.11.0", + "group", "rand_core 0.6.3", "sec1", "subtle", @@ -1345,16 +1408,6 @@ dependencies = [ "log", ] -[[package]] -name = "ff" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" -dependencies = [ - "rand_core 0.6.3", - "subtle", -] - [[package]] name = "ff" version = "0.11.0" @@ -1452,6 +1505,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1843,25 +1902,14 @@ dependencies = [ "system-deps 6.0.2", ] -[[package]] -name = "group" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" -dependencies = [ - "byteorder", - "ff 0.10.1", - "rand_core 0.6.3", - "subtle", -] - [[package]] name = "group" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ - "ff 0.11.0", + "byteorder", + "ff", "rand_core 0.6.3", "subtle", ] @@ -2657,6 +2705,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -2944,10 +2993,12 @@ dependencies = [ "bls12_381", "bs58", "digest 0.9.0", - "ff 0.10.1", + "dkg", + "ff", "getrandom 0.2.5", - "group 0.10.0", + "group", "itertools", + "pemstore", "rand 0.8.5", "serde", "serde_derive", @@ -3063,11 +3114,11 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" [[package]] name = "pairing" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" dependencies = [ - "group 0.10.0", + "group", ] [[package]] @@ -3219,6 +3270,24 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64", + "once_cell", + "regex", +] + +[[package]] +name = "pemstore" +version = "0.1.0" +dependencies = [ + "pem", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3590,6 +3659,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -4648,6 +4723,12 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.38" @@ -5899,6 +5980,15 @@ dependencies = [ "windows-implement", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.19.1" From c5c16cd6b0b51095c187f68051abdf3e55886d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 21:00:48 +0100 Subject: [PATCH 47/75] client-core: add warning when delay multiplier is larger than 1 --- .../src/client/real_messages_control/real_traffic_stream.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 87368536a5..bc5535d6b1 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -482,6 +482,12 @@ where } else { log::debug!("{status_str}"); } + if self.sending_delay_controller.current_multiplier() > 1 { + log::warn!( + "Unable to send packets fast enough - sending delay multiplier set to: {}", + self.sending_delay_controller.current_multiplier() + ); + } } #[cfg(not(target_arch = "wasm32"))] From 83041461951914edc4030742f13f642e5a75ba67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 10:26:09 +0100 Subject: [PATCH 48/75] Fix decrypting stored received msg (#1786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix decrypting stored received msg * rustfmt * Moving binary message recovery to separate function Co-authored-by: Jędrzej Stuczyński --- .../client-libs/gateway-client/src/client.rs | 29 ++++++++++++------- common/client-libs/gateway-client/src/lib.rs | 21 ++++++++++++++ .../gateway-client/src/socket_state.rs | 21 +++----------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 9e17f55ff8..bac92ec092 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -2,19 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bandwidth::BandwidthController; -use crate::cleanup_socket_message; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; use crate::socket_state::{PartiallyDelegated, SocketState}; -#[cfg(target_arch = "wasm32")] -use crate::wasm_storage::PersistentStorage; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(not(target_arch = "wasm32"))] -use credential_storage::PersistentStorage; +use crate::{cleanup_socket_message, try_decrypt_binary_message}; use crypto::asymmetric::identity; use futures::{FutureExt, SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; @@ -28,13 +22,20 @@ use rand::rngs::OsRng; use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; -#[cfg(not(target_arch = "wasm32"))] -use task::ShutdownListener; use tungstenite::protocol::Message; +#[cfg(feature = "coconut")] +use coconut_interface::Credential; + +#[cfg(not(target_arch = "wasm32"))] +use credential_storage::PersistentStorage; +#[cfg(not(target_arch = "wasm32"))] +use task::ShutdownListener; #[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::connect_async; +#[cfg(target_arch = "wasm32")] +use crate::wasm_storage::PersistentStorage; #[cfg(target_arch = "wasm32")] use wasm_timer; #[cfg(target_arch = "wasm32")] @@ -336,7 +337,15 @@ impl GatewayClient { }; match ws_msg { Message::Binary(bin_msg) => { - if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { + // if we have established the shared key already, attempt to use it for decryption + // otherwise there's not much we can do apart from just routing what we have on hand + if let Some(shared_keys) = &self.shared_key { + if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_keys) { + if let Err(err) = self.packet_router.route_received(vec![plaintext]) { + log::warn!("Route received failed: {:?}", err); + } + } + } else if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { log::warn!("Route received failed: {:?}", err); } } diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 1becf01b4a..8671a7363b 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -3,6 +3,9 @@ use crate::error::GatewayClientError; pub use client::GatewayClient; +use gateway_requests::registration::handshake::SharedKeys; +use gateway_requests::BinaryResponse; +use log::warn; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; @@ -37,3 +40,21 @@ pub(crate) fn cleanup_socket_messages( None => Err(GatewayClientError::ConnectionAbruptlyClosed), } } + +pub(crate) fn try_decrypt_binary_message( + bin_msg: Vec, + shared_keys: &SharedKeys, +) -> Option> { + match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) { + Ok(bin_response) => match bin_response { + BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext), + }, + Err(err) => { + warn!( + "message received from the gateway was malformed! - {:?}", + err + ); + None + } + } +} diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index df1b95c0ae..2d66e782e8 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -1,14 +1,13 @@ // Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cleanup_socket_messages; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; +use crate::{cleanup_socket_messages, try_decrypt_binary_message}; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use gateway_requests::registration::handshake::SharedKeys; -use gateway_requests::BinaryResponse; use log::*; use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] @@ -50,21 +49,9 @@ impl PartiallyDelegated { match ws_msg { Message::Binary(bin_msg) => { // this function decrypts the request and checks the MAC - let plaintext = match BinaryResponse::try_from_encrypted_tagged_bytes( - bin_msg, shared_key, - ) { - Ok(bin_response) => match bin_response { - BinaryResponse::PushedMixMessage(plaintext) => plaintext, - }, - Err(err) => { - warn!( - "message received from the gateway was malformed! - {:?}", - err - ); - continue; - } - }; - plaintexts.push(plaintext) + if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_key) { + plaintexts.push(plaintext) + } } // I think that in the future we should perhaps have some sequence number system, i.e. // so each request/response pair can be easily identified, so that if messages are From cbf9db91ab3644f9e1a99d568ba619520d757938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 24 Nov 2022 17:02:31 +0000 Subject: [PATCH 49/75] Feature/use expect instead of panicking (#1797) * Implementation of 'Debug' on 'RealMessage' * expect with failed channel name instead of throwing empty panics * Introduced Debug trait constraint in ProxyRunner * Derive Debug for socks5_requests::Message --- .../input_message_listener.rs | 8 ++------ .../retransmission_request_listener.rs | 8 ++------ .../real_traffic_stream.rs | 1 + clients/native/src/client/mod.rs | 16 ++++------------ clients/native/src/websocket/handler.rs | 14 ++++++++------ clients/socks5/src/socks/client.rs | 7 ++++--- clients/webassembly/src/client/mod.rs | 8 ++------ common/nymsphinx/forwarding/src/packet.rs | 15 ++++++++++++++- .../proxy-helpers/src/proxy_runner/inbound.rs | 18 ++++++++---------- .../proxy-helpers/src/proxy_runner/mod.rs | 3 ++- common/socks5/requests/src/msg.rs | 1 + .../network-requester/src/core.rs | 14 ++++---------- .../src/statistics/collector.rs | 16 ++++------------ 13 files changed, 56 insertions(+), 73 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 58fa65af56..0738d0aa80 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -187,14 +187,10 @@ where // there's no point in trying to send nothing if let Some(real_messages) = real_messages { // tells real message sender (with the poisson timer) to send this to the mix network - if self - .real_message_sender + self.real_message_sender .send((real_messages, lane)) .await - .is_err() - { - panic!(); - } + .expect("BatchRealMessageReceiver has stopped receiving!"); } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 2fbab44b61..4b0bc3cc99 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -116,17 +116,13 @@ where .unwrap(); // send to `OutQueueControl` to eventually send to the mix network - if self - .real_message_sender + self.real_message_sender .send(( vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], TransmissionLane::Retransmission, )) .await - .is_err() - { - panic!(); - } + .expect("BatchRealMessageReceiver has stopped receiving!"); } #[cfg(not(target_arch = "wasm32"))] diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index bc5535d6b1..503203922c 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -140,6 +140,7 @@ where lane_queue_lengths: LaneQueueLengths, } +#[derive(Debug)] pub(crate) struct RealMessage { mix_packet: MixPacket, fragment_id: FragmentIdentifier, diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 5e16cf0a0f..4d60e3ad54 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -327,16 +327,12 @@ impl NymClient { let lane = TransmissionLane::General; let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); - if self - .input_tx + self.input_tx .as_ref() .expect("start method was not called before!") .send(input_msg) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); } /// EXPERIMENTAL DIRECT RUST API @@ -345,16 +341,12 @@ impl NymClient { pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { let input_msg = InputMessage::new_reply(reply_surb, message); - if self - .input_tx + self.input_tx .as_ref() .expect("start method was not called before!") .send(input_msg) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); } /// EXPERIMENTAL DIRECT RUST API diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 53e6a4d491..0daa171532 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -97,9 +97,10 @@ impl Handler { // the ack control is now responsible for chunking, etc. let lane = TransmissionLane::ConnectionId(connection_id); let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); - if self.msg_input.send(input_msg).await.is_err() { - panic!(); - } + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); // on receiving a send, we reply back the current lane queue length for that connection id. // Note that this does _NOT_ take into account the packets that have been received but not @@ -123,9 +124,10 @@ impl Handler { } let input_msg = InputMessage::new_reply(reply_surb, message); - if self.msg_input.send(input_msg).await.is_err() { - panic!(); - } + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); None } diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index e77dba2975..df73d9ee7f 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -240,9 +240,10 @@ impl SocksClient { false, TransmissionLane::ConnectionId(self.connection_id), ); - if self.input_sender.send(input_message).await.is_err() { - panic!(); - } + self.input_sender + .send(input_message) + .await + .expect("InputMessageReceiver has stopped receiving!"); } async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) { diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 608674a169..c64dbe69e0 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -395,16 +395,12 @@ impl NymClient { let input_msg = InputMessage::new_fresh(recipient, message, false, lane); - if self - .input_tx + self.input_tx .as_ref() .expect("start method was not called before!") .send(input_msg) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); self } diff --git a/common/nymsphinx/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 62df1028e3..8c22b43f5c 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -5,7 +5,7 @@ use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressEr use nymsphinx_params::{PacketMode, PacketSize}; use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; #[derive(Debug)] pub enum MixPacketFormattingError { @@ -50,6 +50,19 @@ pub struct MixPacket { packet_mode: PacketMode, } +impl Debug for MixPacket { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "MixPacket to {:?} with packet_mode {:?}. Sphinx header: {:?}, payload length: {}", + self.next_hop, + self.packet_mode, + self.sphinx_packet.header, + self.sphinx_packet.payload.len() + ) + } +} + impl MixPacket { pub fn new( next_hop: NymNodeRoutingAddress, diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index bb5cf98f37..12caff2071 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -12,6 +12,7 @@ use futures::StreamExt; use log::*; use ordered_buffer::OrderedMessageSender; use socks5_requests::ConnectionId; +use std::fmt::Debug; use std::time::Duration; use std::{io, sync::Arc}; use task::ShutdownListener; @@ -25,15 +26,13 @@ async fn send_empty_close( adapter_fn: F, ) where F: Fn(ConnectionId, Vec, bool) -> S, + S: Debug, { let ordered_msg = message_sender.wrap_message(Vec::new()).into_bytes(); - if mix_sender + mix_sender .send(adapter_fn(connection_id, ordered_msg, true)) .await - .is_err() - { - panic!(); - } + .expect("BatchRealMessageReceiver has stopped receiving!"); } #[allow(clippy::too_many_arguments)] @@ -49,6 +48,7 @@ async fn deal_with_data( ) -> bool where F: Fn(ConnectionId, Vec, bool) -> S, + S: Debug, { let (read_data, is_finished) = match read_data { Some(data) => match data { @@ -89,13 +89,10 @@ where } } - if mix_sender + mix_sender .send(adapter_fn(connection_id, ordered_msg, is_finished)) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); if is_finished { // technically we already informed it when we sent the message to mixnet above @@ -168,6 +165,7 @@ pub(super) async fn run_inbound( ) -> OwnedReadHalf where F: Fn(ConnectionId, Vec, bool) -> S + Send + 'static, + S: Debug, { let mut available_reader = AvailableReader::new(&mut reader); let mut message_sender = OrderedMessageSender::new(); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index 573f09f884..fbd8cd21b3 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -4,6 +4,7 @@ use crate::connection_controller::ConnectionReceiver; use client_connections::LaneQueueLengths; use socks5_requests::ConnectionId; +use std::fmt::Debug; use std::{sync::Arc, time::Duration}; use task::ShutdownListener; use tokio::{net::TcpStream, sync::Notify}; @@ -54,7 +55,7 @@ pub struct ProxyRunner { impl ProxyRunner where - S: Send + 'static, + S: Debug + Send + 'static, { #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/common/socks5/requests/src/msg.rs b/common/socks5/requests/src/msg.rs index b13f346f35..2b688d8214 100644 --- a/common/socks5/requests/src/msg.rs +++ b/common/socks5/requests/src/msg.rs @@ -25,6 +25,7 @@ pub enum MessageError { UnknownMessageType, } +#[derive(Debug)] pub enum Message { Request(Request), Response(Response), diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 6bea582581..80528d1bed 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -212,16 +212,13 @@ impl ServiceProvider { ); // inform the remote that the connection is closed before it even was established - if mix_input_sender + mix_input_sender .send(( Socks5Message::Response(Response::new(conn_id, Vec::new(), true)), return_address, )) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); return; } @@ -271,7 +268,7 @@ impl ServiceProvider { if !self.open_proxy && !self.outbound_request_filter.check(&remote_addr) { let log_msg = format!("Domain {:?} failed filter check", remote_addr); log::info!("{}", log_msg); - if mix_input_sender + mix_input_sender .send(( Socks5Message::NetworkRequesterResponse(NetworkRequesterResponse::new( conn_id, log_msg, @@ -279,10 +276,7 @@ impl ServiceProvider { return_address, )) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); return; } diff --git a/service-providers/network-requester/src/statistics/collector.rs b/service-providers/network-requester/src/statistics/collector.rs index 72fc276860..ac43252c33 100644 --- a/service-providers/network-requester/src/statistics/collector.rs +++ b/service-providers/network-requester/src/statistics/collector.rs @@ -175,30 +175,22 @@ impl StatisticsCollector for ServiceStatisticsCollector { ), self.stats_provider_addr, ); - if self - .mix_input_sender + self.mix_input_sender .send(( Socks5Message::Request(connect_req), self.stats_provider_addr, )) .await - .is_err() - { - panic!(); - } + .expect("MixProxyReader has stopped receiving!"); trace!("Sending data to statistics service"); let mut message_sender = OrderedMessageSender::new(); let ordered_msg = message_sender.wrap_message(msg).into_bytes(); let send_req = Request::new_send(conn_id, ordered_msg, true); - if self - .mix_input_sender + self.mix_input_sender .send((Socks5Message::Request(send_req), self.stats_provider_addr)) .await - .is_err() - { - panic!(); - } + .expect("MixProxyReader has stopped receiving!"); Ok(()) } From e645d14005f615f3aed0513bcfbd01e1214a189e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 23:13:51 +0100 Subject: [PATCH 50/75] Make connection_id optional in ClientRequest::Send (#1798) --- .../native/examples/websocket_binarysend.rs | 4 +- clients/native/src/websocket/handler.rs | 40 ++++++++++++++----- .../native/websocket-requests/src/requests.rs | 19 +++++---- clients/native/websocket-requests/src/text.rs | 2 +- .../network-requester/src/core.rs | 2 +- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/clients/native/examples/websocket_binarysend.rs b/clients/native/examples/websocket_binarysend.rs index 160a9bf009..f103815af2 100644 --- a/clients/native/examples/websocket_binarysend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -43,7 +43,7 @@ async fn send_file_with_reply() { recipient, message: read_data, with_reply_surb: true, - connection_id: 0, + connection_id: Some(0), }; println!("sending content of 'dummy_file' over the mix network..."); @@ -92,7 +92,7 @@ async fn send_file_without_reply() { recipient, message: read_data, with_reply_surb: false, - connection_id: 0, + connection_id: Some(0), }; println!("sending content of 'dummy_file' over the mix network..."); diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 0daa171532..11d0dda55f 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -92,26 +92,42 @@ impl Handler { recipient: &Recipient, message: Vec, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, ) -> Option { + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); + // the ack control is now responsible for chunking, etc. - let lane = TransmissionLane::ConnectionId(connection_id); let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); self.msg_input .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::Reply + | TransmissionLane::Retransmission + | TransmissionLane::Control => return None, + TransmissionLane::ConnectionId(id) => id, + }; + // on receiving a send, we reply back the current lane queue length for that connection id. // Note that this does _NOT_ take into account the packets that have been received but not // yet reach `OutQueueControl`, so it might be a tad low. - if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { - let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); - return Some(ServerResponse::LaneQueueLength(connection_id, queue_length)); - } + let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { + log::warn!( + "Failed to get the lane queue length lock, \ + not responding back with the current queue length" + ); + return None; + }; - log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); - None + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) } async fn handle_reply( @@ -120,7 +136,13 @@ impl Handler { message: Vec, ) -> Option { if message.len() > ReplySurb::max_msg_len(Default::default()) { - return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySurb::max_msg_len(Default::default())))); + return Some( + ServerResponse::new_error( + format!( + "too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", + message.len(), ReplySurb::max_msg_len(Default::default())) + ) + ); } let input_msg = InputMessage::new_reply(reply_surb, message); diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index 9b5af6e70d..fafefb5827 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -34,7 +34,7 @@ pub enum ClientRequest { message: Vec, // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? with_reply_surb: bool, - connection_id: u64, + connection_id: Option, }, Reply { message: Vec, @@ -53,10 +53,10 @@ impl ClientRequest { recipient: Recipient, data: Vec, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, ) -> Vec { let data_len_bytes = (data.len() as u64).to_be_bytes(); - let conn_id_bytes = connection_id.to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); std::iter::once(SEND_REQUEST_TAG) .chain(std::iter::once(with_reply_surb as u8)) .chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant @@ -106,6 +106,11 @@ impl ClientRequest { connection_id_bytes .copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) + }; let data_len_bytes = &b[2 + Recipient::LEN + size_of::()..2 + Recipient::LEN + 2 * size_of::()]; @@ -350,7 +355,7 @@ mod tests { recipient, message: b"foomp".to_vec(), with_reply_surb: false, - connection_id: 42, + connection_id: Some(42), }; let bytes = send_request_no_surb.serialize(); @@ -365,7 +370,7 @@ mod tests { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); assert!(!with_reply_surb); - assert_eq!(connection_id, 42) + assert_eq!(connection_id, Some(42)) } _ => unreachable!(), } @@ -374,7 +379,7 @@ mod tests { recipient, message: b"foomp".to_vec(), with_reply_surb: true, - connection_id: 213, + connection_id: None, }; let bytes = send_request_surb.serialize(); @@ -389,7 +394,7 @@ mod tests { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); assert!(with_reply_surb); - assert_eq!(connection_id, 213) + assert_eq!(connection_id, None) } _ => unreachable!(), } diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs index cca6569f4f..1083ecd9a8 100644 --- a/clients/native/websocket-requests/src/text.rs +++ b/clients/native/websocket-requests/src/text.rs @@ -20,7 +20,7 @@ pub(super) enum ClientRequestText { message: String, recipient: String, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, }, SelfAddress, #[serde(rename_all = "camelCase")] diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 80528d1bed..870376d896 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -102,7 +102,7 @@ impl ServiceProvider { recipient: return_address, message: msg.into_bytes(), with_reply_surb: false, - connection_id: conn_id, + connection_id: Some(conn_id), }; let message = Message::Binary(response_message.serialize()); From a9983003d41437e3a366ce1183e11a4d6544d379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 23:29:37 +0100 Subject: [PATCH 51/75] changelog: add missing entry for fixing message decrypt in gateway-client --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de700926d1..5d6d57cd6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,16 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747]) - socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) +### Fixed + +- gateway-client: fix decrypting stored messages on reconnect ([#1786]) + [#1678]: https://github.com/nymtech/nym/pull/1678 [#1708]: https://github.com/nymtech/nym/pull/1708 [#1720]: https://github.com/nymtech/nym/pull/1720 [#1747]: https://github.com/nymtech/nym/pull/1747 [#1783]: https://github.com/nymtech/nym/pull/1783 +[#1786]: https://github.com/nymtech/nym/pull/1786 ## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09) From 9c361385a771383c9de444db5a64be6af2abc4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 23:45:25 +0100 Subject: [PATCH 52/75] websocket-requests: fix length check before deserialize (#1799) --- .../native/websocket-requests/src/requests.rs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index fafefb5827..315399ae3d 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -239,7 +239,14 @@ impl ClientRequest { } // CLOSED_CONNECTION_REQUEST_TAG - fn deserialize_closed_connection(b: &[u8]) -> Self { + fn deserialize_closed_connection(b: &[u8]) -> Result { + if b.len() != 1 + size_of::() { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + "the received closed connection has invalid length".to_string(), + )); + } + // this MUST match because it was called by 'deserialize' debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG); @@ -247,7 +254,7 @@ impl ClientRequest { connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); - ClientRequest::ClosedConnection(connection_id) + Ok(ClientRequest::ClosedConnection(connection_id)) } // GET_LANE_QUEUE_LENGHT_TAG @@ -259,7 +266,14 @@ impl ClientRequest { } // GET_LANE_QUEUE_LENGHT_TAG - fn deserialize_get_lane_queue_length(b: &[u8]) -> Self { + fn deserialize_get_lane_queue_length(b: &[u8]) -> Result { + if b.len() != 1 + size_of::() { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + "the received get lane queue length has invalid length".to_string(), + )); + } + // this MUST match because it was called by 'deserialize' debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); @@ -267,7 +281,7 @@ impl ClientRequest { connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); - ClientRequest::GetLaneQueueLength(connection_id) + Ok(ClientRequest::GetLaneQueueLength(connection_id)) } pub fn serialize(self) -> Vec { @@ -318,8 +332,8 @@ impl ClientRequest { SEND_REQUEST_TAG => Self::deserialize_send(b), REPLY_REQUEST_TAG => Self::deserialize_reply(b), SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), - CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)), - GET_LANE_QUEUE_LENGHT_TAG => Ok(Self::deserialize_get_lane_queue_length(b)), + CLOSED_CONNECTION_REQUEST_TAG => Self::deserialize_closed_connection(b), + GET_LANE_QUEUE_LENGHT_TAG => Self::deserialize_get_lane_queue_length(b), n => Err(error::Error::new( ErrorKind::UnknownRequest, format!("type {n}"), From c6d624a3b336d054f8dc1c8bca653fabe51dc944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 12:03:58 +0100 Subject: [PATCH 53/75] Network-requester: throttle inbound connections (#1789) * Return and handle ClientRequest::LaneQueueLenghts * Pass lane queue lengths to inbound future * Remove unused self reference * Request lane queue lengths periodically for all open connections * Add timeouts * Rename to ConnectionCommandSender and Receiver * Rename to client_connection_tx/rx * Fix wasm build * Replace bool with enum --- .../src/client/real_messages_control/mod.rs | 6 +- .../real_traffic_stream.rs | 24 ++-- clients/native/src/client/mod.rs | 23 ++-- clients/native/src/websocket/handler.rs | 43 +++++-- .../native/websocket-requests/src/requests.rs | 27 +++++ clients/socks5/src/client/mod.rs | 16 +-- clients/socks5/src/socks/server.rs | 13 ++- clients/webassembly/src/client/mod.rs | 10 +- common/client-connections/src/lib.rs | 20 +++- .../src/connection_controller.rs | 49 ++++++-- .../proxy-helpers/src/proxy_runner/inbound.rs | 34 ++++-- .../network-requester/src/connection.rs | 4 +- .../network-requester/src/core.rs | 107 ++++++++++++++---- 13 files changed, 282 insertions(+), 94 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index d8b9dd34a6..ffeeea61e3 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -16,7 +16,7 @@ use crate::{ }, spawn_future, }; -use client_connections::{ClosedConnectionReceiver, LaneQueueLengths}; +use client_connections::{ConnectionCommandReceiver, LaneQueueLengths}; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; @@ -115,7 +115,7 @@ impl RealMessagesController { topology_access: TopologyAccessor, #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, lane_queue_lengths: LaneQueueLengths, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, ) -> Self { let rng = OsRng; @@ -166,7 +166,7 @@ impl RealMessagesController { config.self_recipient, topology_access, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ); RealMessagesController { diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index d6f34a5460..68b078e53c 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -5,7 +5,7 @@ use crate::client::mix_traffic::BatchMixMessageSender; use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender; use crate::client::topology_control::TopologyAccessor; use client_connections::{ - ClosedConnectionReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, + ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, }; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; @@ -134,7 +134,7 @@ where /// Incoming channel for being notified of closed connections, so that we can close lanes /// corresponding to connections. To avoid sending traffic unnecessary - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, /// Report queue lengths so that upstream can backoff sending data, and keep connections open. lane_queue_lengths: LaneQueueLengths, @@ -182,7 +182,7 @@ where our_full_destination: Recipient, topology_access: TopologyAccessor, lane_queue_lengths: LaneQueueLengths, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, ) -> Self { OutQueueControl { config, @@ -196,7 +196,7 @@ where rng, topology_access, transmission_buffer: Default::default(), - closed_connection_rx, + client_connection_rx, lane_queue_lengths, } } @@ -262,7 +262,7 @@ where self.sent_notify(fragment_id); } - // In addition to closing connections on receiving messages throught closed_connection_rx, + // In addition to closing connections on receiving messages throught client_connection_rx, // also close connections when sufficiently stale. self.transmission_buffer.prune_stale_connections(); @@ -335,8 +335,11 @@ where // Start by checking if we have any incoming messages about closed connections // NOTE: this feels a bit iffy, the `OutQueueControl` is getting ripe for a rewrite to // something simpler. - if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) { - self.on_close_connection(id); + if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) { + match id { + ConnectionCommand::Close(id) => self.on_close_connection(id), + ConnectionCommand::ActiveConnections(_) => panic!(), + } } if let Some(ref mut next_delay) = &mut self.next_delay { @@ -411,8 +414,11 @@ where fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll> { // Start by checking if we have any incoming messages about closed connections - if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) { - self.on_close_connection(id); + if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) { + match id { + ConnectionCommand::Close(id) => self.on_close_connection(id), + ConnectionCommand::ActiveConnections(_) => panic!(), + } } match Pin::new(&mut self.real_receiver).poll_recv(cx) { diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 357293d17e..5e16cf0a0f 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use client_connections::{ - ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths, TransmissionLane, + ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, }; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ @@ -123,7 +123,7 @@ impl NymClient { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, lane_queue_lengths: LaneQueueLengths, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, shutdown: ShutdownListener, ) { let mut controller_config = real_messages_control::Config::new( @@ -154,7 +154,7 @@ impl NymClient { topology_accessor, reply_key_storage, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ) .start_with_shutdown(shutdown); } @@ -299,15 +299,17 @@ impl NymClient { &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + shared_lane_queue_lengths: LaneQueueLengths, + client_connection_tx: ConnectionCommandSender, ) { info!("Starting websocket listener..."); let websocket_handler = websocket::Handler::new( msg_input, - closed_connection_tx, + client_connection_tx, buffer_requester, &self.as_mix_recipient(), + shared_lane_queue_lengths, ); websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler); @@ -449,11 +451,11 @@ impl NymClient { // Channels that the websocket listener can use to signal downstream to the real traffic // controller that connections are closed. - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); // Shared queue length data. Published by the `OutQueueController` in the client, and used // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) - let shared_lane_queue_length = LaneQueueLengths::new(); + let shared_lane_queue_lengths = LaneQueueLengths::new(); self.start_real_traffic_controller( shared_topology_accessor.clone(), @@ -461,8 +463,8 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - shared_lane_queue_length, - closed_connection_rx, + shared_lane_queue_lengths.clone(), + client_connection_rx, shutdown.subscribe(), ); @@ -482,7 +484,8 @@ impl NymClient { SocketType::WebSocket => self.start_websocket_listener( received_buffer_request_sender, input_sender, - closed_connection_tx, + shared_lane_queue_lengths, + client_connection_tx, ), SocketType::None => { // if we did not start the socket, it means we're running (supposedly) in the native mode diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 07b196354d..53e6a4d491 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::{ClosedConnectionSender, TransmissionLane}; +use client_connections::{ + ConnectionCommand, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, +}; use client_core::client::{ inbound_messages::{InputMessage, InputMessageSender}, received_buffer::{ @@ -35,11 +37,12 @@ impl Default for ReceivedResponseType { pub(crate) struct Handler { msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: Recipient, socket: Option>, received_response_type: ReceivedResponseType, + lane_queue_lengths: LaneQueueLengths, } // clone is used to use handler on a new connection, which initially is `None` @@ -47,11 +50,12 @@ impl Clone for Handler { fn clone(&self) -> Self { Handler { msg_input: self.msg_input.clone(), - closed_connection_tx: self.closed_connection_tx.clone(), + client_connection_tx: self.client_connection_tx.clone(), buffer_requester: self.buffer_requester.clone(), self_full_address: self.self_full_address, socket: None, received_response_type: Default::default(), + lane_queue_lengths: self.lane_queue_lengths.clone(), } } } @@ -67,17 +71,19 @@ impl Drop for Handler { impl Handler { pub(crate) fn new( msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: &Recipient, + lane_queue_lengths: LaneQueueLengths, ) -> Self { Handler { msg_input, - closed_connection_tx, + client_connection_tx, buffer_requester, self_full_address: *self_full_address, socket: None, received_response_type: Default::default(), + lane_queue_lengths, } } @@ -95,6 +101,15 @@ impl Handler { panic!(); } + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + return Some(ServerResponse::LaneQueueLength(connection_id, queue_length)); + } + + log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); None } @@ -120,12 +135,25 @@ impl Handler { } fn handle_closed_connection(&self, connection_id: u64) -> Option { - self.closed_connection_tx - .unbounded_send(connection_id) + self.client_connection_tx + .unbounded_send(ConnectionCommand::Close(connection_id)) .unwrap(); None } + fn handle_get_lane_queue_length(&self, connection_id: u64) -> Option { + let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { + log::warn!( + "Failed to get the lane queue length lock, not responding back with the current queue length" + ); + return None; + }; + + let lane = TransmissionLane::ConnectionId(connection_id); + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) + } + async fn handle_request(&mut self, request: ClientRequest) -> Option { match request { ClientRequest::Send { @@ -143,6 +171,7 @@ impl Handler { } => self.handle_reply(reply_surb, message).await, ClientRequest::SelfAddress => Some(self.handle_self_address()), ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id), + ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id), } } diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index f681b4264c..9b5af6e70d 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -23,6 +23,9 @@ pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02; /// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03; +/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] +pub const GET_LANE_QUEUE_LENGHT_TAG: u8 = 0x04; + #[allow(non_snake_case)] #[derive(Debug)] pub enum ClientRequest { @@ -39,6 +42,7 @@ pub enum ClientRequest { }, SelfAddress, ClosedConnection(u64), + GetLaneQueueLength(u64), } // we could have been parsing it directly TryFrom, but we want to retain @@ -241,6 +245,26 @@ impl ClientRequest { ClientRequest::ClosedConnection(connection_id) } + // GET_LANE_QUEUE_LENGHT_TAG + fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec { + let conn_id_bytes = connection_id.to_be_bytes(); + std::iter::once(GET_LANE_QUEUE_LENGHT_TAG) + .chain(conn_id_bytes.iter().copied()) + .collect() + } + + // GET_LANE_QUEUE_LENGHT_TAG + fn deserialize_get_lane_queue_length(b: &[u8]) -> Self { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); + + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + + ClientRequest::GetLaneQueueLength(connection_id) + } + pub fn serialize(self) -> Vec { match self { ClientRequest::Send { @@ -258,6 +282,8 @@ impl ClientRequest { ClientRequest::SelfAddress => Self::serialize_self_address(), ClientRequest::ClosedConnection(id) => Self::serialize_closed_connection(id), + + ClientRequest::GetLaneQueueLength(id) => Self::serialize_get_lane_queue_lengths(id), } } @@ -288,6 +314,7 @@ impl ClientRequest { REPLY_REQUEST_TAG => Self::deserialize_reply(b), SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)), + GET_LANE_QUEUE_LENGHT_TAG => Ok(Self::deserialize_get_lane_queue_length(b)), n => Err(error::Error::new( ErrorKind::UnknownRequest, format!("type {n}"), diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index ef07360e2b..c6b734a749 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -9,7 +9,7 @@ use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, LaneQueueLengths}; +use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -120,7 +120,7 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { @@ -152,7 +152,7 @@ impl NymClient { topology_accessor, reply_key_storage, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ) .start_with_shutdown(shutdown); } @@ -297,7 +297,7 @@ impl NymClient { &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { @@ -316,7 +316,7 @@ impl NymClient { ); tokio::spawn(async move { sphinx_socks - .serve(msg_input, buffer_requester, closed_connection_tx) + .serve(msg_input, buffer_requester, client_connection_tx) .await }); } @@ -426,7 +426,7 @@ impl NymClient { // Channel for announcing closed (socks5) connections by the controller. // This will be forwarded to `OutQueueControl` - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); // Shared queue length data. Published by the `OutQueueController` in the client, and used // primarily to throttle incoming connections @@ -438,7 +438,7 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + client_connection_rx, shared_lane_queue_lengths.clone(), shutdown.subscribe(), ); @@ -458,7 +458,7 @@ impl NymClient { self.start_socks5_listener( received_buffer_request_sender, input_sender, - closed_connection_tx, + client_connection_tx, shared_lane_queue_lengths, shutdown.subscribe(), ); diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index af5a6f9fed..f61d6052a0 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -4,13 +4,13 @@ use super::{ mixnet_responses::MixnetResponseListener, types::{ResponseCode, SocksProxyError}, }; -use client_connections::{ClosedConnectionSender, LaneQueueLengths}; +use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, }; use log::*; use nymsphinx::addressing::clients::Recipient; -use proxy_helpers::connection_controller::Controller; +use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller}; use std::net::SocketAddr; use task::ShutdownListener; use tokio::net::TcpListener; @@ -55,14 +55,17 @@ impl SphinxSocksServer { &mut self, input_sender: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, ) -> Result<(), SocksProxyError> { let listener = TcpListener::bind(self.listening_address).await.unwrap(); info!("Serving Connections..."); // controller for managing all active connections - let (mut active_streams_controller, controller_sender) = - Controller::new(closed_connection_tx, self.shutdown.clone()); + let (mut active_streams_controller, controller_sender) = Controller::new( + client_connection_tx, + BroadcastActiveConnections::Off, + self.shutdown.clone(), + ); tokio::spawn(async move { active_streams_controller.run().await; }); diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 654989644e..608674a169 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; -use client_connections::{ClosedConnectionReceiver, LaneQueueLengths, TransmissionLane}; +use client_connections::{ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane}; use client_core::client::{ cover_traffic_stream::LoopCoverTrafficStream, inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, @@ -128,7 +128,7 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, lane_queue_lengths: LaneQueueLengths, ) { let mut controller_config = real_messages_control::Config::new( @@ -155,7 +155,7 @@ impl NymClient { mix_sender, topology_accessor, lane_queue_lengths, - closed_connection_rx, + client_connection_rx, ) .start(); } @@ -334,7 +334,7 @@ impl NymClient { // Channel that the real traffix controller can listed to for closing connections. // Currently unused in the wasm client. - let (_closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (_client_connection_tx, client_connection_rx) = mpsc::unbounded(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. @@ -364,7 +364,7 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + client_connection_rx, shared_lane_queue_lengths, ); diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index d1782e8d90..515938ed54 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -16,11 +16,21 @@ pub enum TransmissionLane { ConnectionId(ConnectionId), } -/// Announce connections that are closed, for whoever is interested. -/// One usecase is that the network-requester and socks5-client wants to know about this, so that -/// they can forward this to the `OutQueueControl` (via `ClientRequest` for the network-requester) -pub type ClosedConnectionSender = mpsc::UnboundedSender; -pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver; +/// Used by the connection controller to report current state for client connections. +pub type ConnectionCommandSender = mpsc::UnboundedSender; +pub type ConnectionCommandReceiver = mpsc::UnboundedReceiver; + +pub enum ConnectionCommand { + // Announce that at a connection was closed. E.g the `OutQueueControl` uses this to discard + // transmission lanes. + Close(ConnectionId), + + // In the network requester for example, we usually want to broadcast active connections + // regularly, so we know what connections we need to request lane queue lengths for from the + // client. + // In the socks5-client, this is not needed since have direct access to the lane queue lengths. + ActiveConnections(Vec), +} // The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down // if needed. diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index d78915d2a5..12fdba7dbb 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -1,14 +1,18 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::ClosedConnectionSender; +use client_connections::{ConnectionCommand, ConnectionCommandSender}; use futures::channel::mpsc; use futures::StreamExt; use log::*; use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData}; use socks5_requests::ConnectionId; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::Duration, +}; use task::ShutdownListener; +use tokio::time; /// A generic message produced after reading from a socket/connection. It includes data that was /// actually read alongside boolean indicating whether the connection got closed so that @@ -64,6 +68,12 @@ impl ActiveConnection { } } +#[derive(PartialEq, Eq)] +pub enum BroadcastActiveConnections { + On, + Off, +} + /// Controller represents a way of managing multiple open connections that are used for socks5 /// proxy. pub struct Controller { @@ -75,7 +85,11 @@ pub struct Controller { recently_closed: HashSet, // Broadcast closed connections - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, + + // The controller can broadcast active connections. This is useful in the network-requester + // where its used to query the client for lane queue lengths + broadcast_connections: BroadcastActiveConnections, // TODO: this can potentially be abused to ddos and kill provider. Not sure at this point // how to handle it more gracefully @@ -89,7 +103,8 @@ pub struct Controller { impl Controller { pub fn new( - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, + broadcast_connections: BroadcastActiveConnections, shutdown: ShutdownListener, ) -> (Self, ControllerSender) { let (sender, receiver) = mpsc::unbounded(); @@ -98,7 +113,8 @@ impl Controller { active_connections: HashMap::new(), receiver, recently_closed: HashSet::new(), - closed_connection_tx, + client_connection_tx, + broadcast_connections, pending_messages: HashMap::new(), shutdown, }, @@ -137,7 +153,10 @@ impl Controller { self.recently_closed.insert(conn_id); // Announce closed connections, currently used by the `OutQueueControl`. - if let Err(err) = self.closed_connection_tx.unbounded_send(conn_id) { + if let Err(err) = self + .client_connection_tx + .unbounded_send(ConnectionCommand::Close(conn_id)) + { if self.shutdown.is_shutdown_poll() { log::debug!("Failed to send: {}", err); } else { @@ -146,6 +165,15 @@ impl Controller { } } + fn broadcast_active_connections(&mut self) { + // What about the recently closed ones? Hopefully we can ignore them ... + let conn_ids = self.active_connections.keys().copied().collect(); + + self.client_connection_tx + .unbounded_send(ConnectionCommand::ActiveConnections(conn_ids)) + .unwrap(); + } + fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec, is_closed: bool) { if let Some(active_connection) = self.active_connections.get_mut(&conn_id) { if !payload.is_empty() { @@ -202,6 +230,8 @@ impl Controller { } pub async fn run(&mut self) { + let mut interval = time::interval(Duration::from_millis(500)); + loop { tokio::select! { command = self.receiver.next() => match command { @@ -216,7 +246,12 @@ impl Controller { log::trace!("SOCKS5 Controller: Stopping since channel closed"); break; } - } + }, + _ = interval.tick() => { + if self.broadcast_connections == BroadcastActiveConnections::On { + self.broadcast_active_connections(); + } + }, } } assert!(self.shutdown.is_shutdown_poll()); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index ef0a09336d..bb5cf98f37 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -106,23 +106,37 @@ where } async fn wait_until_lane_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { - wait_for_lane( - lane_queue_lengths, - connection_id, - 0, - Duration::from_millis(500), + if tokio::time::timeout( + Duration::from_secs(2 * 60), + wait_for_lane( + lane_queue_lengths, + connection_id, + 0, + Duration::from_millis(500), + ), ) .await + .is_err() + { + log::warn!("Wait until lane empty timed out"); + } } async fn wait_until_lane_almost_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { - wait_for_lane( - lane_queue_lengths, - connection_id, - 10, - Duration::from_millis(100), + if tokio::time::timeout( + Duration::from_secs(2 * 60), + wait_for_lane( + lane_queue_lengths, + connection_id, + 10, + Duration::from_millis(100), + ), ) .await + .is_err() + { + log::debug!("Wait until lane almost empty timed out"); + } } async fn wait_for_lane( diff --git a/service-providers/network-requester/src/connection.rs b/service-providers/network-requester/src/connection.rs index 6627fdbf4b..95bdac06a3 100644 --- a/service-providers/network-requester/src/connection.rs +++ b/service-providers/network-requester/src/connection.rs @@ -1,6 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use client_connections::LaneQueueLengths; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::ConnectionReceiver; use proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner}; @@ -40,6 +41,7 @@ impl Connection { &mut self, mix_receiver: ConnectionReceiver, mix_sender: MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let stream = self.conn.take().unwrap(); @@ -53,7 +55,7 @@ impl Connection { mix_receiver, mix_sender, connection_id, - None, + Some(lane_queue_lengths), shutdown, ) .run(move |conn_id, read_data, socket_closed| { diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 88d66d159d..6bea582581 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -7,14 +7,18 @@ use crate::error::NetworkRequesterError; use crate::statistics::ServiceStatisticsCollector; use crate::websocket; use crate::websocket::TSWebsocketStream; -use client_connections::ClosedConnectionReceiver; +use client_connections::{ + ConnectionCommand, ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane, +}; use futures::channel::mpsc; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::receiver::ReconstructedMessage; -use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender}; +use proxy_helpers::connection_controller::{ + BroadcastActiveConnections, Controller, ControllerCommand, ControllerSender, +}; use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender}; use socks5_requests::{ ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response, @@ -70,7 +74,7 @@ impl ServiceProvider { mut websocket_writer: SplitSink, mut mix_reader: MixProxyReader<(Socks5Message, Recipient)>, stats_collector: Option, - mut closed_connection_rx: ClosedConnectionReceiver, + mut client_connection_rx: ConnectionCommandReceiver, ) { loop { tokio::select! { @@ -108,17 +112,49 @@ impl ServiceProvider { break; } }, - Some(id) = closed_connection_rx.next() => { - let msg = ClientRequest::ClosedConnection(id); - let ws_msg = Message::Binary(msg.serialize()); - websocket_writer.send(ws_msg).await.unwrap(); - } + Some(command) = client_connection_rx.next() => { + match command { + ConnectionCommand::Close(id) => { + let msg = ClientRequest::ClosedConnection(id); + let ws_msg = Message::Binary(msg.serialize()); + websocket_writer.send(ws_msg).await.unwrap(); + } + ConnectionCommand::ActiveConnections(ids) => { + // We can optimize this by sending a single request, but this is + // usually in the low single digits, max a few tens, so we leave that + // for a rainy day. + // Also that means fiddling with the currently manual + // serialize/deserialize we do with ClientRequests ... bleh + for id in ids { + log::trace!("Requesting lane queue length for: {}", id); + let msg = ClientRequest::GetLaneQueueLength(id); + let ws_msg = Message::Binary(msg.serialize()); + websocket_writer.send(ws_msg).await.unwrap(); + } + } + } + }, } } } + fn handle_lane_queue_length_response( + lane_queue_lengths: &LaneQueueLengths, + lane: u64, + queue_length: usize, + ) { + log::trace!("Received LaneQueueLength lane: {lane}, queue_length: {queue_length}"); + if let Ok(mut lane_queue_lengths) = lane_queue_lengths.lock() { + let lane = TransmissionLane::ConnectionId(lane); + lane_queue_lengths.map.insert(lane, queue_length); + } else { + log::warn!("Unable to lock lane queue lengths, skipping updating received lane length") + } + } + async fn read_websocket_message( websocket_reader: &mut SplitStream, + lane_queue_lengths: LaneQueueLengths, ) -> Option { while let Some(msg) = websocket_reader.next().await { let data = msg @@ -139,6 +175,14 @@ impl ServiceProvider { let received = match deserialized_message { ServerResponse::Received(received) => received, + ServerResponse::LaneQueueLength(lane, queue_length) => { + Self::handle_lane_queue_length_response( + &lane_queue_lengths, + lane, + queue_length, + ); + continue; + } ServerResponse::Error(err) => { panic!("received error from native client! - {}", err) } @@ -155,6 +199,7 @@ impl ServiceProvider { return_address: Recipient, controller_sender: ControllerSender, mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let mut conn = match Connection::new(conn_id, remote_addr.clone(), return_address).await { @@ -196,7 +241,7 @@ impl ServiceProvider { ); // run the proxy on the connection - conn.run_proxy(mix_receiver, mix_input_sender, shutdown) + conn.run_proxy(mix_receiver, mix_input_sender, lane_queue_lengths, shutdown) .await; // proxy is done - remove the access channel from the controller @@ -212,10 +257,12 @@ impl ServiceProvider { ); } + #[allow(clippy::too_many_arguments)] async fn handle_proxy_connect( &mut self, controller_sender: &mut ControllerSender, mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, conn_id: ConnectionId, remote_addr: String, return_address: Recipient, @@ -250,6 +297,7 @@ impl ServiceProvider { return_address, controller_sender_clone, mix_input_sender_clone, + lane_queue_lengths, shutdown, ) .await @@ -257,7 +305,6 @@ impl ServiceProvider { } fn handle_proxy_send( - &self, controller_sender: &mut ControllerSender, conn_id: ConnectionId, data: Vec, @@ -273,6 +320,7 @@ impl ServiceProvider { raw_request: &[u8], controller_sender: &mut ControllerSender, mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, stats_collector: Option, shutdown: ShutdownListener, ) { @@ -296,6 +344,7 @@ impl ServiceProvider { self.handle_proxy_connect( controller_sender, mix_input_sender, + lane_queue_lengths, req.conn_id, req.remote_addr, req.return_address, @@ -319,7 +368,7 @@ impl ServiceProvider { .processed(remote_addr, data.len() as u32); } } - self.handle_proxy_send(controller_sender, conn_id, data, closed) + Self::handle_proxy_send(controller_sender, conn_id, data, closed) } }, Socks5Message::Response(_) | Socks5Message::NetworkRequesterResponse(_) => {} @@ -341,16 +390,23 @@ impl ServiceProvider { // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). let shutdown = task::ShutdownNotifier::default(); - // Channel for announcing closed (socks5) connections by the controller. - // The `mixnet_response_listener` will forward this info to the client using a - // `ClientRequest`. - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + // Channel for announcing client connection state by the controller. + // The `mixnet_response_listener` will use this to either report closed connection to the + // client or request lane queue lengths. + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); // Controller for managing all active connections. // We provide it with a ShutdownListener since it requires it, even though for the network // requester shutdown signalling is not yet fully implemented. - let (mut active_connections_controller, mut controller_sender) = - Controller::new(closed_connection_tx, shutdown.subscribe()); + let (mut active_connections_controller, mut controller_sender) = Controller::new( + client_connection_tx, + BroadcastActiveConnections::On, + shutdown.subscribe(), + ); tokio::spawn(async move { active_connections_controller.run().await; @@ -378,7 +434,7 @@ impl ServiceProvider { websocket_writer, mix_input_receiver, stats_collector_clone, - closed_connection_rx, + client_connection_rx, ) .await; }); @@ -386,12 +442,14 @@ impl ServiceProvider { println!("\nAll systems go. Press CTRL-C to stop the server."); // for each incoming message from the websocket... (which in 99.99% cases is going to be a mix message) loop { - let received = match Self::read_websocket_message(&mut websocket_reader).await { - Some(msg) => msg, - None => { - error!("The websocket stream has finished!"); - return Ok(()); - } + let Some(received) = Self::read_websocket_message( + &mut websocket_reader, + shared_lane_queue_lengths.clone() + ) + .await + else { + log::error!("The websocket stream has finished!"); + return Ok(()); }; let raw_message = received.message; @@ -401,6 +459,7 @@ impl ServiceProvider { &raw_message, &mut controller_sender, &mix_input_sender, + shared_lane_queue_lengths.clone(), stats_collector.clone(), shutdown.subscribe(), ) From c9779df2a4db65e6533d1028b42d0690aba55653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 15:51:51 +0100 Subject: [PATCH 54/75] rust: bump required version to 1.65 in some crates that need it --- clients/native/Cargo.toml | 2 +- service-providers/network-requester/Cargo.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 4f4cca6a85..ab22e76a9d 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -4,7 +4,7 @@ version = "1.1.0" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" -rust-version = "1.56" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index eb3cc1d229..8d81920af5 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -6,6 +6,7 @@ name = "nym-network-requester" version = "1.1.0" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2021" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 4ff741ed9af2501ff95444b69cfc296646699f00 Mon Sep 17 00:00:00 2001 From: Fran Arbanas Date: Wed, 23 Nov 2022 16:13:18 +0100 Subject: [PATCH 55/75] Add step to release GH actions (#1792) * feat: add a release step to nym contracts GH action * feat: add shrinking the size of wasm --- .github/workflows/contracts-build.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index 84428b01cd..a25806c1b7 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -1,10 +1,8 @@ name: Build release of Nym smart contracts on: workflow_dispatch: - -defaults: - run: - working-directory: contracts + release: + types: [created] jobs: build: @@ -21,7 +19,7 @@ jobs: components: rustfmt, clippy - name: Build release contracts - run: RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + run: make wasm - name: Upload Mixnet Contract Artifact uses: actions/upload-artifact@v3 @@ -36,3 +34,11 @@ jobs: name: vesting_contract.wasm path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm retention-days: 5 + + - name: Upload to release based on tag name + uses: softprops/action-gh-release@v1 + if: github.event_name == 'release' + with: + files: | + contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm + contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm From 39ca9c22af0939b14ebcec2da763dfe46a4eae2f Mon Sep 17 00:00:00 2001 From: cgi-bin/ <6095048+sven-hash@users.noreply.github.com> Date: Wed, 23 Nov 2022 16:14:43 +0100 Subject: [PATCH 56/75] Possibilty to change gateway ws listener (#1779) * add: set gatewayListener * Update types.ts * Update worker.ts --- sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts | 5 +++++ sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts b/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts index 8e984f66c2..36aacd5adb 100644 --- a/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts +++ b/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts @@ -27,6 +27,11 @@ export interface NymClientConfig { * Optional. The identity key of the preferred gateway to connect to. */ preferredGatewayIdentityKey?: string; + + /** + * Optional. The listener websocket of the preferred gateway to connect to. + */ + gatewayListener?: string; /** * Optional. Settings for the WASM client. diff --git a/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts b/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts index 61df19b408..cb1c8a32e4 100644 --- a/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts +++ b/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts @@ -133,7 +133,11 @@ wasm_bindgen(wasmUrl) config.validatorApiUrl, config.preferredGatewayIdentityKey, ); - + + // set a different gatewayListener in order to avoid workaround ws over https error + if (config.gatewayListener) + gatewayEndpoint.gateway_listener = config.gatewayListener; + // create the client, passing handlers for events wrapper.init( new wasm_bindgen.Config( From f1378c3488a0bcc1b3791acc1f9479b8e37cb778 Mon Sep 17 00:00:00 2001 From: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> Date: Wed, 23 Nov 2022 15:51:21 +0000 Subject: [PATCH 57/75] Update contracts-build.yml --- .github/workflows/contracts-build.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index a25806c1b7..3921bd3a6d 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -9,6 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + + - name: Check the release tag starts with `nym-contracts-` + if: startsWith(github.ref, 'refs/tags/nym-contracts-') == false && github.event_name != 'workflow_dispatch' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Release tag did not start with nym-contracts-...') - name: Install Rust stable uses: actions-rs/toolchain@v1 From 30dc929e40d3571bb070b1bb5d1f27d2bff0aa14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 16:56:09 +0100 Subject: [PATCH 58/75] real_traffic_stream: reduce frequency of status print (#1794) --- .../real_traffic_stream.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 68b078e53c..87368536a5 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -464,17 +464,23 @@ where let lanes = self.transmission_buffer.num_lanes(); let mult = self.sending_delay_controller.current_multiplier(); let delay = self.current_average_message_sending_delay().as_millis(); - if self.config.disable_poisson_packet_distribution { - log::info!( + let status_str = if self.config.disable_poisson_packet_distribution { + format!( "Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), no delay", backlog - ); + ) } else { - log::info!( + format!( "Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), avg delay: {}ms ({mult})", - backlog, - delay - ); + backlog, delay + ) + }; + if packets > 1000 { + log::warn!("{status_str}"); + } else if packets > 0 { + log::info!("{status_str}"); + } else { + log::debug!("{status_str}"); } } From bb98d796a8cf17821a7eafbd8af42aa4a1da17fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 20:59:12 +0100 Subject: [PATCH 59/75] Update wallet and connect lock files (#1793) --- nym-connect/Cargo.lock | 136 ++++++++++++++++++++++++++++--- nym-wallet/Cargo.lock | 176 +++++++++++++++++++++++++++++++---------- 2 files changed, 257 insertions(+), 55 deletions(-) diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 918cc02baf..cab899eacf 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -268,6 +268,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.8.1" @@ -334,11 +346,25 @@ dependencies = [ "digest 0.9.0", "ff 0.10.1", "group 0.10.0", - "pairing", + "pairing 0.20.0", "rand_core 0.6.4", "subtle 2.4.1", ] +[[package]] +name = "bls12_381" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" +dependencies = [ + "digest 0.9.0", + "ff 0.11.1", + "group 0.11.0", + "pairing 0.21.0", + "rand_core 0.6.4", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "brotli" version = "3.3.4" @@ -599,6 +625,7 @@ name = "client-connections" version = "0.1.0" dependencies = [ "futures", + "log", ] [[package]] @@ -689,6 +716,8 @@ version = "0.1.0" dependencies = [ "contracts-common", "cosmwasm-std", + "cw-utils", + "multisig-contract-common", "schemars", "serde", ] @@ -942,11 +971,10 @@ dependencies = [ name = "credentials" version = "0.1.0" dependencies = [ - "bls12_381", + "bls12_381 0.5.0", "coconut-interface", "crypto", "thiserror", - "url", "validator-api-requests", "validator-client", ] @@ -1136,9 +1164,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -1170,6 +1198,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw2" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + [[package]] name = "cw3" version = "0.13.4" @@ -1182,6 +1222,22 @@ dependencies = [ "serde", ] +[[package]] +name = "cw3-fixed-multisig" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw4" version = "0.13.4" @@ -1337,6 +1393,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec", + "bls12_381 0.6.0", + "bs58", + "ff 0.11.1", + "group 0.11.0", + "lazy_static", + "pemstore", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1660,6 +1737,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2116,6 +2199,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ + "byteorder", "ff 0.11.1", "rand_core 0.6.4", "subtle 2.4.1", @@ -3040,6 +3124,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -3292,6 +3377,7 @@ dependencies = [ "serde", "snafu", "socks5-requests", + "tap", "task", "thiserror", "tokio", @@ -3306,13 +3392,15 @@ dependencies = [ name = "nymcoconut" version = "0.5.0" dependencies = [ - "bls12_381", + "bls12_381 0.6.0", "bs58", "digest 0.9.0", - "ff 0.10.1", + "dkg", + "ff 0.11.1", "getrandom 0.2.7", - "group 0.10.0", + "group 0.11.0", "itertools", + "pemstore", "rand 0.8.5", "serde", "serde_derive", @@ -3570,6 +3658,15 @@ dependencies = [ "group 0.10.0", ] +[[package]] +name = "pairing" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" +dependencies = [ + "group 0.11.0", +] + [[package]] name = "pango" version = "0.15.10" @@ -4085,6 +4182,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -6772,6 +6875,15 @@ dependencies = [ "windows-implement", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.20.0" @@ -6795,9 +6907,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -6821,9 +6933,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 895eab810c..999378447f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -256,6 +256,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.5" @@ -319,16 +331,16 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bls12_381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" dependencies = [ "digest 0.9.0", - "ff 0.10.1", - "group 0.10.0", + "ff", + "group", "pairing", "rand_core 0.6.3", "subtle", + "zeroize", ] [[package]] @@ -601,6 +613,8 @@ version = "0.1.0" dependencies = [ "contracts-common", "cosmwasm-std", + "cw-utils", + "multisig-contract-common", "schemars", "serde", ] @@ -965,9 +979,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", @@ -976,10 +990,22 @@ dependencies = [ ] [[package]] -name = "cw3" -version = "0.13.2" +name = "cw2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f871854338a54c7bb094d16ffe17212b93b146d9659dbce4c9402a9b77e240ef" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" dependencies = [ "cosmwasm-std", "cw-utils", @@ -988,10 +1014,26 @@ dependencies = [ ] [[package]] -name = "cw4" -version = "0.13.2" +name = "cw3-fixed-multisig" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4476d6a7c13c46ed9ff260bd0e1cf648dc37b13f483822e1ff2a431f0f6ee52" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1142,6 +1184,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec", + "bls12_381", + "bs58", + "ff", + "group", + "lazy_static", + "pemstore", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1232,9 +1295,9 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "ff 0.11.0", + "ff", "generic-array 0.14.5", - "group 0.11.0", + "group", "rand_core 0.6.3", "sec1", "subtle", @@ -1345,16 +1408,6 @@ dependencies = [ "log", ] -[[package]] -name = "ff" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" -dependencies = [ - "rand_core 0.6.3", - "subtle", -] - [[package]] name = "ff" version = "0.11.0" @@ -1452,6 +1505,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1843,25 +1902,14 @@ dependencies = [ "system-deps 6.0.2", ] -[[package]] -name = "group" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" -dependencies = [ - "byteorder", - "ff 0.10.1", - "rand_core 0.6.3", - "subtle", -] - [[package]] name = "group" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ - "ff 0.11.0", + "byteorder", + "ff", "rand_core 0.6.3", "subtle", ] @@ -2657,6 +2705,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -2944,10 +2993,12 @@ dependencies = [ "bls12_381", "bs58", "digest 0.9.0", - "ff 0.10.1", + "dkg", + "ff", "getrandom 0.2.5", - "group 0.10.0", + "group", "itertools", + "pemstore", "rand 0.8.5", "serde", "serde_derive", @@ -3063,11 +3114,11 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" [[package]] name = "pairing" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" dependencies = [ - "group 0.10.0", + "group", ] [[package]] @@ -3219,6 +3270,24 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64", + "once_cell", + "regex", +] + +[[package]] +name = "pemstore" +version = "0.1.0" +dependencies = [ + "pem", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3590,6 +3659,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -4648,6 +4723,12 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.38" @@ -5899,6 +5980,15 @@ dependencies = [ "windows-implement", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.19.1" From fb31dbee160dfc73c6ecaf73155540d42ec593c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 23 Nov 2022 21:00:48 +0100 Subject: [PATCH 60/75] client-core: add warning when delay multiplier is larger than 1 --- .../src/client/real_messages_control/real_traffic_stream.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 87368536a5..bc5535d6b1 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -482,6 +482,12 @@ where } else { log::debug!("{status_str}"); } + if self.sending_delay_controller.current_multiplier() > 1 { + log::warn!( + "Unable to send packets fast enough - sending delay multiplier set to: {}", + self.sending_delay_controller.current_multiplier() + ); + } } #[cfg(not(target_arch = "wasm32"))] From 358687f43a6ffbd17a68b127bded26d332bd9762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 10:26:09 +0100 Subject: [PATCH 61/75] Fix decrypting stored received msg (#1786) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix decrypting stored received msg * rustfmt * Moving binary message recovery to separate function Co-authored-by: Jędrzej Stuczyński --- .../client-libs/gateway-client/src/client.rs | 29 ++++++++++++------- common/client-libs/gateway-client/src/lib.rs | 21 ++++++++++++++ .../gateway-client/src/socket_state.rs | 21 +++----------- 3 files changed, 44 insertions(+), 27 deletions(-) diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 9e17f55ff8..bac92ec092 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -2,19 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bandwidth::BandwidthController; -use crate::cleanup_socket_message; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; use crate::socket_state::{PartiallyDelegated, SocketState}; -#[cfg(target_arch = "wasm32")] -use crate::wasm_storage::PersistentStorage; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(not(target_arch = "wasm32"))] -use credential_storage::PersistentStorage; +use crate::{cleanup_socket_message, try_decrypt_binary_message}; use crypto::asymmetric::identity; use futures::{FutureExt, SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; @@ -28,13 +22,20 @@ use rand::rngs::OsRng; use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; -#[cfg(not(target_arch = "wasm32"))] -use task::ShutdownListener; use tungstenite::protocol::Message; +#[cfg(feature = "coconut")] +use coconut_interface::Credential; + +#[cfg(not(target_arch = "wasm32"))] +use credential_storage::PersistentStorage; +#[cfg(not(target_arch = "wasm32"))] +use task::ShutdownListener; #[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::connect_async; +#[cfg(target_arch = "wasm32")] +use crate::wasm_storage::PersistentStorage; #[cfg(target_arch = "wasm32")] use wasm_timer; #[cfg(target_arch = "wasm32")] @@ -336,7 +337,15 @@ impl GatewayClient { }; match ws_msg { Message::Binary(bin_msg) => { - if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { + // if we have established the shared key already, attempt to use it for decryption + // otherwise there's not much we can do apart from just routing what we have on hand + if let Some(shared_keys) = &self.shared_key { + if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_keys) { + if let Err(err) = self.packet_router.route_received(vec![plaintext]) { + log::warn!("Route received failed: {:?}", err); + } + } + } else if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { log::warn!("Route received failed: {:?}", err); } } diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 1becf01b4a..8671a7363b 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -3,6 +3,9 @@ use crate::error::GatewayClientError; pub use client::GatewayClient; +use gateway_requests::registration::handshake::SharedKeys; +use gateway_requests::BinaryResponse; +use log::warn; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; @@ -37,3 +40,21 @@ pub(crate) fn cleanup_socket_messages( None => Err(GatewayClientError::ConnectionAbruptlyClosed), } } + +pub(crate) fn try_decrypt_binary_message( + bin_msg: Vec, + shared_keys: &SharedKeys, +) -> Option> { + match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) { + Ok(bin_response) => match bin_response { + BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext), + }, + Err(err) => { + warn!( + "message received from the gateway was malformed! - {:?}", + err + ); + None + } + } +} diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index df1b95c0ae..2d66e782e8 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -1,14 +1,13 @@ // Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cleanup_socket_messages; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; +use crate::{cleanup_socket_messages, try_decrypt_binary_message}; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use gateway_requests::registration::handshake::SharedKeys; -use gateway_requests::BinaryResponse; use log::*; use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] @@ -50,21 +49,9 @@ impl PartiallyDelegated { match ws_msg { Message::Binary(bin_msg) => { // this function decrypts the request and checks the MAC - let plaintext = match BinaryResponse::try_from_encrypted_tagged_bytes( - bin_msg, shared_key, - ) { - Ok(bin_response) => match bin_response { - BinaryResponse::PushedMixMessage(plaintext) => plaintext, - }, - Err(err) => { - warn!( - "message received from the gateway was malformed! - {:?}", - err - ); - continue; - } - }; - plaintexts.push(plaintext) + if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_key) { + plaintexts.push(plaintext) + } } // I think that in the future we should perhaps have some sequence number system, i.e. // so each request/response pair can be easily identified, so that if messages are From 0be6fe507914b6a0d69d2d1524e46d03154b572a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 24 Nov 2022 17:02:31 +0000 Subject: [PATCH 62/75] Feature/use expect instead of panicking (#1797) * Implementation of 'Debug' on 'RealMessage' * expect with failed channel name instead of throwing empty panics * Introduced Debug trait constraint in ProxyRunner * Derive Debug for socks5_requests::Message --- .../input_message_listener.rs | 8 ++------ .../retransmission_request_listener.rs | 8 ++------ .../real_traffic_stream.rs | 1 + clients/native/src/client/mod.rs | 16 ++++------------ clients/native/src/websocket/handler.rs | 14 ++++++++------ clients/socks5/src/socks/client.rs | 7 ++++--- clients/webassembly/src/client/mod.rs | 8 ++------ common/nymsphinx/forwarding/src/packet.rs | 15 ++++++++++++++- .../proxy-helpers/src/proxy_runner/inbound.rs | 18 ++++++++---------- .../proxy-helpers/src/proxy_runner/mod.rs | 3 ++- common/socks5/requests/src/msg.rs | 1 + .../network-requester/src/core.rs | 14 ++++---------- .../src/statistics/collector.rs | 16 ++++------------ 13 files changed, 56 insertions(+), 73 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 58fa65af56..0738d0aa80 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -187,14 +187,10 @@ where // there's no point in trying to send nothing if let Some(real_messages) = real_messages { // tells real message sender (with the poisson timer) to send this to the mix network - if self - .real_message_sender + self.real_message_sender .send((real_messages, lane)) .await - .is_err() - { - panic!(); - } + .expect("BatchRealMessageReceiver has stopped receiving!"); } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 2fbab44b61..4b0bc3cc99 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -116,17 +116,13 @@ where .unwrap(); // send to `OutQueueControl` to eventually send to the mix network - if self - .real_message_sender + self.real_message_sender .send(( vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], TransmissionLane::Retransmission, )) .await - .is_err() - { - panic!(); - } + .expect("BatchRealMessageReceiver has stopped receiving!"); } #[cfg(not(target_arch = "wasm32"))] diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index bc5535d6b1..503203922c 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -140,6 +140,7 @@ where lane_queue_lengths: LaneQueueLengths, } +#[derive(Debug)] pub(crate) struct RealMessage { mix_packet: MixPacket, fragment_id: FragmentIdentifier, diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 5e16cf0a0f..4d60e3ad54 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -327,16 +327,12 @@ impl NymClient { let lane = TransmissionLane::General; let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); - if self - .input_tx + self.input_tx .as_ref() .expect("start method was not called before!") .send(input_msg) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); } /// EXPERIMENTAL DIRECT RUST API @@ -345,16 +341,12 @@ impl NymClient { pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { let input_msg = InputMessage::new_reply(reply_surb, message); - if self - .input_tx + self.input_tx .as_ref() .expect("start method was not called before!") .send(input_msg) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); } /// EXPERIMENTAL DIRECT RUST API diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 53e6a4d491..0daa171532 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -97,9 +97,10 @@ impl Handler { // the ack control is now responsible for chunking, etc. let lane = TransmissionLane::ConnectionId(connection_id); let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); - if self.msg_input.send(input_msg).await.is_err() { - panic!(); - } + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); // on receiving a send, we reply back the current lane queue length for that connection id. // Note that this does _NOT_ take into account the packets that have been received but not @@ -123,9 +124,10 @@ impl Handler { } let input_msg = InputMessage::new_reply(reply_surb, message); - if self.msg_input.send(input_msg).await.is_err() { - panic!(); - } + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); None } diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index e77dba2975..df73d9ee7f 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -240,9 +240,10 @@ impl SocksClient { false, TransmissionLane::ConnectionId(self.connection_id), ); - if self.input_sender.send(input_message).await.is_err() { - panic!(); - } + self.input_sender + .send(input_message) + .await + .expect("InputMessageReceiver has stopped receiving!"); } async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) { diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 608674a169..c64dbe69e0 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -395,16 +395,12 @@ impl NymClient { let input_msg = InputMessage::new_fresh(recipient, message, false, lane); - if self - .input_tx + self.input_tx .as_ref() .expect("start method was not called before!") .send(input_msg) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); self } diff --git a/common/nymsphinx/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 62df1028e3..8c22b43f5c 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -5,7 +5,7 @@ use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressEr use nymsphinx_params::{PacketMode, PacketSize}; use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; #[derive(Debug)] pub enum MixPacketFormattingError { @@ -50,6 +50,19 @@ pub struct MixPacket { packet_mode: PacketMode, } +impl Debug for MixPacket { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "MixPacket to {:?} with packet_mode {:?}. Sphinx header: {:?}, payload length: {}", + self.next_hop, + self.packet_mode, + self.sphinx_packet.header, + self.sphinx_packet.payload.len() + ) + } +} + impl MixPacket { pub fn new( next_hop: NymNodeRoutingAddress, diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index bb5cf98f37..12caff2071 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -12,6 +12,7 @@ use futures::StreamExt; use log::*; use ordered_buffer::OrderedMessageSender; use socks5_requests::ConnectionId; +use std::fmt::Debug; use std::time::Duration; use std::{io, sync::Arc}; use task::ShutdownListener; @@ -25,15 +26,13 @@ async fn send_empty_close( adapter_fn: F, ) where F: Fn(ConnectionId, Vec, bool) -> S, + S: Debug, { let ordered_msg = message_sender.wrap_message(Vec::new()).into_bytes(); - if mix_sender + mix_sender .send(adapter_fn(connection_id, ordered_msg, true)) .await - .is_err() - { - panic!(); - } + .expect("BatchRealMessageReceiver has stopped receiving!"); } #[allow(clippy::too_many_arguments)] @@ -49,6 +48,7 @@ async fn deal_with_data( ) -> bool where F: Fn(ConnectionId, Vec, bool) -> S, + S: Debug, { let (read_data, is_finished) = match read_data { Some(data) => match data { @@ -89,13 +89,10 @@ where } } - if mix_sender + mix_sender .send(adapter_fn(connection_id, ordered_msg, is_finished)) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); if is_finished { // technically we already informed it when we sent the message to mixnet above @@ -168,6 +165,7 @@ pub(super) async fn run_inbound( ) -> OwnedReadHalf where F: Fn(ConnectionId, Vec, bool) -> S + Send + 'static, + S: Debug, { let mut available_reader = AvailableReader::new(&mut reader); let mut message_sender = OrderedMessageSender::new(); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index 573f09f884..fbd8cd21b3 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -4,6 +4,7 @@ use crate::connection_controller::ConnectionReceiver; use client_connections::LaneQueueLengths; use socks5_requests::ConnectionId; +use std::fmt::Debug; use std::{sync::Arc, time::Duration}; use task::ShutdownListener; use tokio::{net::TcpStream, sync::Notify}; @@ -54,7 +55,7 @@ pub struct ProxyRunner { impl ProxyRunner where - S: Send + 'static, + S: Debug + Send + 'static, { #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/common/socks5/requests/src/msg.rs b/common/socks5/requests/src/msg.rs index b13f346f35..2b688d8214 100644 --- a/common/socks5/requests/src/msg.rs +++ b/common/socks5/requests/src/msg.rs @@ -25,6 +25,7 @@ pub enum MessageError { UnknownMessageType, } +#[derive(Debug)] pub enum Message { Request(Request), Response(Response), diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 6bea582581..80528d1bed 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -212,16 +212,13 @@ impl ServiceProvider { ); // inform the remote that the connection is closed before it even was established - if mix_input_sender + mix_input_sender .send(( Socks5Message::Response(Response::new(conn_id, Vec::new(), true)), return_address, )) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); return; } @@ -271,7 +268,7 @@ impl ServiceProvider { if !self.open_proxy && !self.outbound_request_filter.check(&remote_addr) { let log_msg = format!("Domain {:?} failed filter check", remote_addr); log::info!("{}", log_msg); - if mix_input_sender + mix_input_sender .send(( Socks5Message::NetworkRequesterResponse(NetworkRequesterResponse::new( conn_id, log_msg, @@ -279,10 +276,7 @@ impl ServiceProvider { return_address, )) .await - .is_err() - { - panic!(); - } + .expect("InputMessageReceiver has stopped receiving!"); return; } diff --git a/service-providers/network-requester/src/statistics/collector.rs b/service-providers/network-requester/src/statistics/collector.rs index 72fc276860..ac43252c33 100644 --- a/service-providers/network-requester/src/statistics/collector.rs +++ b/service-providers/network-requester/src/statistics/collector.rs @@ -175,30 +175,22 @@ impl StatisticsCollector for ServiceStatisticsCollector { ), self.stats_provider_addr, ); - if self - .mix_input_sender + self.mix_input_sender .send(( Socks5Message::Request(connect_req), self.stats_provider_addr, )) .await - .is_err() - { - panic!(); - } + .expect("MixProxyReader has stopped receiving!"); trace!("Sending data to statistics service"); let mut message_sender = OrderedMessageSender::new(); let ordered_msg = message_sender.wrap_message(msg).into_bytes(); let send_req = Request::new_send(conn_id, ordered_msg, true); - if self - .mix_input_sender + self.mix_input_sender .send((Socks5Message::Request(send_req), self.stats_provider_addr)) .await - .is_err() - { - panic!(); - } + .expect("MixProxyReader has stopped receiving!"); Ok(()) } From 17f89aecd57a21834f5960501ef2735d4f2d5c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 23:13:51 +0100 Subject: [PATCH 63/75] Make connection_id optional in ClientRequest::Send (#1798) --- .../native/examples/websocket_binarysend.rs | 4 +- clients/native/src/websocket/handler.rs | 40 ++++++++++++++----- .../native/websocket-requests/src/requests.rs | 19 +++++---- clients/native/websocket-requests/src/text.rs | 2 +- .../network-requester/src/core.rs | 2 +- 5 files changed, 47 insertions(+), 20 deletions(-) diff --git a/clients/native/examples/websocket_binarysend.rs b/clients/native/examples/websocket_binarysend.rs index 160a9bf009..f103815af2 100644 --- a/clients/native/examples/websocket_binarysend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -43,7 +43,7 @@ async fn send_file_with_reply() { recipient, message: read_data, with_reply_surb: true, - connection_id: 0, + connection_id: Some(0), }; println!("sending content of 'dummy_file' over the mix network..."); @@ -92,7 +92,7 @@ async fn send_file_without_reply() { recipient, message: read_data, with_reply_surb: false, - connection_id: 0, + connection_id: Some(0), }; println!("sending content of 'dummy_file' over the mix network..."); diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 0daa171532..11d0dda55f 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -92,26 +92,42 @@ impl Handler { recipient: &Recipient, message: Vec, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, ) -> Option { + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); + // the ack control is now responsible for chunking, etc. - let lane = TransmissionLane::ConnectionId(connection_id); let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); self.msg_input .send(input_msg) .await .expect("InputMessageReceiver has stopped receiving!"); + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::Reply + | TransmissionLane::Retransmission + | TransmissionLane::Control => return None, + TransmissionLane::ConnectionId(id) => id, + }; + // on receiving a send, we reply back the current lane queue length for that connection id. // Note that this does _NOT_ take into account the packets that have been received but not // yet reach `OutQueueControl`, so it might be a tad low. - if let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() { - let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); - return Some(ServerResponse::LaneQueueLength(connection_id, queue_length)); - } + let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { + log::warn!( + "Failed to get the lane queue length lock, \ + not responding back with the current queue length" + ); + return None; + }; - log::warn!("Failed to get the lane queue length lock, not responding back with the current queue length"); - None + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) } async fn handle_reply( @@ -120,7 +136,13 @@ impl Handler { message: Vec, ) -> Option { if message.len() > ReplySurb::max_msg_len(Default::default()) { - return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySurb::max_msg_len(Default::default())))); + return Some( + ServerResponse::new_error( + format!( + "too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", + message.len(), ReplySurb::max_msg_len(Default::default())) + ) + ); } let input_msg = InputMessage::new_reply(reply_surb, message); diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index 9b5af6e70d..fafefb5827 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -34,7 +34,7 @@ pub enum ClientRequest { message: Vec, // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? with_reply_surb: bool, - connection_id: u64, + connection_id: Option, }, Reply { message: Vec, @@ -53,10 +53,10 @@ impl ClientRequest { recipient: Recipient, data: Vec, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, ) -> Vec { let data_len_bytes = (data.len() as u64).to_be_bytes(); - let conn_id_bytes = connection_id.to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); std::iter::once(SEND_REQUEST_TAG) .chain(std::iter::once(with_reply_surb as u8)) .chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant @@ -106,6 +106,11 @@ impl ClientRequest { connection_id_bytes .copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) + }; let data_len_bytes = &b[2 + Recipient::LEN + size_of::()..2 + Recipient::LEN + 2 * size_of::()]; @@ -350,7 +355,7 @@ mod tests { recipient, message: b"foomp".to_vec(), with_reply_surb: false, - connection_id: 42, + connection_id: Some(42), }; let bytes = send_request_no_surb.serialize(); @@ -365,7 +370,7 @@ mod tests { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); assert!(!with_reply_surb); - assert_eq!(connection_id, 42) + assert_eq!(connection_id, Some(42)) } _ => unreachable!(), } @@ -374,7 +379,7 @@ mod tests { recipient, message: b"foomp".to_vec(), with_reply_surb: true, - connection_id: 213, + connection_id: None, }; let bytes = send_request_surb.serialize(); @@ -389,7 +394,7 @@ mod tests { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); assert!(with_reply_surb); - assert_eq!(connection_id, 213) + assert_eq!(connection_id, None) } _ => unreachable!(), } diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs index cca6569f4f..1083ecd9a8 100644 --- a/clients/native/websocket-requests/src/text.rs +++ b/clients/native/websocket-requests/src/text.rs @@ -20,7 +20,7 @@ pub(super) enum ClientRequestText { message: String, recipient: String, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, }, SelfAddress, #[serde(rename_all = "camelCase")] diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index 80528d1bed..870376d896 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -102,7 +102,7 @@ impl ServiceProvider { recipient: return_address, message: msg.into_bytes(), with_reply_surb: false, - connection_id: conn_id, + connection_id: Some(conn_id), }; let message = Message::Binary(response_message.serialize()); From cb7e57b5f8efbe92283e411b752c8ee1eea791c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 23:29:37 +0100 Subject: [PATCH 64/75] changelog: add missing entry for fixing message decrypt in gateway-client --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de700926d1..5d6d57cd6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,11 +18,16 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747]) - socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) +### Fixed + +- gateway-client: fix decrypting stored messages on reconnect ([#1786]) + [#1678]: https://github.com/nymtech/nym/pull/1678 [#1708]: https://github.com/nymtech/nym/pull/1708 [#1720]: https://github.com/nymtech/nym/pull/1720 [#1747]: https://github.com/nymtech/nym/pull/1747 [#1783]: https://github.com/nymtech/nym/pull/1783 +[#1786]: https://github.com/nymtech/nym/pull/1786 ## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09) From e2ba85c9bf4a5cacdc9190f734f3531c27444efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 24 Nov 2022 23:45:25 +0100 Subject: [PATCH 65/75] websocket-requests: fix length check before deserialize (#1799) --- .../native/websocket-requests/src/requests.rs | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index fafefb5827..315399ae3d 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -239,7 +239,14 @@ impl ClientRequest { } // CLOSED_CONNECTION_REQUEST_TAG - fn deserialize_closed_connection(b: &[u8]) -> Self { + fn deserialize_closed_connection(b: &[u8]) -> Result { + if b.len() != 1 + size_of::() { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + "the received closed connection has invalid length".to_string(), + )); + } + // this MUST match because it was called by 'deserialize' debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG); @@ -247,7 +254,7 @@ impl ClientRequest { connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); - ClientRequest::ClosedConnection(connection_id) + Ok(ClientRequest::ClosedConnection(connection_id)) } // GET_LANE_QUEUE_LENGHT_TAG @@ -259,7 +266,14 @@ impl ClientRequest { } // GET_LANE_QUEUE_LENGHT_TAG - fn deserialize_get_lane_queue_length(b: &[u8]) -> Self { + fn deserialize_get_lane_queue_length(b: &[u8]) -> Result { + if b.len() != 1 + size_of::() { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + "the received get lane queue length has invalid length".to_string(), + )); + } + // this MUST match because it was called by 'deserialize' debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); @@ -267,7 +281,7 @@ impl ClientRequest { connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); - ClientRequest::GetLaneQueueLength(connection_id) + Ok(ClientRequest::GetLaneQueueLength(connection_id)) } pub fn serialize(self) -> Vec { @@ -318,8 +332,8 @@ impl ClientRequest { SEND_REQUEST_TAG => Self::deserialize_send(b), REPLY_REQUEST_TAG => Self::deserialize_reply(b), SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), - CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)), - GET_LANE_QUEUE_LENGHT_TAG => Ok(Self::deserialize_get_lane_queue_length(b)), + CLOSED_CONNECTION_REQUEST_TAG => Self::deserialize_closed_connection(b), + GET_LANE_QUEUE_LENGHT_TAG => Self::deserialize_get_lane_queue_length(b), n => Err(error::Error::new( ErrorKind::UnknownRequest, format!("type {n}"), From bff079a3f8d4d7d703fdca707d20ff81ed8ef2d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Fri, 25 Nov 2022 14:18:07 +0200 Subject: [PATCH 66/75] Fix export dkg contract addr (#1800) * Export dkg contract for mainnet when no config file present * Remove redundant env files --- common/network-defaults/envs/mainnet.env | 20 -------------------- common/network-defaults/envs/qa.env | 20 -------------------- common/network-defaults/src/mainnet.rs | 8 ++++++++ 3 files changed, 8 insertions(+), 40 deletions(-) delete mode 100644 common/network-defaults/envs/mainnet.env delete mode 100644 common/network-defaults/envs/qa.env diff --git a/common/network-defaults/envs/mainnet.env b/common/network-defaults/envs/mainnet.env deleted file mode 100644 index dce1c3c9c0..0000000000 --- a/common/network-defaults/envs/mainnet.env +++ /dev/null @@ -1,20 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 - -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g -VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy -STATISTICS_SERVICE_DOMAIN_ADDRESS="http://127.0.0.1:8090" -NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" -API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/common/network-defaults/envs/qa.env b/common/network-defaults/envs/qa.env deleted file mode 100644 index 842fb54c4d..0000000000 --- a/common/network-defaults/envs/qa.env +++ /dev/null @@ -1,20 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 - -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep -VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw -MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs -REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq -STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -NYMD_VALIDATOR="https://qa-validator.nymtech.net" -API_VALIDATOR="https://qa-validator-api.nymtech.net/api" diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index c2e16dee00..59242376f9 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -85,6 +85,10 @@ pub fn export_to_env() { var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); + set_var_to_default( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + COCONUT_DKG_CONTRACT_ADDRESS, + ); set_var_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, @@ -125,6 +129,10 @@ pub fn export_to_env_if_not_set() { var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); + set_var_conditionally_to_default( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + COCONUT_DKG_CONTRACT_ADDRESS, + ); set_var_conditionally_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, From df90ff86581e4512af6fc7f468376dc7609cd9dd Mon Sep 17 00:00:00 2001 From: Mark Sinclair Date: Fri, 25 Nov 2022 13:15:21 +0000 Subject: [PATCH 67/75] nym-cli: improve error reporting/handling and changed `vesting-schedule` queries to use query client instead of signing client --- CHANGELOG.md | 1 + Cargo.lock | 2 ++ common/commands/Cargo.toml | 1 + common/commands/src/context/errors.rs | 6 ++++++ common/commands/src/context/mod.rs | 9 +++++---- .../commands/src/validator/vesting/balance.rs | 16 ++++++++++++---- .../vesting/query_vesting_schedule.rs | 18 ++++++++++++++---- tools/nym-cli/Cargo.toml | 1 + tools/nym-cli/src/validator/vesting.rs | 10 +++++++--- 9 files changed, 49 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6d57cd6b..ef9c535d9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - clients,validator-api: take coconut signers from the chain instead of specifying them via CLI ([#1747]) - multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747]) - socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) +- nym-cli: improve error reporting/handling and changed `vesting-schedule` queries to use query client instead of signing client ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 4303004d95..3c177de6a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3139,6 +3139,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", + "tap", "tokio", "validator-client", ] @@ -3164,6 +3165,7 @@ dependencies = [ "rand 0.6.5", "serde", "serde_json", + "tap", "thiserror", "time 0.3.14", "toml", diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 35b38512fa..ce0195cce4 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -22,6 +22,7 @@ thiserror = "1" time = { version = "0.3.6", features = ["parsing", "formatting"] } toml = "0.5.6" url = "2.2" +tap = "1" cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } cosmwasm-std = { version = "1.0.0" } diff --git a/common/commands/src/context/errors.rs b/common/commands/src/context/errors.rs index 5c3d24a2bb..44a99f49ed 100644 --- a/common/commands/src/context/errors.rs +++ b/common/commands/src/context/errors.rs @@ -15,4 +15,10 @@ pub enum ContextError { // TODO: improve this to return known errors #[error("failed to create client - {0}")] NymdError(String), + + #[error("{0}")] + NymdErrorPassthrough(#[from] validator_client::nymd::error::NymdError), + + #[error("{0}")] + ValidatorClientError(#[from] validator_client::ValidatorClientError), } diff --git a/common/commands/src/context/mod.rs b/common/commands/src/context/mod.rs index 856a03e62a..4f55aa1847 100644 --- a/common/commands/src/context/mod.rs +++ b/common/commands/src/context/mod.rs @@ -6,6 +6,7 @@ use network_defaults::{ var_names::{API_VALIDATOR, MIXNET_CONTRACT_ADDRESS, NYMD_VALIDATOR, VESTING_CONTRACT_ADDRESS}, NymNetworkDetails, }; +use tap::prelude::*; use validator_client::nymd::{self, AccountId, NymdClient, QueryNymdClient, SigningNymdClient}; pub use validator_client::validator_api::Client as ValidatorApiClient; @@ -58,7 +59,7 @@ pub fn create_signing_client( network_details: &NymNetworkDetails, ) -> Result { let client_config = nymd::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; // get mnemonic let mnemonic = match std::env::var("MNEMONIC") { @@ -87,7 +88,7 @@ pub fn create_query_client( network_details: &NymNetworkDetails, ) -> Result { let client_config = nymd::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; let nymd_url = network_details .endpoints @@ -107,7 +108,7 @@ pub fn create_signing_client_with_validator_api( network_details: &NymNetworkDetails, ) -> Result { let client_config = validator_client::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; // get mnemonic let mnemonic = match std::env::var("MNEMONIC") { @@ -129,7 +130,7 @@ pub fn create_query_client_with_validator_api( network_details: &NymNetworkDetails, ) -> Result { let client_config = validator_client::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; match validator_client::client::Client::new_query(client_config) { Ok(client) => Ok(client), diff --git a/common/commands/src/validator/vesting/balance.rs b/common/commands/src/validator/vesting/balance.rs index 45b2f7866d..0dff560b9f 100644 --- a/common/commands/src/validator/vesting/balance.rs +++ b/common/commands/src/validator/vesting/balance.rs @@ -3,11 +3,11 @@ use clap::Parser; use cosmrs::AccountId; -use log::info; +use log::{error, info}; use validator_client::nymd::{Coin, VestingQueryClient}; -use crate::context::SigningClient; +use crate::context::QueryClient; use crate::utils::show_error; use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; @@ -18,8 +18,16 @@ pub struct Args { pub address: Option, } -pub async fn balance(args: Args, client: SigningClient) { - let account_id = args.address.unwrap_or_else(|| client.address().clone()); +pub async fn balance(args: Args, client: QueryClient, address_from_mnemonic: Option) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let account_id = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/common/commands/src/validator/vesting/query_vesting_schedule.rs b/common/commands/src/validator/vesting/query_vesting_schedule.rs index 23118c1bf5..10a4dc41d3 100644 --- a/common/commands/src/validator/vesting/query_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/query_vesting_schedule.rs @@ -4,11 +4,11 @@ use clap::Parser; use cosmrs::AccountId; use cosmwasm_std::Coin as CosmWasmCoin; -use log::info; +use log::{error, info}; use validator_client::nymd::{Coin, VestingQueryClient}; -use crate::context::SigningClient; +use crate::context::QueryClient; use crate::utils::show_error; use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; @@ -19,8 +19,18 @@ pub struct Args { pub address: Option, } -pub async fn query(args: Args, client: SigningClient) { - let account_id = args.address.unwrap_or_else(|| client.address().clone()); +pub async fn query(args: Args, client: QueryClient, address_from_mnemonic: Option) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let account_id = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + + info!("Checking account {} for a vesting schedule...", account_id); + let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 2ec5af6404..67c3bda09a 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -18,6 +18,7 @@ serde_json = "1" tokio = { version = "1.11", features = [ "net", "rt-multi-thread", "macros", "signal"] } bip39 = "1.0.1" anyhow = "1" +tap = "1" nym-cli-commands = { path = "../../common/commands" } logging = { path = "../../common/logging"} diff --git a/tools/nym-cli/src/validator/vesting.rs b/tools/nym-cli/src/validator/vesting.rs index d87ca2c9ff..202011bc3c 100644 --- a/tools/nym-cli/src/validator/vesting.rs +++ b/tools/nym-cli/src/validator/vesting.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use network_defaults::NymNetworkDetails; -use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; pub(crate) async fn execute( global_args: ClientArgs, @@ -19,18 +19,22 @@ pub(crate) async fn execute( .await } Some(nym_cli_commands::validator::vesting::VestingScheduleCommands::Query(args)) => { + let address_from_args = args.address.clone(); nym_cli_commands::validator::vesting::query_vesting_schedule::query( args, - create_signing_client(global_args, network_details)?, + create_query_client(network_details)?, + address_from_args, ) .await } Some(nym_cli_commands::validator::vesting::VestingScheduleCommands::VestedBalance( args, )) => { + let address_from_args = args.address.clone(); nym_cli_commands::validator::vesting::balance::balance( args, - create_signing_client(global_args, network_details)?, + create_query_client(network_details)?, + address_from_args, ) .await } From 186896bb3711fb246025a38f17714b37ce514fbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 25 Nov 2022 13:29:42 +0000 Subject: [PATCH 68/75] Feature/gateway client protocol version (#1795) * Introducing concept of gateway protocol version * Remove version-based gateway filtering * Fixed the unit test * grammar --- .../client-libs/gateway-client/src/client.rs | 50 ++++++++++++++-- .../client-libs/gateway-client/src/error.rs | 3 + common/topology/src/lib.rs | 10 +--- gateway/gateway-requests/src/lib.rs | 6 +- .../src/registration/handshake/state.rs | 5 +- gateway/gateway-requests/src/types.rs | 55 ++++++++++++++--- .../websocket/connection_handler/fresh.rs | 60 ++++++++++++++++--- 7 files changed, 158 insertions(+), 31 deletions(-) diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index bac92ec092..e88424b747 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -14,7 +14,7 @@ use futures::{FutureExt, SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use gateway_requests::iv::IV; use gateway_requests::registration::handshake::{client_handshake, SharedKeys}; -use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; +use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse, PROTOCOL_VERSION}; use log::*; use network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; use nymsphinx::forwarding::packet::MixPacket; @@ -445,6 +445,33 @@ impl GatewayClient { } } + fn check_gateway_protocol( + &self, + gateway_protocol: Option, + ) -> Result<(), GatewayClientError> { + // right now there are no failure cases here, but this might change in the future + match gateway_protocol { + None => { + warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); + // note: in 1.2.0 we will have to return a hard error here + Ok(()) + } + Some(v) if v != PROTOCOL_VERSION => { + let err = GatewayClientError::IncompatibleProtocol { + gateway: Some(v), + current: PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } + + Some(_) => { + info!("the gateway is using exactly the same protocol version as we are. We're good to continue!"); + Ok(()) + } + } + } + async fn register(&mut self) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -467,11 +494,20 @@ impl GatewayClient { .map_err(GatewayClientError::RegistrationFailure), _ => unreachable!(), }?; - self.authenticated = match self.read_control_response().await? { - ServerResponse::Register { status } => Ok(status), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), - }?; + let (authentication_status, gateway_protocol) = match self.read_control_response().await? { + ServerResponse::Register { + protocol_version, + status, + } => (status, protocol_version), + ServerResponse::Error { message } => { + return Err(GatewayClientError::GatewayError(message)) + } + _ => return Err(GatewayClientError::UnexpectedResponse), + }; + + self.check_gateway_protocol(gateway_protocol)?; + self.authenticated = authentication_status; + if self.authenticated { self.shared_key = Some(Arc::new(shared_key)); } @@ -510,9 +546,11 @@ impl GatewayClient { match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { + protocol_version, status, bandwidth_remaining, } => { + self.check_gateway_protocol(protocol_version)?; self.authenticated = status; self.bandwidth_remaining = bandwidth_remaining; Ok(()) diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index bb6f71f564..8a8104658b 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -85,6 +85,9 @@ pub enum GatewayClientError { #[error("Failed to send mixnet message")] MixnetMsgSenderFailedToSend, + + #[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")] + IncompatibleProtocol { gateway: Option, current: u8 }, } impl GatewayClientError { diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index ed6b7c71fb..6ef41c9aad 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -209,18 +209,14 @@ impl NymTopology { #[must_use] pub fn filter_system_version(&self, expected_version: &str) -> Self { - self.filter_node_versions(expected_version, expected_version) + self.filter_node_versions(expected_version) } #[must_use] - pub fn filter_node_versions( - &self, - expected_mix_version: &str, - expected_gateway_version: &str, - ) -> Self { + pub fn filter_node_versions(&self, expected_mix_version: &str) -> Self { NymTopology { mixes: self.mixes.filter_by_version(expected_mix_version), - gateways: self.gateways.filter_by_version(expected_gateway_version), + gateways: self.gateways.clone(), } } } diff --git a/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs index 98de45c377..d1c1861a8a 100644 --- a/gateway/gateway-requests/src/lib.rs +++ b/gateway/gateway-requests/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020 - Nym Technologies SA +// Copyright 2020-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 pub use crypto::generic_array; @@ -12,6 +12,10 @@ pub mod iv; pub mod registration; pub mod types; +/// Defines the current version of the communication protocol between gateway and clients. +/// It has to be incremented for any breaking change. +pub const PROTOCOL_VERSION: u8 = 1; + pub type GatewayMac = HmacOutput; // TODO: could using `Mac` trait here for OutputSize backfire? diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 10c0ae9729..1560b5fd9f 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -210,7 +210,10 @@ impl<'a, S> State<'a, S> { match msg { WsMessage::Text(ws_msg) => match types::RegistrationHandshake::try_from(ws_msg) { Ok(reg_handshake_msg) => return match reg_handshake_msg { - types::RegistrationHandshake::HandshakePayload { data } => Ok(data), + // hehe, that's a bit disgusting that the type system requires we explicitly ignore the + // protocol_version field that we actually never attach at this point + // yet another reason for the overdue refactor + types::RegistrationHandshake::HandshakePayload { data, .. } => Ok(data), types::RegistrationHandshake::HandshakeError { message } => Err(HandshakeError::RemoteError(message)), }, Err(_) => error!("Received a non-handshake message during the registration handshake! It's getting dropped."), diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index bfa20a9273..f73f85bca5 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -4,7 +4,7 @@ use crate::authentication::encrypted_address::EncryptedAddressBytes; use crate::iv::IV; use crate::registration::handshake::SharedKeys; -use crate::GatewayMacSize; +use crate::{GatewayMacSize, PROTOCOL_VERSION}; use crypto::generic_array::typenum::Unsigned; use crypto::hmac::recompute_keyed_hmac_and_verify_tag; use crypto::symmetric::stream_cipher; @@ -28,13 +28,22 @@ use credentials::token::bandwidth::TokenCredential; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] pub enum RegistrationHandshake { - HandshakePayload { data: Vec }, - HandshakeError { message: String }, + HandshakePayload { + #[serde(default)] + protocol_version: Option, + data: Vec, + }, + HandshakeError { + message: String, + }, } impl RegistrationHandshake { pub fn new_payload(data: Vec) -> Self { - RegistrationHandshake::HandshakePayload { data } + RegistrationHandshake::HandshakePayload { + protocol_version: Some(PROTOCOL_VERSION), + data, + } } pub fn new_error>(message: S) -> Self { @@ -115,12 +124,16 @@ pub enum ClientControlRequest { // TODO: should this also contain a MAC considering that at this point we already // have the shared key derived? Authenticate { + #[serde(default)] + protocol_version: Option, address: String, enc_address: String, iv: String, }, #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { + #[serde(default)] + protocol_version: Option, data: Vec, }, BandwidthCredential { @@ -137,6 +150,7 @@ impl ClientControlRequest { iv: IV, ) -> Self { ClientControlRequest::Authenticate { + protocol_version: Some(PROTOCOL_VERSION), address: address.as_base58_string(), enc_address: enc_address.to_base58_string(), iv: iv.to_base58_string(), @@ -223,10 +237,14 @@ impl TryInto for ClientControlRequest { #[serde(tag = "type", rename_all = "camelCase")] pub enum ServerResponse { Authenticate { + #[serde(default)] + protocol_version: Option, status: bool, bandwidth_remaining: i64, }, Register { + #[serde(default)] + protocol_version: Option, status: bool, }, Bandwidth { @@ -381,14 +399,37 @@ mod tests { #[test] fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() { let handshake_data = vec![1, 2, 3, 4, 5, 6]; - let handshake_payload = RegistrationHandshake::HandshakePayload { + let handshake_payload_with_protocol = RegistrationHandshake::HandshakePayload { + protocol_version: Some(42), data: handshake_data.clone(), }; - let serialized = serde_json::to_string(&handshake_payload).unwrap(); + let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap(); let deserialized = ClientControlRequest::try_from(serialized).unwrap(); match deserialized { - ClientControlRequest::RegisterHandshakeInitRequest { data } => { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert_eq!(protocol_version, Some(42)); + assert_eq!(data, handshake_data) + } + _ => unreachable!("this branch shouldn't have been reached!"), + } + + let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { + protocol_version: None, + data: handshake_data.clone(), + }; + let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap(); + let deserialized = ClientControlRequest::try_from(serialized).unwrap(); + + match deserialized { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert!(protocol_version.is_none()); assert_eq!(data, handshake_data) } _ => unreachable!("this branch shouldn't have been reached!"), diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index abe2f30a87..14116c948b 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -18,7 +18,7 @@ use gateway_requests::iv::{IVConversionError, IV}; use gateway_requests::registration::handshake::error::HandshakeError; use gateway_requests::registration::handshake::{gateway_handshake, SharedKeys}; use gateway_requests::types::{ClientControlRequest, ServerResponse}; -use gateway_requests::BinaryResponse; +use gateway_requests::{BinaryResponse, PROTOCOL_VERSION}; use log::*; use mixnet_client::forwarder::MixForwardingSender; use nymsphinx::DestinationAddressBytes; @@ -55,6 +55,9 @@ enum InitialAuthenticationError { #[error("Experienced connection error - {0}")] ConnectionError(#[from] WsError), + + #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] + IncompatibleProtocol { client: Option, current: u8 }, } impl InitialAuthenticationError { @@ -279,10 +282,7 @@ where // push them to the client if let Err(err) = self.push_packets_to_client(shared_keys, messages).await { - warn!( - "We failed to send stored messages to fresh client - {}", - err - ); + warn!("We failed to send stored messages to fresh client - {err}",); return Err(InitialAuthenticationError::ConnectionError(err)); } else { // if it was successful - remove them from the store @@ -342,6 +342,33 @@ where } } + fn check_client_protocol( + &self, + client_protocol: Option, + ) -> Result<(), InitialAuthenticationError> { + // right now there are no failure cases here, but this might change in the future + match client_protocol { + None => { + warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); + // note: in 1.2.0 we will have to return a hard error here + Ok(()) + } + Some(v) if v != PROTOCOL_VERSION => { + let err = InitialAuthenticationError::IncompatibleProtocol { + client: Some(v), + current: PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } + + Some(_) => { + info!("the client is using exactly the same protocol version as we are. We're good to continue!"); + Ok(()) + } + } + } + /// Using the received challenge data, i.e. client's address as well the ciphertext of it plus /// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches /// the expected value if encrypted with the shared key. @@ -389,6 +416,7 @@ where /// * `iv`: fresh IV received with the request. async fn handle_authenticate( &mut self, + client_protocol_version: Option, address: String, enc_address: String, iv: String, @@ -396,6 +424,8 @@ where where S: AsyncRead + AsyncWrite + Unpin, { + self.check_client_protocol(client_protocol_version)?; + let address = DestinationAddressBytes::try_from_base58_string(address) .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; let encrypted_address = EncryptedAddressBytes::try_from_base58_string(enc_address)?; @@ -420,6 +450,7 @@ where Ok(InitialAuthResult::new( client_details, ServerResponse::Authenticate { + protocol_version: Some(PROTOCOL_VERSION), status, bandwidth_remaining, }, @@ -474,11 +505,14 @@ where /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, + client_protocol_version: Option, init_data: Vec, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, { + self.check_client_protocol(client_protocol_version)?; + let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -493,7 +527,10 @@ where Ok(InitialAuthResult::new( Some(client_details), - ServerResponse::Register { status }, + ServerResponse::Register { + protocol_version: Some(PROTOCOL_VERSION), + status, + }, )) } @@ -513,13 +550,18 @@ where if let Ok(request) = ClientControlRequest::try_from(raw_request) { match request { ClientControlRequest::Authenticate { + protocol_version, address, enc_address, iv, - } => self.handle_authenticate(address, enc_address, iv).await, - ClientControlRequest::RegisterHandshakeInitRequest { data } => { - self.handle_register(data).await + } => { + self.handle_authenticate(protocol_version, address, enc_address, iv) + .await } + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => self.handle_register(protocol_version, data).await, // won't accept anything else (like bandwidth) without prior authentication _ => Err(InitialAuthenticationError::InvalidRequest), } From 0084ba221bd795f6454ef3ce7b5d348250d02893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Fri, 25 Nov 2022 16:03:04 +0100 Subject: [PATCH 69/75] Set build on latest release on schedule event --- .../nightly_build_release_matrix.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/nightly_build_release_matrix.json b/.github/workflows/nightly_build_release_matrix.json index c292817b6a..a979c90a62 100644 --- a/.github/workflows/nightly_build_release_matrix.json +++ b/.github/workflows/nightly_build_release_matrix.json @@ -2,49 +2,49 @@ { "os":"ubuntu-latest", "rust":"stable", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"windows-latest", "rust":"stable", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"macos-latest", "rust":"stable", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"ubuntu-latest", "rust":"beta", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"windows-latest", "rust":"beta", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"macos-latest", "rust":"beta", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"ubuntu-latest", "rust":"nightly", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"windows-latest", "rust":"nightly", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" }, { "os":"macos-latest", "rust":"nightly", - "runOnEvent":"workflow_dispatch" + "runOnEvent":"schedule" } ] From d25848e6f85ca82b4bdfbbe12fe67f20517ee949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Walther?= Date: Mon, 28 Nov 2022 10:41:17 +0100 Subject: [PATCH 70/75] Added nightly build workflow on second latest release --- .github/workflows/nightly_build_release.yml | 8 +- .github/workflows/nightly_build_release2.yml | 203 ++++++++++++++++++ .../nightly_build_release_matrix.json | 50 ----- 3 files changed, 207 insertions(+), 54 deletions(-) create mode 100644 .github/workflows/nightly_build_release2.yml delete mode 100644 .github/workflows/nightly_build_release_matrix.json diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 9a1ba43dc5..4474b399b6 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -9,12 +9,12 @@ jobs: outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - # creates the matrix strategy from nightly_build_release_matrix.json + # creates the matrix strategy from nightly_build_matrix_includes.json - uses: actions/checkout@v3 - id: set-matrix uses: JoshuaTheMiller/conditional-build-matrix@main with: - inputFile: '.github/workflows/nightly_build_release_matrix.json' + inputFile: '.github/workflows/nightly_build_matrix_includes.json' filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' get_release: runs-on: ubuntu-latest @@ -174,7 +174,7 @@ jobs: args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings notification: - needs: build + needs: [build,get_release] runs-on: ubuntu-latest steps: - name: Collect jobs status @@ -192,7 +192,7 @@ jobs: NYM_PROJECT_NAME: "Nym nightly build on latest release" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" + GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}" KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" diff --git a/.github/workflows/nightly_build_release2.yml b/.github/workflows/nightly_build_release2.yml new file mode 100644 index 0000000000..71f89948c9 --- /dev/null +++ b/.github/workflows/nightly_build_release2.yml @@ -0,0 +1,203 @@ +name: Nightly builds on second latest release + +on: + schedule: + - cron: '24 2 * * *' +jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from nightly_build_matrix_includes.json + - uses: actions/checkout@v3 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/nightly_build_matrix_includes.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' + get_release: + runs-on: ubuntu-latest + needs: matrix_prep + outputs: + output1: ${{ steps.step2.outputs.latest_release }} + steps: + - name: Check out repository code + uses: actions/checkout@v3 + - name: Fetch all branches + run: git fetch --all + - name: Set output variable to latest release branch + id: step2 + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 2 | head -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT + build: + needs: [get_release,matrix_prep] + strategy: + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }} + steps: + - name: Install Dependencies (Linux) + run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + if: matrix.os == 'ubuntu-latest' + + - name: Check out latest release branch + uses: actions/checkout@v3 + with: + ref: ${{needs.get_release.outputs.output1}} + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - name: Build all binaries + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run all tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run expensive tests + if: github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master' + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --all-features -- --ignored + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - uses: actions-rs/clippy-check@v1 + name: Clippy checks + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - name: Run clippy + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets -- -D warnings + + - name: Reclaim some disk space + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + with: + command: clean + + # COCONUT stuff + - name: Build all binaries with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace --features=coconut + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run all tests with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --features=coconut + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run clippy with coconut enabled + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets --features=coconut -- -D warnings + + # nym-wallet (the rust part) + - name: Build nym-wallet rust code + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Run nym-wallet tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Check nym-wallet formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --manifest-path nym-wallet/Cargo.toml --all -- --check + + - name: Run clippy for nym-wallet + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings + + notification: + needs: [build,get_release] + runs-on: ubuntu-latest + steps: + - name: Collect jobs status + uses: technote-space/workflow-conclusion-action@v2 + - name: Check out repository code + uses: actions/checkout@v3 + - name: Keybase - Node Install + if: env.WORKFLOW_CONCLUSION == 'failure' + run: npm install + working-directory: .github/workflows/support-files + - name: Keybase - Send Notification + if: env.WORKFLOW_CONCLUSION == 'failure' + env: + NYM_NOTIFICATION_KIND: nightly + NYM_PROJECT_NAME: "Nym nightly build on latest release" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" + KEYBASE_NYM_CHANNEL: "ci-nightly-release" + IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/nightly_build_release_matrix.json b/.github/workflows/nightly_build_release_matrix.json deleted file mode 100644 index a979c90a62..0000000000 --- a/.github/workflows/nightly_build_release_matrix.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "os":"ubuntu-latest", - "rust":"stable", - "runOnEvent":"schedule" - }, - - { - "os":"windows-latest", - "rust":"stable", - "runOnEvent":"schedule" - }, - { - "os":"macos-latest", - "rust":"stable", - "runOnEvent":"schedule" - }, - - { - "os":"ubuntu-latest", - "rust":"beta", - "runOnEvent":"schedule" - }, - { - "os":"windows-latest", - "rust":"beta", - "runOnEvent":"schedule" - }, - { - "os":"macos-latest", - "rust":"beta", - "runOnEvent":"schedule" - }, - - { - "os":"ubuntu-latest", - "rust":"nightly", - "runOnEvent":"schedule" - }, - { - "os":"windows-latest", - "rust":"nightly", - "runOnEvent":"schedule" - }, - { - "os":"macos-latest", - "rust":"nightly", - "runOnEvent":"schedule" - } -] From 65a69b2cbae45f4df9cf6081d6939f51a1797ea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 28 Nov 2022 10:49:00 +0100 Subject: [PATCH 71/75] socks5: if any task panics, signal all other tasks to shutdown (#1805) * socks5: signal shutdown on error * Mark as success * Tidy * Reduce wait to 5 sec * Replace unwrap with expect * Two more unwraps * Update changelog --- CHANGELOG.md | 2 + Cargo.lock | 10 +- .../src/client/cover_traffic_stream.rs | 4 +- clients/client-core/src/client/mix_traffic.rs | 6 +- clients/client-core/src/client/mod.rs | 9 -- .../acknowledgement_listener.rs | 6 +- .../action_controller.rs | 4 +- .../input_message_listener.rs | 6 +- .../retransmission_request_listener.rs | 6 +- .../real_traffic_stream.rs | 4 +- .../client-core/src/client/received_buffer.rs | 12 +- .../src/client/topology_control.rs | 4 +- clients/client-core/src/error.rs | 3 + clients/socks5/src/client/mod.rs | 57 ++++++--- clients/socks5/src/commands/mod.rs | 5 +- clients/socks5/src/commands/run.rs | 8 +- clients/socks5/src/error.rs | 7 ++ clients/socks5/src/main.rs | 5 +- clients/socks5/src/socks/client.rs | 2 + clients/socks5/src/socks/mixnet_responses.rs | 6 +- clients/socks5/src/socks/server.rs | 14 ++- .../client-libs/gateway-client/src/client.rs | 2 + .../src/connection_controller.rs | 3 + .../proxy-helpers/src/proxy_runner/inbound.rs | 1 + .../proxy-helpers/src/proxy_runner/mod.rs | 1 + .../src/proxy_runner/outbound.rs | 1 + common/task/Cargo.toml | 2 + common/task/src/shutdown.rs | 113 +++++++++++++++++- common/task/src/signal.rs | 29 +++++ nym-connect/Cargo.lock | 10 +- nym-connect/Cargo.toml | 2 +- nym-connect/src-tauri/src/tasks.rs | 2 +- 32 files changed, 283 insertions(+), 63 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d6d57cd6b..770201d659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### Fixed - gateway-client: fix decrypting stored messages on reconnect ([#1786]) +- socks5-client: fix shutting down all tasks if anyone of them panics or errors out ([#1805]) [#1678]: https://github.com/nymtech/nym/pull/1678 [#1708]: https://github.com/nymtech/nym/pull/1708 @@ -28,6 +29,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1747]: https://github.com/nymtech/nym/pull/1747 [#1783]: https://github.com/nymtech/nym/pull/1783 [#1786]: https://github.com/nymtech/nym/pull/1786 +[#1805]: https://github.com/nymtech/nym/pull/1805 ## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09) diff --git a/Cargo.lock b/Cargo.lock index 4303004d95..0ad2f180df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5693,7 +5693,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", ] @@ -5833,18 +5835,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index ee3b7851cb..228234663a 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -228,7 +228,9 @@ impl LoopCoverTrafficStream { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("LoopCoverTrafficStream: Exiting"); }) } diff --git a/clients/client-core/src/client/mix_traffic.rs b/clients/client-core/src/client/mix_traffic.rs index d5702cc0a8..06846316cc 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -69,6 +69,8 @@ impl MixTrafficController { #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + spawn_future(async move { debug!("Started MixTrafficController with graceful shutdown support"); @@ -88,7 +90,9 @@ impl MixTrafficController { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("MixTrafficController: Exiting"); }) } diff --git a/clients/client-core/src/client/mod.rs b/clients/client-core/src/client/mod.rs index bce1a089dc..309be529f1 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -1,5 +1,3 @@ -use std::sync::atomic::AtomicBool; - pub mod cover_traffic_stream; pub mod inbound_messages; pub mod key_manager; @@ -9,10 +7,3 @@ pub mod received_buffer; #[cfg(feature = "reply-surb")] pub mod reply_key_storage; pub mod topology_control; - -// This is *NOT* used to signal shutdown. -// It's critical that we don't have any tasks finishing early, this is an additional safety check -// that tasks exiting are doing so because shutdown has been signalled, and no other reason. -// In particular for tasks that rely on their associated channel being closed to signal shutdown, -// and don't have access to a shutdown listener channel. -pub static SHUTDOWN_HAS_BEEN_SIGNALLED: AtomicBool = AtomicBool::new(false); diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 9b285d8444..dff476ad3c 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -72,6 +72,8 @@ impl AcknowledgementListener { #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started AcknowledgementListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -88,7 +90,9 @@ impl AcknowledgementListener { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("AcknowledgementListener: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 2b21639230..822cea0a2f 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -272,7 +272,9 @@ impl ActionController { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("ActionController: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 0738d0aa80..c58967104c 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -196,6 +196,8 @@ where #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started InputMessageListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -214,7 +216,9 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("InputMessageListener: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 4b0bc3cc99..d0fd805d22 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -127,6 +127,8 @@ where #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started RetransmissionRequestListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -143,7 +145,9 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("RetransmissionRequestListener: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 503203922c..b10e0f8b96 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -514,7 +514,9 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("OutQueueControl: Exiting"); } diff --git a/clients/client-core/src/client/received_buffer.rs b/clients/client-core/src/client/received_buffer.rs index fa61b9b9a0..7e5a0d7db8 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -322,6 +322,8 @@ impl RequestReceiver { #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started RequestReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { @@ -340,7 +342,9 @@ impl RequestReceiver { }, } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("RequestReceiver: Exiting"); } @@ -372,6 +376,8 @@ impl FragmentedMessageReceiver { #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started FragmentedMessageReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { @@ -389,7 +395,9 @@ impl FragmentedMessageReceiver { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("FragmentedMessageReceiver: Exiting"); } diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index afedc7ad61..b685cfb8b4 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -311,7 +311,9 @@ impl TopologyRefresher { }, } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("TopologyRefresher: Exiting"); }) } diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 24e846087f..0313db076b 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -26,4 +26,7 @@ pub enum ClientCoreError { CouldNotLoadExistingGatewayConfiguration(std::io::Error), #[error("The current network topology seem to be insufficient to route any packets through")] InsufficientNetworkTopology, + + #[error("Unexpected exit")] + UnexpectedExit, } diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index c6b734a749..399254c0d1 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::Ordering; +use std::error::Error; use crate::client::config::Config; use crate::error::Socks5ClientError; @@ -38,7 +38,8 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use tap::TapFallible; -use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; +use task::signal::wait_for_signal_and_error; +use task::{ShutdownListener, ShutdownNotifier}; pub mod config; @@ -299,7 +300,7 @@ impl NymClient { msg_input: InputMessageSender, client_connection_tx: ConnectionCommandSender, lane_queue_lengths: LaneQueueLengths, - shutdown: ShutdownListener, + mut shutdown: ShutdownListener, ) { info!("Starting socks5 listener..."); let auth_methods = vec![AuthenticationMethods::NoAuth as u8]; @@ -312,38 +313,57 @@ impl NymClient { self.config.get_provider_mix_address(), self.as_mix_recipient(), lane_queue_lengths, - shutdown, + shutdown.clone(), ); tokio::spawn(async move { - sphinx_socks + // Ideally we should have a fully fledged task manager to check for errors in all + // tasks. + // However, pragmatically, we start out by at least reporting errors for some of the + // tasks that interact with the outside world and can fail in normal operation, such as + // network issues. + // TODO: replace this by a generic solution, such as a task manager that stores all + // JoinHandles of all spawned tasks. + if let Err(res) = sphinx_socks .serve(msg_input, buffer_requester, client_connection_tx) .await + { + shutdown.send_we_stopped(Box::new(res)); + } }); } /// blocking version of `start` method. Will run forever (or until SIGINT is sent) - pub async fn run_forever(&mut self) -> Result<(), Socks5ClientError> { - let mut shutdown = self.start().await?; - wait_for_signal().await; + pub async fn run_forever(&mut self) -> Result<(), Box> { + let mut shutdown = self + .start() + .await + .map_err(|err| Box::new(err) as Box)?; + + let res = wait_for_signal_and_error(&mut shutdown).await; log::info!("Sending shutdown"); - client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); shutdown.signal_shutdown().ok(); log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); shutdown.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); - Ok(()) + res } // Variant of `run_forever` that listends for remote control messages pub async fn run_and_listen( &mut self, mut receiver: Socks5ControlMessageReceiver, - ) -> Result<(), Socks5ClientError> { - let mut shutdown = self.start().await?; - tokio::select! { + ) -> Result<(), Box> { + // Start the main task + let mut shutdown = self + .start() + .await + .map_err(|err| Box::new(err) as Box)?; + + let res = tokio::select! { + biased; message = receiver.next() => { log::debug!("Received message: {:?}", message); match message { @@ -354,21 +374,26 @@ impl NymClient { log::info!("Channel closed, stopping"); } } + Ok(()) + } + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) } _ = tokio::signal::ctrl_c() => { log::info!("Received SIGINT"); + Ok(()) }, - } + }; log::info!("Sending shutdown"); - client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); shutdown.signal_shutdown().ok(); log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); shutdown.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); - Ok(()) + res } pub async fn start(&mut self) -> Result { diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index b069c13542..e1d42e0fb1 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; + use crate::client::config::Config; -use crate::error::Socks5ClientError; use clap::CommandFactory; use clap::{Parser, Subcommand}; use completions::{fig_generate, ArgShell}; @@ -87,7 +88,7 @@ pub(crate) struct OverrideConfig { enabled_credentials_mode: bool, } -pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Cli) -> Result<(), Box> { let bin_name = "nym-socks5-client"; match &args.command { diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 406ae72d94..d74e6678fe 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -86,14 +86,16 @@ fn version_check(cfg: &Config) -> bool { } } -pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Run) -> Result<(), Box> { let id = &args.id; let mut config = match Config::load_from_file(Some(id)) { Ok(cfg) => cfg, Err(err) => { error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); - return Err(Socks5ClientError::FailedToLoadConfig(id.to_string())); + return Err(Box::new(Socks5ClientError::FailedToLoadConfig( + id.to_string(), + ))); } }; @@ -102,7 +104,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> { if !version_check(&config) { error!("failed the local version check"); - return Err(Socks5ClientError::FailedLocalVersionCheck); + return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck)); } NymClient::new(config).run_forever().await diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index 4ebca0e2e5..6b67e6a339 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -3,6 +3,8 @@ use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; +use crate::socks::types::SocksProxyError; + #[derive(thiserror::Error, Debug)] pub enum Socks5ClientError { #[error("I/O error: {0}")] @@ -18,8 +20,13 @@ pub enum Socks5ClientError { #[error("Reply key storage error: {0}")] ReplyKeyStorageError(#[from] ReplyKeyStorageError), + #[error("SOCKS proxy error")] + SocksProxyError(SocksProxyError), + #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, + #[error("Fail to bind address")] + FailToBindAddress, } diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index d7a0fdf01f..d41e5e4bd8 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; + use clap::{crate_version, Parser}; -use error::Socks5ClientError; use logging::setup_logging; use network_defaults::setup_env; @@ -12,7 +13,7 @@ pub mod error; pub mod socks; #[tokio::main] -async fn main() -> Result<(), Socks5ClientError> { +async fn main() -> Result<(), Box> { setup_logging(); println!("{}", banner()); diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index df73d9ee7f..3e3c24347c 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -202,6 +202,7 @@ impl SocksClient { pub async fn shutdown(&mut self) -> Result<(), SocksProxyError> { info!("client is shutting down its TCP stream"); self.stream.shutdown().await?; + self.shutdown_listener.mark_as_success(); Ok(()) } @@ -318,6 +319,7 @@ impl SocksClient { SocksCommand::UdpAssociate => unimplemented!(), // not handled }; + self.shutdown_listener.mark_as_success(); Ok(()) } diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 760ec36d24..fa9ad1abe8 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use futures::channel::mpsc; use futures::StreamExt; use log::*; @@ -103,7 +105,9 @@ impl MixnetResponseListener { } } } - assert!(self.shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("MixnetResponseListener: Exiting"); } } diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index f61d6052a0..551f82b788 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -1,9 +1,8 @@ +use crate::error::Socks5ClientError; + use super::authentication::Authenticator; use super::client::SocksClient; -use super::{ - mixnet_responses::MixnetResponseListener, - types::{ResponseCode, SocksProxyError}, -}; +use super::{mixnet_responses::MixnetResponseListener, types::ResponseCode}; use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, @@ -12,6 +11,7 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller}; use std::net::SocketAddr; +use tap::TapFallible; use task::ShutdownListener; use tokio::net::TcpListener; @@ -56,8 +56,10 @@ impl SphinxSocksServer { input_sender: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, client_connection_tx: ConnectionCommandSender, - ) -> Result<(), SocksProxyError> { - let listener = TcpListener::bind(self.listening_address).await.unwrap(); + ) -> Result<(), Socks5ClientError> { + let listener = TcpListener::bind(self.listening_address) + .await + .tap_err(|err| log::error!("Failed to bind to address: {err}"))?; info!("Serving Connections..."); // controller for managing all active connections diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index bac92ec092..4b34f2239b 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -307,6 +307,8 @@ impl GatewayClient { let m_shutdown = self.shutdown.clone(); async { if let Some(mut s) = m_shutdown { + // TODO: fix this by marking as success _after_ the select + s.mark_as_success(); s.recv().await } else { std::future::pending::<()>().await diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 12fdba7dbb..f0f552d83c 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -254,6 +254,9 @@ impl Controller { }, } } + tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); assert!(self.shutdown.is_shutdown_poll()); log::debug!("SOCKS5 Controller: Exiting"); } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index 12caff2071..f2ebc2cd9b 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -208,5 +208,6 @@ where trace!("{} - inbound closed", connection_id); shutdown_notify.notify_one(); + shutdown_listener.mark_as_success(); reader } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index fbd8cd21b3..defcbf4f5f 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -134,6 +134,7 @@ where } pub fn into_inner(mut self) -> (TcpStream, ConnectionReceiver) { + self.shutdown_listener.mark_as_success(); ( self.socket.take().unwrap(), self.mix_receiver.take().unwrap(), diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index 8fad665785..78db73afb6 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -90,5 +90,6 @@ pub(super) async fn run_outbound( trace!("{} - outbound closed", connection_id); shutdown_notify.notify_one(); + shutdown_listener.mark_as_success(); (writer, mix_receiver) } diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index 4c0c088d67..f074cd2abd 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +futures = "0.3" log = "0.4" +thiserror = "1.0.37" tokio = { version = "1.21.2", features = ["macros", "signal", "time", "sync"] } [dev-dependencies] diff --git a/common/task/src/shutdown.rs b/common/task/src/shutdown.rs index 21ea12569d..5d4f2c77bb 100644 --- a/common/task/src/shutdown.rs +++ b/common/task/src/shutdown.rs @@ -1,27 +1,62 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::time::Duration; +use std::{error::Error, time::Duration}; -use tokio::sync::watch::{self, error::SendError}; +use futures::FutureExt; +use tokio::{ + sync::{ + mpsc, + watch::{self, error::SendError}, + }, + time::sleep, +}; const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5; +pub(crate) type SentError = Box; +type ErrorSender = mpsc::UnboundedSender; +type ErrorReceiver = mpsc::UnboundedReceiver; + +#[derive(thiserror::Error, Debug)] +enum TaskError { + #[error("Task halted unexpectedly")] + UnexpectedHalt, +} + /// Used to notify other tasks to gracefully shutdown #[derive(Debug)] pub struct ShutdownNotifier { + // These channels have the dual purpose of signalling it's time to shutdown, but also to keep + // track of which tasks we are still waiting for. notify_tx: watch::Sender<()>, notify_rx: Option>, shutdown_timer_secs: u64, + + // If any task failed, it needs to report separately + task_return_error_tx: ErrorSender, + task_return_error_rx: Option, + + // Also signal when the notifier is dropped, in case the task exits unexpectedly. + // Why are we not reusing the return error channel? Well, let me tell you kids, it's because I + // didn't manage to reliably get the explicitly sent error (and not the error sent during drop) + task_drop_tx: ErrorSender, + task_drop_rx: Option, } impl Default for ShutdownNotifier { fn default() -> Self { let (notify_tx, notify_rx) = watch::channel(()); + let (task_halt_tx, task_halt_rx) = mpsc::unbounded_channel(); + let (task_drop_tx, task_drop_rx) = mpsc::unbounded_channel(); Self { notify_tx, notify_rx: Some(notify_rx), shutdown_timer_secs: DEFAULT_SHUTDOWN_TIMER_SECS, + task_return_error_tx: task_halt_tx, + task_return_error_rx: Some(task_halt_rx), + task_drop_tx, + task_drop_rx: Some(task_drop_rx), } } } @@ -40,6 +75,8 @@ impl ShutdownNotifier { .as_ref() .expect("Unable to subscribe to shutdown notifier that is already shutdown") .clone(), + self.task_return_error_tx.clone(), + self.task_drop_tx.clone(), ) } @@ -47,7 +84,31 @@ impl ShutdownNotifier { self.notify_tx.send(()) } + pub async fn wait_for_error(&mut self) -> Option { + let mut error_rx = self + .task_return_error_rx + .take() + .expect("Unable to wait for error: attempt to wait twice?"); + let mut drop_rx = self + .task_drop_rx + .take() + .expect("Unable to wait for error: attempt to wait twice?"); + + // During an error we are likely like to be swamped with drop notifications as well, this + // is a crude way to give priority to real errors (if there are any). + let drop_rx = drop_rx.recv().then(|msg| async move { + sleep(Duration::from_millis(50)).await; + msg + }); + + tokio::select! { + msg = error_rx.recv() => msg, + msg = drop_rx => msg + } + } + pub async fn wait_for_shutdown(&mut self) { + log::info!("Waiting for shutdown"); if let Some(notify_rx) = self.notify_rx.take() { drop(notify_rx); } @@ -56,7 +117,7 @@ impl ShutdownNotifier { _ = self.notify_tx.closed() => { log::info!("All registered tasks succesfully shutdown"); }, - _ = tokio::signal::ctrl_c() => { + _ = tokio::signal::ctrl_c() => { log::info!("Forcing shutdown"); } _ = tokio::time::sleep(Duration::from_secs(self.shutdown_timer_secs)) => { @@ -69,15 +130,36 @@ impl ShutdownNotifier { /// Listen for shutdown notifications #[derive(Clone, Debug)] pub struct ShutdownListener { + // If a shutdown notification has been registered shutdown: bool, + + // Listen for shutdown notifications, as well as a mechanism to report back that we have + // finished (the receiver is closed). notify: watch::Receiver<()>, + + // Send back error if we stopped + return_error: ErrorSender, + + // Also notify if we dropped without shutdown being registered + drop_error: ErrorSender, + + // Sometimes it's necessary to clone and drop the shutdown listener during normal operation, + // for those situations we need to explicitly not drop (and trigger shutdown). + set_not_drop: bool, } impl ShutdownListener { - fn new(notify: watch::Receiver<()>) -> ShutdownListener { + fn new( + notify: watch::Receiver<()>, + return_error: ErrorSender, + drop_error: ErrorSender, + ) -> ShutdownListener { ShutdownListener { shutdown: false, notify, + return_error, + drop_error, + set_not_drop: false, } } @@ -111,6 +193,29 @@ impl ShutdownListener { } } } + + pub fn send_we_stopped(&mut self, err: SentError) { + log::trace!("Notifying we stopped: {:?}", err); + if self.return_error.send(err).is_err() { + log::error!("Failed to send back error message"); + } + } + + pub fn mark_as_success(&mut self) { + self.set_not_drop = true; + } +} + +impl Drop for ShutdownListener { + fn drop(&mut self) { + if !self.set_not_drop && !self.is_shutdown_poll() { + log::trace!("Notifying stop on unexpected drop"); + // If we can't send, well then there is not much to do + self.drop_error + .send(Box::new(TaskError::UnexpectedHalt)) + .ok(); + } + } } #[cfg(test)] diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs index 40ca122848..a580c5e293 100644 --- a/common/task/src/signal.rs +++ b/common/task/src/signal.rs @@ -17,6 +17,35 @@ pub async fn wait_for_signal() { } } +#[cfg(unix)] +pub async fn wait_for_signal_and_error( + shutdown: &mut crate::ShutdownNotifier, +) -> Result<(), crate::shutdown::SentError> { + use tokio::signal::unix::{signal, SignalKind}; + + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); + let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + Ok(()) + }, + _ = sigterm.recv() => { + log::info!("Received SIGTERM"); + Ok(()) + } + _ = sigquit.recv() => { + log::info!("Received SIGQUIT"); + Ok(()) + } + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) + } + } +} + #[cfg(not(unix))] pub async fn wait_for_signal() { tokio::select! { diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index cab899eacf..22331cf94d 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -5459,7 +5459,9 @@ dependencies = [ name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", ] @@ -5784,18 +5786,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", diff --git a/nym-connect/Cargo.toml b/nym-connect/Cargo.toml index b9fb14e41b..3ee3f20be3 100644 --- a/nym-connect/Cargo.toml +++ b/nym-connect/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["src-tauri"] \ No newline at end of file +members = ["src-tauri"] diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 751f7c5566..85da77dfee 100644 --- a/nym-connect/src-tauri/src/tasks.rs +++ b/nym-connect/src-tauri/src/tasks.rs @@ -50,7 +50,7 @@ pub fn start_nym_socks5_client( .block_on(async move { socks5_client.run_and_listen(socks5_ctrl_rx).await }); if let Err(err) = result { - log::error!("SOCKS5 proxy failed to start: {err}"); + log::error!("SOCKS5 proxy failed: {err}"); socks5_status_tx .send(Socks5StatusMessage::FailedToStart) .expect("Failed to send status message back to main task"); From 018bf8c241b186cacc82f703a73af58e8e3f6fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 28 Nov 2022 12:14:27 +0100 Subject: [PATCH 72/75] client-core: less frequent status logging (#1806) --- .../real_messages_control/real_traffic_stream.rs | 10 +++++++++- .../real_traffic_stream/sending_delay_controller.rs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index b10e0f8b96..5b9bf6ed5b 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -483,6 +483,10 @@ where } else { log::debug!("{status_str}"); } + } + + #[cfg(not(target_arch = "wasm32"))] + fn log_status_infrequent(&self) { if self.sending_delay_controller.current_multiplier() > 1 { log::warn!( "Unable to send packets fast enough - sending delay multiplier set to: {}", @@ -495,7 +499,8 @@ where pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started OutQueueControl with graceful shutdown support"); - let mut status_timer = tokio::time::interval(Duration::from_secs(1)); + let mut status_timer = tokio::time::interval(Duration::from_secs(5)); + let mut infrequent_status_timer = tokio::time::interval(Duration::from_secs(60)); while !shutdown.is_shutdown() { tokio::select! { @@ -506,6 +511,9 @@ where _ = status_timer.tick() => { self.log_status(); } + _ = infrequent_status_timer.tick() => { + self.log_status_infrequent(); + } next_message = self.next() => if let Some(next_message) = next_message { self.on_message(next_message).await; } else { diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index c90f7b4983..b22ffae7ff 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -82,7 +82,7 @@ impl SendingDelayController { self.current_multiplier = (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); self.time_when_changed = get_time_now(); - log::debug!( + log::warn!( "Increasing sending delay multiplier to: {}", self.current_multiplier ); From d004db803726f845c6ad1f535eed22caeda4a26c Mon Sep 17 00:00:00 2001 From: Fouad Date: Mon, 28 Nov 2022 11:22:11 +0000 Subject: [PATCH 73/75] Feature/nym connect UI updates (#1784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * create custom titlebar * create help page * create generic modal component * create separate connection time component * link to shipyard docs * move timer to separate component and update connection status component usage * use separate component for copying ip and port details * only show infomodal once after connection * set service provider on tauri side * Emit events when stopped * listen and unlisten for tauri events * connect: add trace log to get_services * Add back CI notifications * Update README Co-authored-by: Jon Häggblad Co-authored-by: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> --- .github/workflows/nym-connect.yml | 32 ++--- nym-connect/.storybook/preview.js | 20 ++-- nym-connect/Cargo.lock | 2 +- nym-connect/README.md | 4 +- nym-connect/src-tauri/Cargo.toml | 2 +- nym-connect/src-tauri/src/models/mod.rs | 4 +- .../src-tauri/src/operations/directory/mod.rs | 2 + nym-connect/src-tauri/src/tasks.rs | 35 +++++- nym-connect/src-tauri/tauri.conf.json | 23 ++-- nym-connect/src/App.tsx | 23 +++- nym-connect/src/assets/help-step-four.png | Bin 0 -> 8530 bytes nym-connect/src/assets/help-step-one.png | Bin 0 -> 14403 bytes nym-connect/src/assets/help-step-three.png | Bin 0 -> 18990 bytes nym-connect/src/assets/help-step-two.png | Bin 0 -> 35631 bytes nym-connect/src/components/AppWindowFrame.tsx | 17 ++- .../src/components/ConnectionButton.tsx | 73 ++++++------ .../src/components/ConnectionStatus.tsx | 71 +++++------ .../src/components/ConntectionTimer.tsx | 26 +++++ nym-connect/src/components/CustomTitleBar.tsx | 41 +++++++ nym-connect/src/components/HelpPage.tsx | 33 ++++++ .../src/components/HelpPageActions.tsx | 20 ++++ nym-connect/src/components/HelpPageImage.tsx | 5 + .../src/components/HelpPageStepIndicator.tsx | 15 +++ nym-connect/src/components/InfoModal.tsx | 69 +++++++++++ .../src/components/IpAddressAndPort.tsx | 8 +- .../src/components/IpAddressAndPortModal.tsx | 41 +++++++ .../components/ServiceProviderSelector.tsx | 110 ++++++++---------- nym-connect/src/context/main.tsx | 60 ++++++++-- nym-connect/src/context/mocks/main.tsx | 24 ++++ nym-connect/src/index.tsx | 8 +- nym-connect/src/layouts/ConnectedLayout.tsx | 37 ++++-- nym-connect/src/layouts/DefaultLayout.tsx | 38 +++--- nym-connect/src/layouts/HelpGuideLayout.tsx | 70 +++++++++++ nym-connect/src/stories/AppFlow.stories.tsx | 13 ++- .../src/stories/ConnectedLayout.stories.tsx | 6 +- .../src/stories/DefaultLayout.stories.tsx | 21 +++- nym-connect/src/theme/index.tsx | 6 +- nym-connect/src/theme/mui-theme.d.ts | 1 + nym-connect/src/theme/theme.tsx | 6 +- nym-connect/src/types/error.ts | 4 + nym-connect/src/types/event.ts | 6 + 41 files changed, 718 insertions(+), 258 deletions(-) create mode 100644 nym-connect/src/assets/help-step-four.png create mode 100644 nym-connect/src/assets/help-step-one.png create mode 100644 nym-connect/src/assets/help-step-three.png create mode 100644 nym-connect/src/assets/help-step-two.png create mode 100644 nym-connect/src/components/ConntectionTimer.tsx create mode 100644 nym-connect/src/components/CustomTitleBar.tsx create mode 100644 nym-connect/src/components/HelpPage.tsx create mode 100644 nym-connect/src/components/HelpPageActions.tsx create mode 100644 nym-connect/src/components/HelpPageImage.tsx create mode 100644 nym-connect/src/components/HelpPageStepIndicator.tsx create mode 100644 nym-connect/src/components/InfoModal.tsx create mode 100644 nym-connect/src/components/IpAddressAndPortModal.tsx create mode 100644 nym-connect/src/context/mocks/main.tsx create mode 100644 nym-connect/src/layouts/HelpGuideLayout.tsx create mode 100644 nym-connect/src/types/error.ts create mode 100644 nym-connect/src/types/event.ts diff --git a/.github/workflows/nym-connect.yml b/.github/workflows/nym-connect.yml index 3a08744fc9..5a652e92d2 100644 --- a/.github/workflows/nym-connect.yml +++ b/.github/workflows/nym-connect.yml @@ -41,19 +41,19 @@ jobs: - name: Keybase - Node Install run: npm install working-directory: .github/workflows/support-files -# - name: Keybase - Send Notification -# env: -# NYM_NOTIFICATION_KIND: nym-connect -# NYM_PROJECT_NAME: "nym-connect" -# NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" -# NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}" -# GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" -# GIT_BRANCH: "${GITHUB_REF##*/}" -# KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" -# KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" -# KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" -# KEYBASE_NYM_CHANNEL: "ci-nym-connect" -# IS_SUCCESS: "${{ job.status == 'success' }}" -# uses: docker://keybaseio/client:stable-node -# with: -# args: .github/workflows/support-files/notifications/entry_point.sh + - name: Keybase - Send Notification + env: + NYM_NOTIFICATION_KIND: nym-connect + NYM_PROJECT_NAME: "nym-connect" + NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" + NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "${GITHUB_REF##*/}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" + KEYBASE_NYM_CHANNEL: "ci-nym-connect" + IS_SUCCESS: "${{ job.status == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/nym-connect/.storybook/preview.js b/nym-connect/.storybook/preview.js index 2dbe041198..9c14c3ec99 100644 --- a/nym-connect/.storybook/preview.js +++ b/nym-connect/.storybook/preview.js @@ -1,26 +1,26 @@ import { NymMixnetTheme } from '../src/theme'; -import { ClientContextProvider } from '../src/context/main'; import { Fonts } from './preview-fonts'; - -const withThemeProvider= (Story, context) =>{ +import { MockProvider } from '../src/context/mocks/main'; +const withThemeProvider = (Story, context) => { return ( - - + + - + - ) -} + ); +}; + export const decorators = [withThemeProvider]; export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, -} \ No newline at end of file +}; diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 0d0022042e..db5454b1bc 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -596,7 +596,7 @@ dependencies = [ [[package]] name = "client-core" -version = "1.0.1" +version = "1.1.0" dependencies = [ "config", "crypto", diff --git a/nym-connect/README.md b/nym-connect/README.md index 5a247235ab..bbd9b55bbb 100644 --- a/nym-connect/README.md +++ b/nym-connect/README.md @@ -12,8 +12,8 @@ Nym Connects sets up a SOCKS5 proxy for local applications to use. ## Installation prerequisites - Linux / Mac - `Yarn` -- `NodeJS >= v16.8.0` -- `Rust & cargo >= v1.56` +- `NodeJS >= v16` +- `Rust & cargo` ## Installation prerequisites - Windows diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index 11f79849b7..dfaf452dd1 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -32,7 +32,7 @@ reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tap = "1.0.1" -tauri = { version = "^1.1.1", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] } +tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "window-start-dragging"] } tendermint-rpc = "0.23.0" thiserror = "1.0" tokio = { version = "1.21.2", features = ["sync", "time"] } diff --git a/nym-connect/src-tauri/src/models/mod.rs b/nym-connect/src-tauri/src/models/mod.rs index ded6a479fc..e5076ac5c0 100644 --- a/nym-connect/src-tauri/src/models/mod.rs +++ b/nym-connect/src-tauri/src/models/mod.rs @@ -45,7 +45,7 @@ pub struct AppEventConnectionStatusChangedPayload { } #[cfg_attr(test, derive(ts_rs::TS))] -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DirectoryService { pub id: String, pub description: String, @@ -53,7 +53,7 @@ pub struct DirectoryService { } #[cfg_attr(test, derive(ts_rs::TS))] -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DirectoryServiceProvider { pub id: String, pub description: String, diff --git a/nym-connect/src-tauri/src/operations/directory/mod.rs b/nym-connect/src-tauri/src/operations/directory/mod.rs index aaac2276d9..b6c45fa8c0 100644 --- a/nym-connect/src-tauri/src/operations/directory/mod.rs +++ b/nym-connect/src-tauri/src/operations/directory/mod.rs @@ -6,9 +6,11 @@ static SERVICE_PROVIDER_WELLKNOWN_URL: &str = #[tauri::command] pub async fn get_services() -> Result> { + log::trace!("Fetching services"); let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL) .await? .json::>() .await?; + log::trace!("Received: {:#?}", res); Ok(res) } diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 751f7c5566..0d13913af1 100644 --- a/nym-connect/src-tauri/src/tasks.rs +++ b/nym-connect/src-tauri/src/tasks.rs @@ -66,6 +66,12 @@ pub fn start_nym_socks5_client( Ok((socks5_ctrl_tx, socks5_status_rx, used_gateway)) } +#[derive(Clone, serde::Serialize)] +struct Payload { + title: String, + message: String, +} + /// The disconnect listener listens to the channel setup between the socks5 proxy task and the main /// tauri task. Primarily it listens for shutdown messages, and updates the state accordingly. pub fn start_disconnect_listener( @@ -78,14 +84,39 @@ pub fn start_disconnect_listener( match status_receiver.await { Ok(Socks5StatusMessage::Stopped) => { log::info!("SOCKS5 task reported it has finished"); + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 finished".into(), + message: "SOCKS5 task reported it has finished".into(), + }, + ) + .unwrap(); } Ok(Socks5StatusMessage::FailedToStart) => { log::info!("SOCKS5 task reported it failed to start"); + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 error".into(), + message: "SOCKS5 failed to start".into(), + }, + ) + .unwrap(); } Err(_) => { log::info!("SOCKS5 task appears to have stopped abruptly"); - // TODO: we should probably generate some events here, or otherwise signal to the - // frontend. + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 error".into(), + message: "SOCKS5 stopped abruptly".into(), + }, + ) + .unwrap(); } } diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json index c4fce76f06..b9cbfd8233 100644 --- a/nym-connect/src-tauri/tauri.conf.json +++ b/nym-connect/src-tauri/tauri.conf.json @@ -10,6 +10,7 @@ "beforeBuildCommand": "" }, "tauri": { + "macOSPrivateApi": true, "systemTray": { "iconPath": "icons/tray_icon.png", "iconAsTemplate": true @@ -18,13 +19,7 @@ "active": true, "targets": "all", "identifier": "net.nymtech.connect", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "resources": [], "externalBin": [], "copyright": "Copyright © 2021-2022 Nym Technologies SA", @@ -49,9 +44,7 @@ }, "updater": { "active": true, - "endpoints": [ - "https://nymtech.net/.wellknown/connect/updater.json" - ], + "endpoints": ["https://nymtech.net/.wellknown/connect/updater.json"], "dialog": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo=" }, @@ -61,14 +54,20 @@ }, "clipboard": { "writeText": true + }, + "window": { + "startDragging": true, + "close": true } }, "windows": [ { "title": "NymConnect", "width": 240, - "height": 500, - "resizable": false + "height": 540, + "resizable": false, + "decorations": false, + "transparent": true } ], "security": { diff --git a/nym-connect/src/App.tsx b/nym-connect/src/App.tsx index 10a2995f34..90dd1a4230 100644 --- a/nym-connect/src/App.tsx +++ b/nym-connect/src/App.tsx @@ -1,37 +1,50 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { DateTime } from 'luxon'; import { ConnectionStatusKind } from './types'; import { useClientContext } from './context/main'; import { DefaultLayout } from './layouts/DefaultLayout'; import { ConnectedLayout } from './layouts/ConnectedLayout'; +import { HelpGuideLayout } from './layouts/HelpGuideLayout'; export const App: React.FC = () => { const context = useClientContext(); const [busy, setBusy] = React.useState(); + const [showInfoModal, setShowInfoModal] = React.useState(false); const handleConnectClick = React.useCallback(async () => { - const oldStatus = context.connectionStatus; - if (oldStatus === ConnectionStatusKind.connected || oldStatus === ConnectionStatusKind.disconnected) { + const currentStatus = context.connectionStatus; + if (currentStatus === ConnectionStatusKind.connected || currentStatus === ConnectionStatusKind.disconnected) { setBusy(true); // eslint-disable-next-line default-case - switch (oldStatus) { + switch (currentStatus) { case ConnectionStatusKind.disconnected: await context.startConnecting(); + context.setConnectedSince(DateTime.now()); break; case ConnectionStatusKind.connected: await context.startDisconnecting(); + context.setConnectedSince(undefined); break; } setBusy(false); } }, [context.connectionStatus]); + useEffect(() => { + if (context.connectionStatus === ConnectionStatusKind.connected) setShowInfoModal(true); + }, [context.connectionStatus]); + + if (context.showHelp) return ; + if ( context.connectionStatus === ConnectionStatusKind.disconnected || context.connectionStatus === ConnectionStatusKind.connecting ) { return ( { return ( setShowInfoModal(false)} status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} diff --git a/nym-connect/src/assets/help-step-four.png b/nym-connect/src/assets/help-step-four.png new file mode 100644 index 0000000000000000000000000000000000000000..55079628923c10eaecf9243a6b99f8124d84d916 GIT binary patch literal 8530 zcmb7~Wl$Wzv*2+Sx8UyXA-FpPcZc91Sb$_9xNGp>4#8PGuz}!gg3H3<5(0~6kwtIAqnxF{$n1ezKuhR<^5x%~iOJ@+b>5w_0) z$4A4$4+RB}>^}_^CBKmBS&8arsIG+4G(~stJiu^O)KNr1X-UPqcfdqJVS;O_C>jG% zPm1t>}Tvq2+v>|R)?mXfDuBiognistFYEea_gt`b^j2QH4>4UjF}iZL4lx~PP< zc*KjdZ<54TZWyD|V5x=X=D~}j0rJ#A>04YZ-12m1CSqV=`}hRQlg$&&^LsCnNhx^* zr<|5-bG@37Y^MJvlBTs&il&fSAKQ2%EFzQtxLouMb|5m#+i8_ss3Xt zw()*v!Q#UoYAua;SMO+jLM2Ttx*7MV?_DHGcG9LWmnlxHpL7^!FAarvMvlUOl$I88(tj%h>5ukWI z`pwJGVa-+`>Z4LTwr@-w9;Ed1urTEXe9Tk2Mm*wX+-|j@(EToKi33oy&65y*iqA|+ zim5X;^Zx0mi79HnxcdD#?J?$73KJJ+D$2I~({Bi)v!1j1r|P{u3N6zRK4kq%2{=ap zzlQDpaLoK!mn|K(qB57*nBV9~ijXY*6v7B*-gVE6i%o-CNVbWcg7&k}FNs=!0u-9q z3WaN##QK$<%|Mo~A&6K6rgvj6e^UxF^O6VI5o!iaIZA=nO~^m^1KQorJuhSEF!3y5lbtxj{>1>EGF2DHMyFJ>&J&0G zm{A1-fC5q`cY=DxkOG+Md%wWy(z{f0m#Ne@|vk6@h%J`Dt0R$F?nzqxzBHePxqP@@}8L0z2sEoOf|r!g#DH5xp5!Nn{l zhS`H+`RT;ne$B*cqw?HL+-E5PVOGN6$Jrt)He&=1FKL5h{|`HqgeY)?G$3TkQmL+B*q3 z&6m!=PE!CY)wLr+8UX_QEBZwtz~U_59WEGgg&a0UswyF(z-*xOr-*Qa2Tmr7(mP11$+sCeKMW*^JGLHLj zgM-g6!xa*fKdAxw@>4#G$BXvZ*h7aTbbYBOGbm@!-kKBkNd0|5~35u%6<9>bx$M%1JX}b%i2W zyJQz#N!i*;_BKXF@ig)%fn0c?)OMCYY0&c3!NL5b=8AB-w5dl_Gf#adHbstB(a-sd za{MFYer-?-8tPZ*2u4a!K?q|w`0~S1D-SVtXGQ2yzDm#?a2&rj=*oEFYh5+tBJX;N zDI8WmB55NoC|}#@AV1;Ek4KHSK8Kwsx|caB!eHJ@V5^{aRFFXao2!{E(KZuXX_jov zF#xSW0Q=N7ihN~amyqDY8xlu5`?KF@tj0rGgEz_#LS6*XQ6AguI?zif4SZ%??+`QH zSJpfgaCcU2J&FrliuEhAeXTXEZ9#K)=G>9a)OD>u@Q8jKNJB%|?CVg4F2v%60qViB zugvAN0!?9|HyWyaHi0#_qTTWe+4q*y-f{HS2#S)*6l)|-%87W2iT%A z51ir#b(~~~>CKV`=n%ipKj$&u)yb6cDwHG_9**;f#zs1bor&ogp<;3j!?T&U(lhuy zP^o#90cbua5i?S(YXj#D=AVb$6Nmt#U=jLn=V|~<*698WeqQjltdb&-^e%%TsMjcK z!O|3XA$~LIclVB$(Hz7*1^3wW>gKR!Jsj=WA{tLYv1Kw3q%Le50|+NEgk@pIc`lml zMemRFhG$i49#%4EUnR^0e5XtXw^|>&VRUZQL`kE#4Ey_rhzEl`{YXc{nir{*ylropS zPtQ-#p>#jf(L<=MkVJFgCt6xAe5wb+Xy1sAz~%P#{%Eimw`=PF~!%+##~tt;M!nPC3DXILJ95wuw(5 z8#MR(pmX8xae(B)&@UWpZ>qQ7je4(CdkQK3Cbr8tY|VS(y$irzBQjVP07m`hWMoy` zMEeXgLrK|T1p``?iY zFX!)JHb6Chn7i>cW0*4XH{%<(fkg-8Uc(Lv<53+EvP~Y#Kj)f}g<%^Bk4#5ZM+yDd z?2&PrrP7~(>A+MdWi-_;hqt^-tR^mUQ8G;&ola znpRb;VmY56LQcRct4L>IL3-J|FkIukAH~_()k%$`t|vCp^$B@inM+Qtc+X{uP|{W6ir;jz(b5JqkBFs? z^-1xAdOPtK6iCMPWnPMt-+pEwBgO~SI0dA^XIG7k#}oQ0E48z*%~hSp%oS5elRNlW z9lyMt^pJ+GL!p()Zmi_wIsQU3D=S3Y{GKB0930dE6HXvhCYOo&qMUD8AiQ1O(#NlR z9L`(p^kY79){KV>VKl(DiH2@#aofN{d-hk-Zd5|T&bqo@aUw6N{Gct8k{{p8E^ZR_ z(W_*$6jyJ~IHJwPIzi<4v-M)Qgv{TSWaW=W*4EYinU0C!rBjxmL*JHMD9Qgaij_z~ z6DFh)orcmFQb$VJ^J0pmj$HjgXiLHl(xP=s1&z?>r(1#@&@iJw5g0V-m6dE4G>rrp zq4OK0dR7J3zxt#XXDM1YG^QvIT8b8`#?Ria!zSA>dy7>RO4MlulT>0vXi7*B@H!!0 z&pC`;RZvuPKN3R=@1*^elLWBbUHRJ%{uC z_5J7+6nyXti5PcOy$teMYoKZS{*`%+sG{!7_4mO+Kzc^kp9@1o*o z?8Z8y9f?tHHDkuK!Kf+mSJpQKGw_2k?=1>XpOiS-Y&R>e6&H_0Pvb|YeT&0SL}iLm zSgQW7L3jZ36yk?WrwUfWp(y6JJBqolBs~ptlB?gXOZ`pSmns$viEweA*pgy0EOE{V zw6kUkMxxysEUJW?oeiH!X`VPRwgM*^K*FT{5`O!PamMCzT9hFNGNJZ-`8BG8KnN+mb6)QUSvA%^vB*sc^t*PGpiM3=TyxA^yzBRiL%uXi~W#S?T+kwQ3?d^1Beox#2hr{SU z{HN@LfGZl8-oeB8*HU=!+hKJ6NvpwYdt$h2U1;Q$8Zb2t4=sGQqUYz&=rVbI1A`6s z5-uy}+E(K_Gg!pqSYo7-0G`tyxj&(JV@y+MI}C(>Im;+5jC-WsJpgLc_QS6u&C}Wy zdcg=Zgs@~lMz{Rj%>F&-B`M1?Vcxf`qv>7b^(%G>Cp6}%WTERpFy3EmDlAbapOReF zKKnnX=C{y&GS#{Gu6(kA>pvFvD?st?atmrKCC+MmldkvgQj;zM<>HK}J0mm>VURoX z=yW^1%FFM|n|Q>N@RBgyk`f8Uu!kc>t^2prK|POC8@-IPf$$>jC==z#K7FP4vYe4- z_Aj>aS&akDlDXF;3&+6qe|;dxKaaLE1T_to%g-lVf*Oxgs4yZ^a!#kA_QL1c)pF2e zVil2)&HrmO`L}?fX1Wg;Jq#QSWF|@LFYQRF97*9|Dx3$@AVpOF~xBb6k55A}= zJrgC>^=tQyzsBL18yz;GxwD)0tg`!uJ)3c&68NO)uZVEAj`8BiFn#rcD!b)_4Rwei zBb9^i$c+>D6@SM#y4#)Tjm;ptx%IlngP#$f4R_`^5#5Te!9;E5;jSo+Ka$bkNfj<` z`0$7Y4%DTXoY1H~T&6$;|7Bs{{`D<( zdQ6d5Lo}zNDqC%Z*|-yW7mMcno%r?DLUex8(=6FLOp0>jU_ocM2l%qnBV7@AIhxyl z{lWAC{WI)qm4T9yjAvs*xpqdzO|Oe%Am-$lM)k(%3MW402Dmdce!t3nA45}p-(O9h zXh=B_x;YR9k&^w|+4&iAU@_u|$zqXKq%At{`uwi9zhR%u&({6sGF7IR1vGup@aKg4 z*RS?AaO}uU8|>9wT%9d6#MSE+?4p=wwX4<};|2F`xpF%3Ur}niHW{R3%3tISj0gS1 z-6DF;uVid8rSdQ_wjyk&H-92PMTJ@&0Reo)R&64I^8BSbA$$xTPeM=xoBO-6@#}E4 z^PyA%=5gk<*)Kx)4*7cY70Tk@Z%(Pv219{FAdFL~&|r~E^6lsf5>yOKl%!#OJUaL>ID@eas+plGknG}(`rxe%u4p-jjY?t*xone+bL+7i*NoTW-xnw)-C zg%xZHe`M-GJICj}{YrA4mQcdp{Lnsv&(s#fS%c8cK-OeVu5Xk5?bD|m434DXsCeRv zh50R#V5iMdtJa{9h^q@b(CW_ByvcI|0Vt2 zzjxxcE%IJJuhI6~mU^p?!RITPa|vak^hX93CgyV5*jcqo=0$1+=9eC0`}OhvoMX?C#m^=Je&$UTYXtC@Hdw(8-k%iPSH9J^M^|&F z4nuCax9C(?4vkhY%)%+u)9znf%Cf1lXKe7Rfq|&7Y9hHC`jFj0JnsD2r>mgZ+6wj5 z`8-#`YPY?mmVWP21ZvE=f}20Mis*%FwC|*rhBhKBtu8Bqll}8{>%{8bSy1UsVc)SL z;R9*Pwa|UWf?np60uvn{zm@l59_TPaNUTO$dm*Nbp@#S4nT-pTyL=o&f7ki8dKPS% z4jC`Bp==+ZRl0avLEeoGm6R2s`92U9o@geoN@L{WA_dIAYEaOp5;mIzwDKs}&)nJB{zhJk{v-uAUHBGpdh^mE7wMoQeF784 zd_}5%g;*S*9ayN~Vb|vI?O*p-7x7Pc`dx>R|4>GrtQ8nTA~(pORV~JUtlnME0n;JJ z0@h>`{CBugSP@>c(#iTq%wc`{>Uy&G2hckV^MUb9Vaop%OPRX5;}9*|e^``pn5sNk zs!yYJAQdu?iIhrOW)@r`L$-haZNN_g4`ma5FpBsT`exufJg2pU^_e`;rIx((MMLD2 ziTG+5D%8Yi4bSSfZ{p?4UVnJ&?p~H?CZ4<6(5ERu1m&%03D9QL%$E*ZMz0HNFI9fR zF0*Vo*A)T+`M&2QAn6tZ%o-6D)J~f#)g4kQPxsTVMlXdQXulo z)ql)`f^7UEe>mJyh11Az1fP&5mbq=Ygu%D&IiHi9bfZIAnb_FU$_aPLgVjnj_o=bY zSqZ*xZYm=X{~CEoQ1i0Y0SQ*1izPBu7gIlI&4v5mg4rzW$D(|RQ?G}wz{%zl3BANA zcaodV8QIIj`-{nQ1pkz;jINp^UeTNBSE^&HefQ)iMrt@SVAjkI#TYHGz`G(lZUYJ< zJ(LKKSp^j$enEVmzJt}x-}2J3L98ZJ=3RNlfAZ@=m4V+j#}Kovtj4!f-@GebvPnaC zCQFQlBG9cr3|+$VHgK3<47Yl3hQ2y>Hv;XPJnxNM?r$D^ac(y!rD}ZC_U&R+NQ>v; zrO{7*3A1G|+*~Hd?t{AuMCCo}LPt2^D~2*xThLeer%h(5pnE@3UB?y6T9c|)E4`x1 z=~TvQ_?Pw9HqOKGB=1h`pMIofX@ve_QAi>}U=W2*6R(U=+6+f3nP2`OU5_X6<>P}o zIDeM%8n$3HuOVKX-Uv=Q^3^V^dxG$qoql4YD1P{5DaYC5kxwdVTeSay2;+?J(+518 zPb08{DTMA<(KsU3ZRAJ~(Qw=OJ?rnCt1x7`$YDu6u@_Nqg%TM13l;?o{JP`RL%>5E zDZYUVWR1s)Yz-11kRev$>!r8Fz5Bh#(F56agv<@Djr0(d^hREI20DsPpFM1RS}-1lCe?vmSj(aMkSXf>N)H1y{*KAv52SaD_PwQR|s6_?bb+uE+Of$kAxt*Ie6Rcl5^Fv#<+&(Q4Y8rZyi}LPo7fd+Z0M zXbWHiCJS&mBTt#Shm4abD}J;*6-wMCI7BV7!$5HLeXp(_O*8siXU&6p?d|WHs$Bq# z8i)TKtiIuwP>_@QsPTlCKvFYLb?pm0}S2)wiO1vdDGbVqxc5#m_ zSS1Y?dh0aMuFphM^dh{JxM{#jr`HQtwwX^aCMJ*$_cu{#u|_6E>JrV z<8@7KEb5}~dTU3j{j@|VGm3h3cX26VVYJ<;l=H-hbiP>TmiI0`nm1#eI9sUs} zq$)-!9tnM|t?d_gvcy7dld4Ado)ujK0&gcIF zF|rqo&F1HN*s}^U={p<1QvQT?`!usMtVEj)CbehjtRXzBA3&1%XyU=35|NP?Bwt_n z1erEM{-P_gbuZ&tsQlvQ&Po$n;d|x-1r)n~W|EQq6jXcJ;}a9V9na7IRzwbF2e&v^ z9RaOA{3m~pujz6Udu9OXOh8oe4@GLme1&1y1T-(SHw$TUd$1}0) zgR`q-nHgNHjuhC$z@V({C!pp&)B%; zY3%x)${n3A6;~tUNk1b8%7_!|LSHDP&=aPWiUSz+q9m%Rug5#5p{j&Ip~J;*4d2q> zeXXm@kf)?*Dn%ye^=#V83Czk6253;h>F-1ZmE#D@SRs0~1@*I>EcaLioBtFZup;`8 z+b#BNlen0fBfk9E_1gqESiZHSsp2uPxSk)by`DUz8jzOJloIhd47fRU@c;AEKnnUk zvV4+6krVOw*D+tzxiByNxaBe;2(8F^$+9EmdHIG?!v!LxC%{V!4D_?%7^*h{{sjD` z15sE8M_U6g?~@(wgY{>K1#56dEA9AKSGj<<)r5LJG z{Ug#T@kU4Lj<8pN3F(&>7^TmI!iN0}&bshRj^27qHc#?oK%vH0YG6I%Cd0Mc-YtS9 z?BwCXh9$d7cxklh1In*aFSUuHLnr8V z4P4$3N>m`)n%a}cMcP`84?mG?A`~q=PvVS?tp}ZJ-7_WKg+v{(!eX0oMNBhh=!Y@* zoqzI!L|EttTh891ceanwivU#=%S;M(sYLPpa2xqn9V1_1#`#LEwK9;A>4)j(Le!(X ze-Ogk+3-%+8-E|XRtUM3K=*2vaz4y2C`hi1^rLPT2w?H&^!!ZSrGM~OA7nEkM>F_q zUd1rZG2Ug_%aiN4^th8zRdrK;$R z8a4a~9$6gK+Iw`m{nD8h64C?dUOn<^%kr3pot-6JY2(X%Gm$visN~-V9$jMnJ@jv! z=FqFeWsW}}cDERT3b!V-P`CP3x`1E@y&c6{=RTL6jAYhkua_>YQhlTFImrJD(fr?# zb4yFXm5^3RJ+*{MkxHYh#oY`IymjxlPlF9>Zaw2wr#1v^TXp9hmpXIXh?u{-qxlcaoni0Z>S=S|AlNy; zb1fl1^Obq?UHnToWpqUR#A_Y^!*dGw|5Hx?@2V~T*J93_)s+>(%;yq*yvhCIxYk%C zaU&P}!g8X;5%ObgWd*DrB2j>$7!TO>C**~Tez8eqE{80PQnLY+G-h@Ur2g4mieD#- fPTkx*p?>VHb2K*s*> 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�t3F0PA2oun6An$m>7S{la;2+sY51dti~ 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 literal 0 HcmV?d00001 diff --git a/nym-connect/src/assets/help-step-two.png b/nym-connect/src/assets/help-step-two.png new file mode 100644 index 0000000000000000000000000000000000000000..f2910c98cf06b4d401e010be1e8ec43871171d3f GIT binary patch literal 35631 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ð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 ( - - - - {children} + + {children} ); diff --git a/nym-connect/src/components/ConnectionButton.tsx b/nym-connect/src/components/ConnectionButton.tsx index 4fd093a569..1a6499cf64 100644 --- a/nym-connect/src/components/ConnectionButton.tsx +++ b/nym-connect/src/components/ConnectionButton.tsx @@ -19,16 +19,16 @@ const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isErro switch (status) { case ConnectionStatusKind.disconnected: if (hover) { - return '#21D072'; + return '#FFFF33'; } - return '#60D6EF'; + return '#FFE600'; case ConnectionStatusKind.connecting: case ConnectionStatusKind.disconnecting: - return '#60D6EF'; + return '#FFE600'; default: // connected if (hover) { - return '#DA465B'; + return '#E43E3E'; } return '#21D072'; } @@ -81,69 +81,62 @@ export const ConnectionButton: React.FC<{ viewBox="0 0 208 208" fill="none" xmlns="http://www.w3.org/2000/svg" - onMouseEnter={() => !disabled && setHover(true)} - onMouseLeave={() => !disabled && setHover(false)} > - + !disabled && setHover(true)} + onMouseLeave={() => !disabled && setHover(false)} + > - - + + - - - - {busy && ( - - )} - + + + - + {status === ConnectionStatusKind.connected && hover ? ( ) : ( )} {statusText} - + - - + + - - + + diff --git a/nym-connect/src/components/ConnectionStatus.tsx b/nym-connect/src/components/ConnectionStatus.tsx index a866f8521d..c2bfcc82f0 100644 --- a/nym-connect/src/components/ConnectionStatus.tsx +++ b/nym-connect/src/components/ConnectionStatus.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { Box, CircularProgress, Typography } from '@mui/material'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; import { DateTime } from 'luxon'; import { ConnectionStatusKind } from '../types'; import { ServiceProvider } from '../types/directory'; -const FONT_SIZE = '16px'; +const FONT_SIZE = '10px'; const FONT_WEIGHT = '600'; const FONT_STYLE = 'normal'; @@ -16,39 +14,41 @@ const ConnectionStatusContent: React.FC<{ switch (status) { case ConnectionStatusKind.connected: return ( - <> - - - Connected - - + + Connected + ); case ConnectionStatusKind.disconnecting: return ( - <> + Disconnecting... - + ); case ConnectionStatusKind.connecting: return ( - <> + Connecting... - + ); case ConnectionStatusKind.disconnected: return ( - <> - - - Disconnected - - + + You are not protected + ); default: return null; @@ -61,29 +61,22 @@ export const ConnectionStatus: React.FC<{ serviceProvider?: ServiceProvider; }> = ({ status, connectedSince, serviceProvider }) => { const color = - status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting ? '#21D072' : '#888'; - const [duration, setDuration] = React.useState(); - React.useEffect(() => { - const intervalId = setInterval(() => { - if (connectedSince) { - setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss')); - } - }, 500); - return () => { - clearInterval(intervalId); - }; - }, [status, connectedSince]); + status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting + ? '#21D072' + : 'warning.main'; + return ( <> - - - - - - {status === ConnectionStatusKind.connected && duration} - + + + + + {serviceProvider && ( + + To {serviceProvider.description} + + )} - {serviceProvider && {serviceProvider.description}} ); }; diff --git a/nym-connect/src/components/ConntectionTimer.tsx b/nym-connect/src/components/ConntectionTimer.tsx new file mode 100644 index 0000000000..25e2af8d9c --- /dev/null +++ b/nym-connect/src/components/ConntectionTimer.tsx @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react'; +import { Stack, Typography } from '@mui/material'; +import { DateTime } from 'luxon'; + +export const ConnectionTimer = ({ connectedSince }: { connectedSince?: DateTime }) => { + const [duration, setDuration] = React.useState(); + useEffect(() => { + const intervalId = setInterval(() => { + if (connectedSince) { + setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss')); + } + }, 500); + return () => { + clearInterval(intervalId); + }; + }, [connectedSince]); + + return ( + + + Connection time + + {duration || '00:00:00'} + + ); +}; diff --git a/nym-connect/src/components/CustomTitleBar.tsx b/nym-connect/src/components/CustomTitleBar.tsx new file mode 100644 index 0000000000..dc4e8c0061 --- /dev/null +++ b/nym-connect/src/components/CustomTitleBar.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ArrowBack, Close, HelpOutline } from '@mui/icons-material'; +import { Box, IconButton } from '@mui/material'; +import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; +import { appWindow } from '@tauri-apps/api/window'; +import { useClientContext } from 'src/context/main'; + +const customTitleBarStyles = { + titlebar: { + background: '#1D2125', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + paddingBottom: '0px', + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px', + }, +}; + +const CustomButton = ({ Icon, onClick }: { Icon: React.JSXElementConstructor; onClick: () => void }) => ( + + + +); + +export const CustomTitleBar = () => { + const { showHelp, handleShowHelp } = useClientContext(); + return ( + + { + handleShowHelp(); + }} + /> + + appWindow.close()} /> + + ); +}; diff --git a/nym-connect/src/components/HelpPage.tsx b/nym-connect/src/components/HelpPage.tsx new file mode 100644 index 0000000000..788f7264d5 --- /dev/null +++ b/nym-connect/src/components/HelpPage.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { HelpPageActions } from './HelpPageActions'; +import { HelpImage } from './HelpPageImage'; +import { StepIndicator } from './HelpPageStepIndicator'; + +export const HelpPage = ({ + step, + description, + img, + onNext, + onPrev, +}: { + step: number; + description: string; + img: any; + onNext?: () => void; + onPrev?: () => void; +}) => ( + + + + + How to connect guide {step}/4 + + + {description} + + + + + +); diff --git a/nym-connect/src/components/HelpPageActions.tsx b/nym-connect/src/components/HelpPageActions.tsx new file mode 100644 index 0000000000..17e513120f --- /dev/null +++ b/nym-connect/src/components/HelpPageActions.tsx @@ -0,0 +1,20 @@ +import { ArrowBack, ArrowForward } from '@mui/icons-material'; +import { Box, Button } from '@mui/material'; +import React from 'react'; + +export const HelpPageActions = ({ onNext, onPrev }: { onNext?: () => void; onPrev?: () => void }) => ( + + {onPrev ? ( + + ) : ( +

+ )} + {onNext && ( + + )} + +); diff --git a/nym-connect/src/components/HelpPageImage.tsx b/nym-connect/src/components/HelpPageImage.tsx new file mode 100644 index 0000000000..27a8dcdca3 --- /dev/null +++ b/nym-connect/src/components/HelpPageImage.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const HelpImage = ({ img, imageDescription }: { img: any; imageDescription: string }) => ( + {imageDescription} +); diff --git a/nym-connect/src/components/HelpPageStepIndicator.tsx b/nym-connect/src/components/HelpPageStepIndicator.tsx new file mode 100644 index 0000000000..b5dff42b34 --- /dev/null +++ b/nym-connect/src/components/HelpPageStepIndicator.tsx @@ -0,0 +1,15 @@ +import { Box } from '@mui/material'; +import React from 'react'; + +const Step = ({ highlight }: { highlight: boolean }) => ( + +); + +export const StepIndicator = ({ step }: { step: number }) => ( + + + = 2} /> + = 3} /> + = 4} /> + +); diff --git a/nym-connect/src/components/InfoModal.tsx b/nym-connect/src/components/InfoModal.tsx new file mode 100644 index 0000000000..ed628de7eb --- /dev/null +++ b/nym-connect/src/components/InfoModal.tsx @@ -0,0 +1,69 @@ +import { Close, ErrorOutline } from '@mui/icons-material'; +import { Box, IconButton, Modal, Theme, Typography } from '@mui/material'; +import React from 'react'; + +const styles = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 200, + bgcolor: '#292E34', + p: 1.5, + borderRadius: 0.5, + height: 'fit-content', + border: (theme: Theme) => `1px solid ${theme.palette.grey[700]}`, +}; + +const ModalTitle = ({ title, withCloseIcon }: { title: string; withCloseIcon: boolean }) => ( + + + + {title} + + +); + +const ModalBody = ({ description, children }: { description: string; children?: React.ReactElement }) => ( + + {children} + + {description} + + +); + +export const InfoModal = ({ + title, + description, + show, + children, + Action, + onClose, +}: { + title: string; + description: string; + show: boolean; + children?: React.ReactElement; + Action?: React.ReactNode; + onClose?: () => void; +}) => ( + + + {onClose && ( + + + + + + )} + + {children} + {Action && ( + + {Action} + + )} + + +); diff --git a/nym-connect/src/components/IpAddressAndPort.tsx b/nym-connect/src/components/IpAddressAndPort.tsx index 435cf926fb..8d7d79c1f2 100644 --- a/nym-connect/src/components/IpAddressAndPort.tsx +++ b/nym-connect/src/components/IpAddressAndPort.tsx @@ -37,8 +37,12 @@ export const IpAddressAndPort: React.FC<{ return ( - {label} - Port + + {label} + + + Port + void; +}) => ( + Done} + > + + + Socks5 address + + + {ipAddress} + + + + + Port + + + {port} + + + + +); diff --git a/nym-connect/src/components/ServiceProviderSelector.tsx b/nym-connect/src/components/ServiceProviderSelector.tsx index b2fd29f42b..8eede5726a 100644 --- a/nym-connect/src/components/ServiceProviderSelector.tsx +++ b/nym-connect/src/components/ServiceProviderSelector.tsx @@ -1,9 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import IconButton from '@mui/material/IconButton'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle'; -import { Box, CircularProgress, Stack, Tooltip, Typography } from '@mui/material'; +import { Box, CircularProgress, Input, Stack, TextField, Tooltip, Typography, MenuItem } from '@mui/material'; import { ServiceProvider, Service, Services } from '../types/directory'; type ServiceWithRandomSp = { @@ -17,11 +13,8 @@ export const ServiceProviderSelector: React.FC<{ services?: Services; currentSp?: ServiceProvider; }> = ({ services, currentSp, onChange }) => { - const [service, setService] = React.useState(); + const [service, setService] = React.useState({ id: '', description: '', items: [] }); const [serviceProvider, setServiceProvider] = React.useState(currentSp); - const textEl = React.useRef(null); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); useEffect(() => { if (!serviceProvider && currentSp) { @@ -32,38 +25,34 @@ export const ServiceProviderSelector: React.FC<{ useEffect(() => { if (services && serviceProvider) { // retrieve the service corresponding to this service provider - setService( - services.find((s) => - s.items.some( - ({ id, address, gateway }) => - id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, - ), + + const match = services.find((s) => + s.items.some( + ({ id, address, gateway }) => + id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, ), ); + + if (match) { + setService(match); + } } }, [serviceProvider, services]); - const handleClick = () => { - setAnchorEl(textEl.current); - }; - const handleClose = (newServiceProvider?: ServiceProvider) => { + const handleSelectSp = (newServiceProvider?: ServiceProvider) => { if (newServiceProvider && newServiceProvider !== currentSp) { setServiceProvider(newServiceProvider); onChange?.(newServiceProvider); } - setAnchorEl(null); }; if (!services) { return ( - + theme.palette.common.white}> Loading services... - - - ); } @@ -78,49 +67,44 @@ export const ServiceProviderSelector: React.FC<{ [services], ); + if (!service) return null; + return ( - <> - - (serviceProvider ? undefined : theme.palette.primary.main)} - > - {service ? service.description : 'Select a service'} - - - - - - handleClose()} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'right', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - MenuListProps={{ - 'aria-labelledby': 'service-provider-button', + + {servicesWithRandomSp.map(({ id, description, sp }) => ( - handleClose(sp)}> + handleSelectSp(sp)}> @@ -145,7 +129,7 @@ export const ServiceProviderSelector: React.FC<{ ))} - - + + ); }; diff --git a/nym-connect/src/context/main.tsx b/nym-connect/src/context/main.tsx index 42bfed638e..5d2b049bf9 100644 --- a/nym-connect/src/context/main.tsx +++ b/nym-connect/src/context/main.tsx @@ -7,20 +7,26 @@ import { forage } from '@tauri-apps/tauri-forage'; import { ConnectionStatusKind } from '../types'; import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ServiceProvider, Services } from '../types/directory'; +import { Error } from 'src/types/error'; +import { TauriEvent } from 'src/types/event'; const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed'; type ModeType = 'light' | 'dark'; -type TClientContext = { +export type TClientContext = { mode: ModeType; connectionStatus: ConnectionStatusKind; connectionStats?: ConnectionStatsItem[]; connectedSince?: DateTime; services?: Services; serviceProvider?: ServiceProvider; + showHelp: boolean; + error?: Error; setMode: (mode: ModeType) => void; + clearError: () => void; + handleShowHelp: () => void; setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void; setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void; setConnectedSince: (connectedSince: DateTime | undefined) => void; @@ -39,6 +45,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const [connectedSince, setConnectedSince] = useState(); const [services, setServices] = React.useState([]); const [serviceProvider, setRawServiceProvider] = React.useState(); + const [showHelp, setShowHelp] = useState(false); + const [error, setError] = useState(); useEffect(() => { invoke('get_services').then((result) => { @@ -47,30 +55,45 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode }, []); useEffect(() => { - let unlisten: UnlistenFn | undefined; + const unlisten: UnlistenFn[] = []; // TODO: fix typings listen(TAURI_EVENT_STATUS_CHANGED, (event) => { const { status } = event.payload as any; console.log(TAURI_EVENT_STATUS_CHANGED, { status, event }); setConnectionStatus(status); + }) + .then((result) => { + unlisten.push(result); + }) + .catch((e) => console.log(e)); + + listen('socks5-event', (e: TauriEvent) => { + setError(e.payload); }).then((result) => { - unlisten = result; + unlisten.push(result); }); return () => { - if (unlisten) { - unlisten(); - } + unlisten.forEach((unsubscribe) => unsubscribe()); }; }, []); const startConnecting = useCallback(async () => { - await invoke('start_connecting'); + try { + await invoke('start_connecting'); + } catch (e) { + setError({ title: 'Could not connect', message: e as string }); + console.log(e); + } }, []); const startDisconnecting = useCallback(async () => { - await invoke('start_disconnecting'); + try { + await invoke('start_disconnecting'); + } catch (e) { + console.log(e); + } }, []); const setSpInStorage = async (sp: ServiceProvider) => { @@ -92,12 +115,17 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })(); if (spFromStorage) { setRawServiceProvider(spFromStorage); + setServiceProvider(spFromStorage); } } catch (e) { console.warn(e); } }; + const handleShowHelp = () => setShowHelp((show) => !show); + + const clearError = () => setError(undefined); + useEffect(() => { const validityCheck = async () => { if (services.length > 0 && serviceProvider) { @@ -122,6 +150,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode () => ({ mode, setMode, + error, + clearError, connectionStatus, setConnectionStatus, connectionStats, @@ -133,8 +163,20 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode services, serviceProvider, setServiceProvider, + showHelp, + handleShowHelp, }), - [mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider], + [ + mode, + error, + connectedSince, + showHelp, + connectionStatus, + connectionStats, + connectedSince, + services, + serviceProvider, + ], ); return {children}; diff --git a/nym-connect/src/context/mocks/main.tsx b/nym-connect/src/context/mocks/main.tsx new file mode 100644 index 0000000000..e2974413eb --- /dev/null +++ b/nym-connect/src/context/mocks/main.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ConnectionStatusKind } from 'src/types'; +import { ClientContext, TClientContext } from '../main'; + +const mockValues: TClientContext = { + mode: 'dark', + connectionStatus: ConnectionStatusKind.disconnected, + services: [], + showHelp: false, + serviceProvider: { id: '1', description: 'Keybase service provider', gateway: 'abc123', address: '123abc' }, + setMode: () => {}, + clearError: () => {}, + handleShowHelp: () => {}, + setConnectedSince: () => {}, + setConnectionStats: () => {}, + setConnectionStatus: () => {}, + setServiceProvider: () => {}, + startConnecting: async () => {}, + startDisconnecting: async () => {}, +}; + +export const MockProvider = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; diff --git a/nym-connect/src/index.tsx b/nym-connect/src/index.tsx index 2938840fff..66a3e11d17 100644 --- a/nym-connect/src/index.tsx +++ b/nym-connect/src/index.tsx @@ -4,16 +4,18 @@ import { ErrorBoundary } from 'react-error-boundary'; import { ClientContextProvider } from './context/main'; import { ErrorFallback } from './components/Error'; import { NymMixnetTheme } from './theme'; -import './fonts/fonts.css'; import { App } from './App'; +import { AppWindowFrame } from './components/AppWindowFrame'; const root = document.getElementById('root'); ReactDOM.render( - - + + + + , diff --git a/nym-connect/src/layouts/ConnectedLayout.tsx b/nym-connect/src/layouts/ConnectedLayout.tsx index a031f18a56..8e34415b39 100644 --- a/nym-connect/src/layouts/ConnectedLayout.tsx +++ b/nym-connect/src/layouts/ConnectedLayout.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Box } from '@mui/material'; +import { Box, Divider } from '@mui/material'; import { DateTime } from 'luxon'; -import { AppWindowFrame } from '../components/AppWindowFrame'; +import { IpAddressAndPortModal } from 'src/components/IpAddressAndPortModal'; +import { ConnectionTimer } from 'src/components/ConntectionTimer'; import { ConnectionStatus } from '../components/ConnectionStatus'; import { ConnectionStatusKind } from '../types'; -import { ConnectionStats, ConnectionStatsItem } from '../components/ConnectionStats'; -import { NeedHelp } from '../components/NeedHelp'; +import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ConnectionButton } from '../components/ConnectionButton'; import { IpAddressAndPort } from '../components/IpAddressAndPort'; import { ServiceProvider } from '../types/directory'; @@ -17,19 +17,32 @@ export const ConnectedLayout: React.FC<{ port: number; connectedSince?: DateTime; busy?: boolean; + showInfoModal: boolean; isError?: boolean; + handleCloseInfoModal: () => void; onConnectClick?: (status: ConnectionStatusKind) => void; serviceProvider?: ServiceProvider; -}> = ({ status, stats, ipAddress, port, connectedSince, busy, isError, serviceProvider, onConnectClick }) => ( - +}> = ({ + status, + showInfoModal, + handleCloseInfoModal, + ipAddress, + port, + connectedSince, + busy, + isError, + serviceProvider, + onConnectClick, +}) => ( + <> + - - - - + + + {/* */} + - - + ); diff --git a/nym-connect/src/layouts/DefaultLayout.tsx b/nym-connect/src/layouts/DefaultLayout.tsx index ba59b7f95d..ed42aad5fe 100644 --- a/nym-connect/src/layouts/DefaultLayout.tsx +++ b/nym-connect/src/layouts/DefaultLayout.tsx @@ -1,48 +1,48 @@ import React from 'react'; import { Typography } from '@mui/material'; -import { AppWindowFrame } from '../components/AppWindowFrame'; +import { Box } from '@mui/material'; +import { ConnectionStatus } from 'src/components/ConnectionStatus'; +import { ConnectionTimer } from 'src/components/ConntectionTimer'; +import { InfoModal } from 'src/components/InfoModal'; +import { Error } from 'src/types/error'; import { ConnectionButton } from '../components/ConnectionButton'; -import { ConnectionStatusKind } from '../types'; -import { NeedHelp } from '../components/NeedHelp'; import { ServiceProviderSelector } from '../components/ServiceProviderSelector'; -import { ServiceProvider, Services } from '../types/directory'; import { useClientContext } from '../context/main'; +import { ConnectionStatusKind } from '../types'; +import { ServiceProvider, Services } from '../types/directory'; export const DefaultLayout: React.FC<{ + error?: Error; status: ConnectionStatusKind; services?: Services; busy?: boolean; isError?: boolean; + clearError: () => void; onConnectClick?: (status: ConnectionStatusKind) => void; onServiceProviderChange?: (serviceProvider: ServiceProvider) => void; -}> = ({ status, services, busy, isError, onConnectClick, onServiceProviderChange }) => { - const [serviceProvider, setServiceProvider] = React.useState(); +}> = ({ status, error, services, busy, isError, onConnectClick, onServiceProviderChange, clearError }) => { const handleServiceProviderChange = (newServiceProvider: ServiceProvider) => { - setServiceProvider(newServiceProvider); onServiceProviderChange?.(newServiceProvider); }; + const { serviceProvider: currentSp } = useClientContext(); return ( - - - This is experimental software.
- Do not rely on it for strong anonymity (yet). -
- - Connect to the -
- Nym mixnet for privacy. + + {error && } + + + Connect to the Nym
mixnet for privacy.
+ - -
+
); }; diff --git a/nym-connect/src/layouts/HelpGuideLayout.tsx b/nym-connect/src/layouts/HelpGuideLayout.tsx new file mode 100644 index 0000000000..d56fd6b0ae --- /dev/null +++ b/nym-connect/src/layouts/HelpGuideLayout.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { HelpPage } from 'src/components/HelpPage'; +import { Button, Link, Stack } from '@mui/material'; +import Image1 from '../assets/help-step-one.png'; +import Image2 from '../assets/help-step-two.png'; +import Image3 from '../assets/help-step-three.png'; +import Image4 from '../assets/help-step-four.png'; + +export const HelpGuideLayout = () => { + const [step, setStep] = useState(0); + + if (step === 1) + return ( + setStep(2)} + /> + ); + + if (step === 2) + return ( + setStep(1)} + onNext={() => setStep(3)} + /> + ); + + if (step === 3) + return ( + setStep(2)} + onNext={() => setStep(4)} + /> + ); + + if (step === 4) + return ( + setStep(3)} + /> + ); + + return ( + + + + + ); +}; diff --git a/nym-connect/src/stories/AppFlow.stories.tsx b/nym-connect/src/stories/AppFlow.stories.tsx index ef7a1ea434..34064e6202 100644 --- a/nym-connect/src/stories/AppFlow.stories.tsx +++ b/nym-connect/src/stories/AppFlow.stories.tsx @@ -67,20 +67,25 @@ export const Mock: ComponentStory = () => { context.connectionStatus === ConnectionStatusKind.connecting ) { return ( - + {}} /> - + ); } return ( - + { + return undefined; + }} status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} @@ -101,6 +106,6 @@ export const Mock: ComponentStory = () => { }, ]} /> - + ); }; diff --git a/nym-connect/src/stories/ConnectedLayout.stories.tsx b/nym-connect/src/stories/ConnectedLayout.stories.tsx index 6063f83c28..53538457bf 100644 --- a/nym-connect/src/stories/ConnectedLayout.stories.tsx +++ b/nym-connect/src/stories/ConnectedLayout.stories.tsx @@ -11,8 +11,12 @@ export default { } as ComponentMeta; export const Default: ComponentStory = () => ( - + { + return undefined; + }} status={ConnectionStatusKind.connected} connectedSince={DateTime.now()} ipAddress="127.0.0.1" diff --git a/nym-connect/src/stories/DefaultLayout.stories.tsx b/nym-connect/src/stories/DefaultLayout.stories.tsx index 2c4ef513c2..ba311a410b 100644 --- a/nym-connect/src/stories/DefaultLayout.stories.tsx +++ b/nym-connect/src/stories/DefaultLayout.stories.tsx @@ -10,7 +10,24 @@ export default { } as ComponentMeta; export const Default: ComponentStory = () => ( - - + + {}} error={undefined} /> + +); + +export const WithServices: ComponentStory = () => ( + + {}} + error={undefined} + /> ); diff --git a/nym-connect/src/theme/index.tsx b/nym-connect/src/theme/index.tsx index 63f9db42fa..075aa7920a 100644 --- a/nym-connect/src/theme/index.tsx +++ b/nym-connect/src/theme/index.tsx @@ -3,12 +3,12 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { getDesignTokens } from './theme'; import { ClientContext } from '../context/main'; +import '../../../assets/fonts/non-variable/fonts.css'; /** - * Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context. + * Provides the theme for Nym Connect by reacting to the light/dark mode choice stored in the app context. */ -export const NymMixnetTheme: React.FC = ({ children }) => { - const { mode } = useContext(ClientContext); +export const NymMixnetTheme: React.FC<{ mode: 'light' | 'dark' }> = ({ children, mode }) => { const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]); return ( diff --git a/nym-connect/src/theme/mui-theme.d.ts b/nym-connect/src/theme/mui-theme.d.ts index a5b92073b3..ef042f6305 100644 --- a/nym-connect/src/theme/mui-theme.d.ts +++ b/nym-connect/src/theme/mui-theme.d.ts @@ -30,6 +30,7 @@ declare module '@mui/material/styles' { interface NymPalette { highlight: string; success: string; + warning: string; info: string; fee: string; background: { light: string; dark: string }; diff --git a/nym-connect/src/theme/theme.tsx b/nym-connect/src/theme/theme.tsx index 18064bf504..3c07506bf6 100644 --- a/nym-connect/src/theme/theme.tsx +++ b/nym-connect/src/theme/theme.tsx @@ -23,8 +23,9 @@ const nymPalette: NymPalette = { highlight: '#FB6E4E', success: '#21D073', info: '#60D7EF', + warning: '#FFE600', fee: '#967FF0', - background: { light: '#F4F6F8', dark: '#121726' }, + background: { light: '#F4F6F8', dark: '#1D2125' }, text: { light: '#F2F2F2', dark: '#121726', @@ -93,6 +94,9 @@ const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({ info: { main: nymPalette.info, }, + warning: { + main: nymPalette.warning, + }, background: { default: variant.background.main, paper: variant.background.paper, diff --git a/nym-connect/src/types/error.ts b/nym-connect/src/types/error.ts new file mode 100644 index 0000000000..599b959c39 --- /dev/null +++ b/nym-connect/src/types/error.ts @@ -0,0 +1,4 @@ +export type Error = { + title: string; + message: string; +}; diff --git a/nym-connect/src/types/event.ts b/nym-connect/src/types/event.ts new file mode 100644 index 0000000000..66d1e514a6 --- /dev/null +++ b/nym-connect/src/types/event.ts @@ -0,0 +1,6 @@ +export type TauriEvent = { + payload: { + title: string; + message: string; + }; +}; From f3e226b2bfd4ed61b2084f743dc8b8fed80331ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Mon, 28 Nov 2022 13:27:48 +0200 Subject: [PATCH 74/75] Use default serde value for upgrade (#1807) --- clients/client-core/src/config/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 4a3d1c1273..277fff30b9 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -315,6 +315,7 @@ pub struct Client { disabled_credentials_mode: bool, /// Addresses to nymd validators via which the client can communicate with the chain. + #[serde(default)] validator_urls: Vec, /// Addresses to APIs running on validator from which the client gets the view of the network. From 66257669fc9e985548f153c7d8dc0a3e0b5adb04 Mon Sep 17 00:00:00 2001 From: Fouad Date: Mon, 28 Nov 2022 12:26:11 +0000 Subject: [PATCH 75/75] fix ui overflow bug (#1808) --- nym-connect/src/components/AppWindowFrame.tsx | 4 +--- .../src/components/ServiceProviderSelector.tsx | 13 +------------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/nym-connect/src/components/AppWindowFrame.tsx b/nym-connect/src/components/AppWindowFrame.tsx index 5fcca9b3b3..5fc3d60ff6 100644 --- a/nym-connect/src/components/AppWindowFrame.tsx +++ b/nym-connect/src/components/AppWindowFrame.tsx @@ -5,10 +5,8 @@ import { CustomTitleBar } from './CustomTitleBar'; export const AppWindowFrame: React.FC = ({ children }) => ( t.palette.background.default, - borderRadius: '12px', - padding: '12px 16px', display: 'grid', + borderRadius: '12px', gridTemplateRows: '40px 1fr', bgcolor: 'nym.background.dark', height: '100vh', diff --git a/nym-connect/src/components/ServiceProviderSelector.tsx b/nym-connect/src/components/ServiceProviderSelector.tsx index abf6a08bff..9dee82fbb4 100644 --- a/nym-connect/src/components/ServiceProviderSelector.tsx +++ b/nym-connect/src/components/ServiceProviderSelector.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import { Box, CircularProgress, Input, Stack, TextField, Tooltip, Typography, MenuItem, ListItemIcon } from '@mui/material'; -import Check from '@mui/icons-material/Check'; +import { Box, CircularProgress, Stack, TextField, Tooltip, Typography, MenuItem, ListItemIcon } from '@mui/material'; import { ServiceProvider, Service, Services } from '../types/directory'; type ServiceWithRandomSp = { @@ -128,16 +127,6 @@ export const ServiceProviderSelector: React.FC<{ > {description} - {id === service?.id && ( - - - - )} ))}